Выбранный UI-фреймворк – вред. Архитектурные требования – профит



    Мы не замечаем, но услуги и продукты, которыми мы пользуемся, постоянно усложняются.

    • Войти в метро теперь – не просто кинуть пятачок, а приложить карту Тройка, записанную на телефон и учитывающую пересадку.
    • Позвонить по телефону и посмотреть телевизор – давно уже не провести два провода в квартиру и вносить фиксированную абонентскую плату, а triple play с кучей опций и возможностей.
    • Посмотреть дневник сына – на святое же покусились! – теперь можно с планшета, заодно ответив на комментарий классного руководителя о его неудовлетворительном поведении.

    Ну и я уже молчу про всякие Tinkoff, Apple Pay, Google Now, умные дома и многое другое.

    Как следствие, в любой компании растут IT-отделы. То, чем раньше занимались несколько десятков сотрудников, сейчас делают команды из тысяч и десятков тысяч человек (кстати, поделитесь в комментариях, как выросли ваши IT-отделы).

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

    Технологическое развитие – увеличение скорости, объема и удобства обмена информацией – позволило большим IT-коллективам не собираться в одном здании, а работать распределенной командой. И вот уже у нас огромные open-source проекты, где много команд из различных компаний, стран и континентов работают над одним продуктом.

    Совместная разработка большой распределенной командой принесла следующие особенности, которые, в свою очередь, выразились в развитии соответствующих инструментов, фреймворков и передовых практик:

    • Декаплинг. Строгое разделение на back-end и front-end. REST-сервис под каждый отдельный UI.
    • Микросервисы. Каждая команда независимо разрабатывает свою часть, но все эти части должны сложиться в одно работающее приложение.

    Эти особенности фактически развязали руки front-end разработчикам и позволили разным командам выбирать front-end технологии в зависимости от бизнес-задач, экспертизы команды, предпочтений и «религии».

    Во многом поэтому мы сейчас наблюдаем активный рост и отсутствие явного и безоговорочного лидера в UI-фреймворках: AngularJS, ReactJS, EmberJS, ExtJS и др. При этом, вполне вероятно, надо быть готовым к тому, что завтра появится новая front-end технология, которая будет более эффективной и будет более активно поддерживаться/развиваться сообществом. Именно поэтому считаю неправильным сейчас делать долгосрочный выбор и останавливаться на одной front-end технологии. Более того, вести разработку надо так, чтобы front-end технология была относительно дешево заменяемой, всегда держать это в голове при разработке архитектуры конкретного приложения. Это задача номер один.

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

    Для решения этих задач мы, front-end практика в компании Netcracker, разрабатываем архитектурные требования, которые обязательны для всех продуктов.

    Немного теории модульного подхода


    В основе архитектурных требований лежит подход, который подразумевает независимые JS-блоки и их встраивание друг в друга. У этого подхода есть несколько названий: Scalable JS Apps, Modular JavaScript, JS Portals. Такие фреймворки сравнительно молоды и, можно сказать, сделаны по принципам Николаса Закаса: если можете писать, просто пишите монолит, если нужна интеграция с различными блоками, то придется использовать модульный подход.

    Терминология


    • Application (конечное приложение) – набор html/js/css, который отображается пользователю. Конечное приложение может в общем случае не использовать принципы Modular JavaScript и быть написано на любом языке, хоть на flash.
    • Application Controller – JS-объект, реализующий принципы Modular JavaScript, позволяющий модулям общаться и встраиваться рядом и друг с другом.
    • Module – JS-приложение на любом языке, подобном JS.
    • Sandbox – объект, через который Module может общаться с внешним миром.
    • Service – утилитарные объекты, имеющие логику, но не имеющие UI.
    • BroadcastService – сервис, позволяющий модулям общаться.
    • Plugin – встраиваемый в Application Controller модуль. Application Controller может использовать его, чтобы получить новый функционал.



    Modular JavaScript. Принципы


    Принципы основаны на том, что мы считаем каждый модуль ребенком. Поэтому к ним применимы следующие правила:

    1. Модуль может вызывать только свои методы или методы Sandbox.
    2. Нельзя смотреть на DOM вне своего Sandbox.
    3. Нельзя трогать ненативные глобальные переменные.
    4. Если модулю что-нибудь нужно, это нужно спросить у Sandbox.
    5. Не разбрасывать игрушки (модуль не должен создавать глобальные переменные).
    6. Не разговаривать с незнакомцами (модуль не должен напрямую вызывать другой модуль).

    Оригинал можно прочитать здесь.

    Эти принципы реализованы Николасом Закасом в ряде проектов, например здесь: http://t3js.org. Однако есть альтернативные реализации: http://openf2.org/ (хотя принципы там те же, о них рассказывается в видео).

    Взяв за основу эти принципы, мы начали уточнять и развивать их в наших проектах.

    Наша специфика и архитектура


    Уточнения и дополнительные принципы, которые диктуются спецификой наших проектов:

    1. Сервис не имеет UI;
    2. Сервис обычно singleton;
    3. Модули могут общаться через сервис общения (это может быть EventBus или publish/subscriber);
    4. Есть только один сервис общения, общий для всех модулей;
    5. Поскольку мы имеем дело с JS, нужен статический анализатор кода (например, ESLint), который запретит внесение изменений в код в случае нарушения принципов;
    6. Реализация Modular JavaScript должна быть JS Agnostic, т. е. модуль может быть написан на любом языке, подобном JS;
    7. Необходима поддержка модуля в модуле, так как часто хочется переиспользовать код (например, модуль table может быть отрисован внутри модуля dashboard, который, в свою очередь, рисуется на модуле tab navigation – наподобие панели c вкладками для переключения);
    8. Из-за того, что модуль не может выйти за рамки своего элемента, необходим DialogManagerService, который управляет body для показа диалогового окна; модуль, который хочет показать диалоговое окно, использует модуль dialog и передает его в сервис;
    9. Поскольку модулей в проекте может быть много, компилирование всех в один пакет или предварительное соединение ссылками может привести к проблемам с производительностью, поэтому модули должны уметь подгружаться асинхронно по требованию и, естественно, запускаться только после того, как подгружены все другие модули и сервисы, от которых они зависят; отсюда следует, что нам понадобится устранять конфликты между зависимыми модулями.

    И вот какая получается архитектура



    Блок T3-NC-powered представляет собой Application Controller. У Application Controller есть набор базовых плагинов (треугольники), которые дополняют его функциональность. Плагины не имеют доступ к Application Controller. Наиболее важным является плагин, позволяющий общаться с сервером для «ленивой» загрузки модулей.

    Сервисы могут обмениваться данными с Application Controller. Наиболее важным является сервис для обмена сообщениями между модулями. Также сервисы могут использовать плагины. Сервисы не имеют UI, так как предоставляют JS-функции.

    При старте модуля по его имени создается Sandbox, который ограничивает модуль. Основной API – дает возможность получить сервис и DOM element, в который модулю нужно встроиться. Если необходимо запустить два модуля с одинаковым именем, то Application Controller создаст два экземпляра модулей, внутренние id которых будут разными.

    Заключение


    Совместная разработка большой распределенной командой принесла декаплинг и микросервисы, что развязало руки front-end разработчикам и позволило разным командам выбирать различные front-end технологии. Это могло бы породить хаос и превратиться в бесконечный спор.

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

    Если вы интересуетесь модульным подходом или уже вовсю используете его, пишите в комментариях – давайте делиться опытом. Мы в свою очередь готовы дать больше технических деталей, если у сообщества будет интерес.
    Netcracker
    международная компания-разработчик IT-решений

    Comments 21

      +5
      Вы как-то подотстали в своём развитии от 2016-го года. У вас нет нового ключевого слова «компонент», хотя в пункте 7 и в углу картинки вы жестами его изобразили. Кто не произносит этого слова, таких даже не берут в космонавты. На что вы рассчитывали, выходя в тусовку в разгаре 2017-го и не произнося «компоненты»?
        +2

        А разве "компоненты" были ещё не в Delphi? Класс TComponent и всё такое. Даже плевались потом, говорили, мол, компоненты плодят антипаттерны. Или сейчас другие компоненты?

          +1

          А можете про антипаттерны в двух словах?

            +1

            Аж в Википедии увековечено: https://ru.m.wikipedia.org/wiki/%D0%90%D0%BD%D1%82%D0%B8%D0%BF%D0%B0%D1%82%D1%82%D0%B5%D1%80%D0%BD


            "Волшебная кнопка (Magic pushbutton): Выполнение результатов действий пользователя в виде неподходящего (недостаточно абстрактного) интерфейса. Например, в системах типа Delphi это написание прикладной логики в обработчиках нажатий на кнопку"

              0

              Ээээммм… Я про


              говорили, мол, компоненты плодят антипаттерны
                +1

                Всё верно. В дельфи кнопка — это компонент класса TButton, унаследованный от TComponent


                Мой комментарий был про то, что термин "компонент" немножко более древний и что люди употребляли термин "компонент" задолго до 2016 года

                  +1

                  Вы по-прежнему не объяснили, почему компоненты плодят антипаттерны

                    0
                    «Компонент» в понимании Delphi поощряет написание кода в формате «волшебная кнопка», размазывая бизнес-логику по обработчикам событий компонентов вместо того, чтобы собирать её в одном классе/модуле. Что имел в виду под компонентом товарищ spmbt, я не знаю, ещё раз повторюсь, что хотел указать лишь на возраст термина
        +4
        Для удовлетворения хотелок фронт-энд разработчиков пощупать разные фреймворки, такая архитектура хороша, наверное. Но с точки зрения бизнеса это лишь усложняет поддержку. Да и непонятно зачем зоопарк фреймворков на клиенте. Придумывать и реализовывать архитектуру ради того чтобы поиграть с ангуларом и реактом одновременно… зачем?
          0
          Вы правы, это не везде применимо. В простых проектах такая архитектура приносит больше сложностей, чем профита.

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

          Тут наверное стоит упомянуть закон Конвея, сформулированный в 1967году:
          «Организации, проектирующие системы, … производят их, копируя структуры коммуникации, сложившиеся в этих организациях»
            0
            По моему опыту, в больших проектах это приносит еще больше сложностей, чем в простых. Даже банально исправление багов в разных частях такого продукта придется поручать разным людям. Либо человеку придется изучать несколько фреймворков и переключать мозг между ними довольно быстро, если баг затрагивает несколько модулей, либо нужно поправить несколько багов.
            Плюс дополнительное обслуживание механизма «дружбы» всех этих модулей.
            Ваш подход может быть оправдан, когда модули между собой не связаны, например бэк-энд и фронт-энд на разных клиентских фреймворках могут работать.
            И я даже не буду сейчас расписывать риски связанные с миграцией разработчиков с одновременным разрастанием зоопарка.
              0
              Берите команду еще больше ;)
              Нет таких кейсов:
              — исправление багов в разных частях такого продукта придется поручать разным людям
              — миграцией разработчиков.

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

              Взгляд с моей колокольни:


              Application (конечное приложение) – набор html/js/css, который отображается пользователю. Конечное приложение может в общем случае не использовать принципы Modular JavaScript и быть написано на любом языке, хоть на flash.

              Приложение — такой же компонент, как и остальные, только больше.


              Application Controller – JS-объект, реализующий принципы Modular JavaScript, позволяющий модулям общаться и встраиваться рядом и друг с другом.

              Для любого компонента контроллером является его владелец — компонент выше по иерархии.


              Module – JS-приложение на любом языке, подобном JS.

              Модуль — набор файлов на разных языках, даже не подобных JS


              Sandbox – объект, через который Module может общаться с внешним миром.

              Компонент не общается с внешним миром, а если и общается, его владелец легко может завернуть общение на себя.


              BroadcastService – сервис, позволяющий модулям общаться.

              Скажем нет этому клубку ниток. Владелец нескольких компонент полностью контролирует коммуникации его подопечных.


              Модуль может вызывать только свои методы или методы Sandbox.

              Модуль может вызывать только свои методы и ничего не знать о внешнем мире.


              Если модулю что-нибудь нужно, это нужно спросить у Sandbox.

              Если модулю что-то нужно — пусть сам себе это и создаст. Кому не понравится поведение по умолчанию — переопределит.


              Не разбрасывать игрушки (модуль не должен создавать глобальные переменные).

              Все имена в нелокальных пространствах мён должны иметь префикс с именем модуля.

                0

                Кх кх, здравствуйте)

                +1
                Расскажите, как у вас реализована транзакционность между микросервисами
                  0
                  Я не автор, но могу описать некоторые из подходов.
                  1. разбить на шаги, каждый из которых это atomic message.
                  Начал проводить платеж, снял деньги с одного, записал состояние в очередь из которой два выхода — снял со второго и закомитил либо не получилось и сложил новое событие на возврат денег первому.
                  Этакий конечный автомат и по сути ручная многоступенчатая транзакция на бизнес логике. Иногда без этого никак если между шагами 1 и два есть например внешний сервис (допустим надо провести проверку на мошенничество, которая может занять время). А деньги снимаются, чтобы параллельный перевод не схватил.
                  2. логическое укрупнение сервисов, чтобы транзакция целиком проходила внутри сервиса.
                  Получается довольно редко, но иногда прокатывает если удается по бизнес логике.
                  3. распределенная транзакция.
                  Это может быть как двухфазный коммит (условно если разные базы) или передача transactionId как параметра.
                    0
                    Хм, понятно, спасибо. Вообщем все очень плохо. Для меня это единственный фатальный недостаток, когда заходит речь о переезде бизнес-логики на микросервисную архитектуру в большом проекте.
                      0
                      Можно в микросервисы вынести части которые не требуют ACID — отчеты там и отображения, куча всего связанного с UI, логами и т.п.
                      Вытащить отдельно кучу интеграций — когда надо данные залить в систему извне или вылить наружу.
                      Монолит прилично урежется, останется один (ну или небольшое количество если удачно поделите) большой сервис который транзакционный.
                      Будет пачка микро и один макро сервис уже позитив. Время деплоя меньше, части менее зависимы. Хотя оркестрация тоже геморрой.
                        0
                        Лучшее (но не идеальное) решение этой проблемы я встретил в статье
                        https://www.infoq.com/articles/microservices-aggregates-events-cqrs-part-1-richardson

                        Если кратко:
                        — кластер доменов это агрегат (микромонолит). Внутри агрегата обычные транзакциями.
                        — приложение строится на CQRS + EventSourcing принципах
                        — консистентность между сервисами обеспечивается на уровне обмена сообщениями. Т.е. глобальной консистентноости нет, но каждый микросервис в отдельный момент времени находится в консистентном состоянии
                        — в статье не указано, но на практике это важно. Повторная отправка + идемпотентность для сообщений

                  Only users with full accounts can post comments. Log in, please.