Друг передал ссылку на Falcor, фреймворк JavaScript, который управляет обменом данными между клиентским приложением и внутренним сервером. Он был предоставлен Netflix с открытым исходным кодом и в настоящее время используется в качестве основной части их клиентских приложений.

Он передал его, потому что знал, что мой интерес (или, что более вероятно, скептицизм) будет возбужден их заявлением «Одна модель везде», что

«Вы кодируете одинаково независимо от того, где находятся данные, будь то в памяти на клиенте или по сети на сервере.

Один из самых глубоких принципов, который я укреплял с любым разработчиком, с которым я работал, заключался в том, что различие между локальным (быстрым, надежным, обычно синхронным) и удаленным (медленным, подверженным ошибкам, всегда асинхронным) было фундаментальным и основывалось на физике. Это была одна важная вещь, которую вы не хотели абстрагировать или скрывать, потому что и архитектура вашего приложения, и дизайн вашего пользовательского опыта должны учитывать это различие.

Я также попытался поделиться мыслями о том, как анализировать новые компоненты, технологии, архитектуры или приложения. Хотя ничто не сравнится с практическим использованием и измерением новой технологии, чтобы получить глубокое понимание, часто вам нужно быстро взглянуть на технологии и оценить их, и вы хотите иметь сильную ментальную модель, которую можно использовать для этого анализа.

Давайте посмотрим на Falcor как на пример этого процесса.

Обычно, конечно, вы начинаете с проверки ссылок - кто это написал, кто этим пользуется, кто поддерживает? Где он находится в активном производственном использовании? Давайте пока пропустим эту часть, хотя мы вернемся к ней чуть позже.

Давайте рассмотрим архитектуру приложения 101. Это часть нашей основной существующей ментальной модели, которую мы используем при анализе любой новой технологии.

У каждого приложения есть модель данных приложения. Эта модель может включать в себя некоторые данные, которые используются множеством пользователей, например, блок данных, представляющий текущую информацию о погоде в Сиэтле. Затем у него есть некоторые данные, которые настраиваются для конкретного пользователя - например, список городов, которые я хочу показать на главном экране моего погодного приложения. Для Netflix у них есть общий каталог видео, а затем история, рейтинги и предпочтения для каждого пользователя.

«Золотая» версия всех этих данных живет в облаке. Клиентское приложение запрашивает некоторое подмножество этих данных для загрузки в локальную модель данных в памяти (и, возможно, кэширует некоторые из них локально, чтобы в следующий раз запускаться быстрее или быть устойчивым к нестабильным соединениям).

Модель данных в памяти проецируется в представление или пользовательский интерфейс. Этот прогноз может быть очень простым или довольно сложным. По мере того, как пользователь предпринимает действия, приложение обновляется, отображая новые части локальной модели данных в представление, или может запрашивать дополнительные данные из облака для передачи в локальную модель данных, а затем, в конечном итоге, в представление. Другие действия пользователя могут изменять локальную модель данных и / или вызывать действия, которые изменяют пользовательские данные, хранящиеся в облаке, а затем отражаются обратно в приложение.

Вот и все. Практически каждое используемое вами приложение имеет такую ​​базовую структуру. Почему я читаю этот пост?

Ах да, я не учел производительность и сложность. Давайте поговорим и об этих аспектах. Начну с выступления.

В наши дни локальная модель данных может быть довольно большой и по-прежнему обеспечивать высокую воспринимаемую производительность. Если вы посмотрите на использование памяти на вкладке браузера Gmail или в приложении Microsoft Outlook, вы увидите, что приложения могут использовать сотни мегабайт памяти и по-прежнему обеспечивать быстрое и гибкое взаимодействие с пользователем. Я не утверждаю, что вам следует игнорировать использование памяти. На самом деле управление памятью имеет решающее значение, но по сравнению с другими частями приложения сокращение базовой модели данных вашего приложения, хранящейся в памяти, обычно не первое место, на которое нужно обратить внимание на повышение интерактивной производительности. Это особенно верно для реального ядра модели данных приложения, которое обычно оказывается очень маленьким по сравнению со всеми другими способами, которые программисты находят для использования памяти.

Сопоставление этой модели данных с представлением требует гораздо большей осторожности, особенно если модель данных велика. Буквально с момента появления интерактивных приложений рисовать пользовательский интерфейс было намного дороже, чем представлять состояние приложения в памяти. Это все еще правда. Приложение любой сложности каким-то образом виртуализирует пользовательский интерфейс, поэтому оно создает только дорогостоящие конструкции пользовательского интерфейса для элементов, которые пользователь может фактически видеть на экране.

Когда мы расширяли Word Web App для поддержки длинных документов, базовая модель данных для основного текста документа обычно была довольно маленькой даже для длинных документов (особенно по сравнению с любыми изображениями, которые могли быть в документе). Настоящая уловка для достижения хорошей производительности заключалась в минимизации количества HTML-элементов, создаваемых при отображении этой модели данных в объектную модель браузеров.

Быстрое и эффективное обновление представления в ответ на действия пользователя также является ключом к хорошей интерактивной производительности и обычно требует более сжатых сроков и большей сложности, чем создание представления в первую очередь. React от Facebook - это пример высоко ценимого фреймворка, который помогает решить эту очень сложную проблему. Ядро React не решает часть проблемы виртуализации, но хорошо взаимодействует со сторонними библиотеками компонентов, которые это делают.

Прежде чем мы сможем сохранить модель данных в памяти, мы должны загрузить ее с сервера. Это еще одно место в нашем потоке данных, где у нас есть несоответствие между этой емкой локальной памятью и производительностью канала, заполняющего эту память. Запрос может занять от 10 до 100 до 1000 миллисекунд (или бесконечно, если соединение не установлено). Одна особенность, которая актуальна практически для каждой части стека приложения, заключается в том, что группирование запросов и ответов может иметь огромное значение для эффективной производительности.

Это еще одна характеристика дизайна приложений, которая остается неизменной навсегда. Во многих случаях фиксированные накладные расходы и задержка составляют большую часть стоимости обработки одного или нескольких запросов, поэтому пакетирование нескольких запросов может привести к мультипликативному повышению эффективной производительности. Другая ключевая характеристика производительности заключается в том, что полоса пропускания практически всегда увеличивается быстрее, чем задержка на каждом уровне стека, поэтому, хотя задержка перед получением первого байта ответа улучшается только медленно (и всегда может широко варьироваться), могут быть получены более крупные ответы. относительно быстро, и размер ответа, который можно эффективно обработать, повсеместно улучшается гораздо быстрее, чем задержка. Это становится еще одним сильным аргументом как в пользу пакетной обработки, так и в пользу выполнения «больших запросов», а не множества мелких запросов.

Разработчики полного стека (известные как «разработчики» 30 лет назад :-)) стали более чувствительными к тому факту, что они также не могут просто игнорировать затраты на обработку сервера. Любой запрос, сделанный приложением, потребует некоторого количества операций ввода-вывода, использования памяти, вычислений и обмена данными в облаке. Это может показаться тривиально очевидным, но может быть удивительно, как многие разработчики клиентов думают, что «волшебство происходит» на другом конце провода. Тот факт, что затраты происходят далеко от устройства, на котором работает приложение, не меняет реальности затрат и последствий для производительности приложения.

Пакетирование также значительно помогает на стороне обслуживания.

Возможно, наиболее эффективный метод снижения затрат на обслуживание - это попытаться гарантировать, что данные, которые запрашивает приложение, уже доступны в минимально возможном количестве «фрагментов» (независимо от того, является ли этот фрагмент страницей на устройстве хранения в базе данных или блок в кэше памяти). Это часто связано с дублированием данных (или денормализацией в терминологии базы данных), чтобы все запрошенные данные можно было получить в одном низкоуровневом запросе ввода-вывода. Непрерывная проблема заключается в том, что вы теперь связываете динамическое поведение приложения (какие данные приложение хочет получить вместе) с вашим состоянием, находящимся в хранилище. Поскольку одна служба может обслуживать несколько клиентов с разной частотой обновления и разным динамическим поведением, необходимо соблюдать осторожность, чтобы избежать слишком тесной привязки клиентов и службы.

Здесь часто возникают трения и сложности, поскольку разработчики клиентов действуют, чтобы быстро реагировать на запросы новых функций (изменяя поведение нагрузки службы), а разработчики служб сосредотачиваются на стабильности службы, масштабируемости и являются надежными хранителями долгосрочных данных, хранящихся в службе.

Обратите внимание, что все это обсуждение касается того, как данные передаются из хранилища или памяти в службе по сети, в память на клиенте и в пользовательский интерфейс. Там, где вам нужно беспокоиться о задержках, пакетировании, локальности или управлении потоком, он оставался удивительно стабильным на протяжении десятилетий. Необработанный объем и объем обработки, который вы можете выполнить в узле системы до или после перемещения данных, заметно увеличились повсюду в конвейере, но там, где вам нужно быть осторожным, не изменились.

Эти инварианты делают так ценно наличие сильной ментальной модели при проведении такого рода анализа.

Хорошо, вернемся к Falcor. Вы должны были все прочесть перед этим, потому что это был просто обзор, подтверждающий наш общий ментальный контекст, верно?

Традиционно, когда компонент сообщает, что он собирается предоставить «одну и ту же модель программирования для локального и удаленного», это означает попытку спроецировать локальный шаблон API на удаленный ресурс. Это был классический исходный проект для удаленного обратного вызова процедур в Xerox Parc. В случае с Falcor вместо этого они предоставляют асинхронный (удаленный) шаблон для всех данных, даже если эти данные уже загружены в локальную модель данных. Безусловно смелый выбор.

Это и хорошо и плохо. Хорошая часть состоит в том, что они не скрывают ложно медленное блокирующее поведение за API, которое выглядит быстрым и синхронным. С практической точки зрения сделать это в JavaScript намного сложнее, чем при традиционном использовании собственных API-интерфейсов. Плохо то, что теперь они делают доступ даже к локальной модели данных медленнее и намного дороже как с практической точки зрения, так и с точки зрения структуры кодирования, необходимой для доступа к ней.

Falcor - это структура с «отношением». Этот выбор и выбор в основном просто предоставить возможность устанавливать и получать отдельные значения данных являются примерами, в которых дизайнеры Falcor демонстрируют, что их ожидаемое поведение приложения - это то, когда приложение «потягивает» в сети данные, напрямую сопоставляя эти данные с пользовательский интерфейс и, как правило, сводит к минимуму локальный контекст и обработку данных. (Или, по крайней мере, для модели данных приложения - приложение Netflix, созданное на основе Falcor, загружает изображения, передает потоковое видео и в совокупности является крупнейшим потребителем полосы пропускания в Интернете.) По сути, оно делает выбор игнорировать тот факт, что большой запросы (с хорошей локальностью) на самом деле могут быть относительно эффективными через стек, и эта локальная обработка данных также может быть сверхэффективной, если позаботиться о том, как эти данные отображаются в пользовательском интерфейсе.

Falcor агрессивно пакетирует эти индивидуальные запросы значений данных (во многих отношениях это ключевое функциональное значение, обеспечиваемое фреймворком), но его основная модель графа не делает никаких усилий, чтобы в противном случае подчеркнуть локальность ссылки в качестве ключевого требования к дизайну приложения.

Falcor также предназначен для владения локальной моделью данных («мы -« M »в MVC»). При использовании любого компонента всегда возникает вопрос, насколько тесно этот компонент становится встроенным в вашу общую базу кода (от изолированного и заменяемого до эффективного требования перезаписи для замены). Компонент, который берет на себя структуру модели приложения и коммуникацию сервисов, явно сильно встроен. Обычно это означает, что вам лучше поверить, что это отличный матч для вас сейчас и будет отличным матчем в будущем.

В этой части анализа также полезно посмотреть, как компонент используется первоначальными авторами и другими. Для Netflix такая категоричная структура выполняет дополнительную функцию по обеспечению стандартизации их набора приложений и отдельных групп, ответственных за них. Они используют структуру программного обеспечения для обеспечения соблюдения набора руководящих принципов и поведения для своих собственных групп разработчиков. Кроме того, когда вы владеете и фреймворком, и приложениями, у вас есть возможность перемещать их параллельно по мере необходимости при изменении требований. Microsoft Office будет активно использовать контроль над библиотеками, используемыми во всех приложениях Office, для внесения значительных последовательных изменений в функции и поведение приложения на уровне всего пакета. Мы могли бы связать основные изменения библиотеки с работой по внедрению этих изменений в приложения.

Сторонний пользователь фреймворка, развивающегося таким образом, страдает двумя способами. Внешне повлиять на общее направление фреймворка сложно. Когда фреймворк действительно перемещается, он имеет тенденцию двигаться более крупными рывками и сдвигами, что либо оставляет вас на какой-то более старой версии, которая больше не поддерживается, либо требует, чтобы вы вложили нетривиальные усилия для перехода на новую версию, которая может измениться в способы, которые не особенно важны для вашего развития. Это не заявление об особой истории Falcor, а повторяющаяся закономерность в подобной ситуации.

Это немного глупо, но я обнаружил, что их документация по функции JSON Graph раздражает, что является общим для многих фреймворков. Большинство нетривиальных моделей данных нельзя моделировать как простые деревья. В модели данных есть определенные объекты, на которые нужно ссылаться из разных мест. С точки зрения базы данных, вот почему вы нормализуете модель данных, разбиваете модель данных приложения на несколько таблиц и используете «внешние ключи» для связи между таблицами. Есть веские причины для стандартизации этого механизма в рамках (не в последнюю очередь потому, что их решение моделировать данные как одно дерево, а не разрешать использование нескольких деревьев требует этого). Раздражает то, что вы проводите все это время, читая о механизме, и должны выяснить для себя: «О, это всего лишь ваше локальное решение общей проблемы« X », которую нужно решить каждому». Фактически, часто из-за того, что «X» является таким ключевым общим архитектурным элементом, он имеет тенденцию наращивать дополнительные специфические для платформы функциональные возможности, и эта детализация документации еще больше затемняет, что его реальная основная цель - это всего лишь одна основная вещь.

Так как же все это свернуть? Сначала я повторю, что это должен быть пример анализа, в котором вы еще не выполнили более трудоемкую работу по созданию реальных прототипов, разговору с существующими клиентами и общей работе над компонентом. . Это может быть критически важной частью процесса для компонентов, от которых вы в конечном итоге будете зависеть, но вам также необходимо уметь организовать свое расследование и инвестиции, а некоторые проблемы возникают только со временем, а также после значительных инвестиций. По этой причине вам также нужна хорошая концептуальная модель ключевого значения, предоставляемого компонентом, в таком виде, который можно было бы довольно просто сформулировать и оценить.

Этот компонент явно является программным обеспечением, которое было неотъемлемой частью набора очень популярных приложений. Это большое одобрение.

Однако есть ряд других вещей, которые отвлекут меня от использования этого конкретного компонента. Его акцент на том, чтобы рассматривать доступ к локальным данным как дорогостоящий, является методом общей минимизации взаимодействия служб и нагрузки. Это может быть хорошо для модели приложений и услуг Netflix, но не является неотъемлемым требованием для приложений с хорошим поведением. Это фокус, который может вступить в конфликт с большим классом приложений.

Рассмотрение общей модели данных как графа и сосредоточение внимания на извлечении отдельных значений данных обеспечивает единообразный метод моделирования, но при этом игнорируется - почти намеренно скрывается - обеспечение сквозной локальности из модели приложения, Нагрузка службы через хранилище служб часто является ключевым методом повышения производительности приложения.

Тот факт, что фреймворк в значительной степени управляется одним набором частных приложений, делает несколько рискованным полагаться на него. Это означает, что это реальное производственное программное обеспечение, но существует риск, что его приоритеты сильно отличаются от ваших.

Falcor на самом деле не решает какую-то конкретную сложную проблему. Если ваше приложение тривиально, то создать собственное довольно просто. Если ваше приложение является сложным - или становится изощренным - то наличие фреймворка с особым, строго обязательным «отношением» означает, что вам либо нужно отлично подходить для этого отношения, либо вы в конечном итоге отказываетесь от него, пытаясь настроить фреймворк для выполнения того, что ты хочешь. Это, вероятно, усиливает диссонанс с любой будущей эволюцией фреймворка, так что вы теряете связь с широким кругом пользователей - не лучшее место для этого.

Главный вопрос, который я всегда задаю: «Какую ключевую сложную проблему решает для меня этот компонент»? В этом случае правильным ответом, вероятно, будет «предоставить жесткую модель, ограничивающую поведение приложения». Лучше это будет то, что вы ищете, если у вас такая глубокая зависимость.