Прим. перев.: недавняя статья от Uber Engineering рассказывает о путешествии этой крупной компании к своей улучшенной версии микросервисной архитектуры. Несмотря на то, что некоторые интернет-пользователи не без причин увидели в новом подходе «всего лишь применение принципов DDD к микросервисам», статья снискала огромный интерес у сообщества разработчиков и других инженеров. А посему — рады представить её русскоязычную версию, подготовленную специально для хабра.



Введение


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

В настоящее время в Uber насчитывается около 2200 критических микросервисов, и мы испытали все достоинства и недостатки этого подхода на себе. В течение последних двух лет Uber пыталась сократить запутанность микросервисного ландшафта, попутно сохранив преимущества данной архитектуры. С помощью этой публикации мы планируем представить наш обобщенный подход к микросервисным архитектурам, получивший название «Domain-Oriented Microservice Architecture» (DOMA).

Хотя в последние годы было популярно критиковать микросервисные архитектуры из-за их недостатков, мало кто осмеливался заявить о том, что от них нужно полностью отказаться. Их эксплуатационные преимущества слишком важны; кроме того, альтернатив этому подходу, похоже, нет (или они крайне ограничены). Цель нашего обобщенного подхода — помочь организациям, которые хотят снизить общую сложность системы, при этом сохранив гибкость, присущую МА.

В этой статье будет рассказано о DOMA, о тех проблемах, которые привели к возникновению этого подхода в Uber, его преимуществах для платформенных и продуктовых команд и, наконец, будут даны некоторые советы тем, кто хочет перейти на эту архитектуру.

Что такое микросервис?


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

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

Представляя микросервисы в таком ключе, мы можем задаться вопросом, зачем нам вообще микросервисная архитектура? Классический ответ на этот вопрос — из-за возможности независимо разворачивать отдельные компоненты и легко их масштабировать. В случае крупного, монолитного приложения организация вынуждена деплоить или выпускать весь код одновременно. В результате каждая новая версия несет в себе массу изменений. Развертывания становятся рискованными и отнимают много времени. Любая ошибка может обрушить всю систему.

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

Мотивации


На момент перехода на микросервисы (примерно в 2012-2013 гг.) у нас в Uber было два основных монолитных сервиса, и мы столкнулись с большим количеством эксплуатационных проблем, которые микросервисы успешно решают:

  • Риски доступности. Любая ошибка в кодовой базе монолита способна уронить всю систему (в данном случае весь Uber).
  • Рискованные и затратные развертывания. Их было очень тяжело проводить, и часто приходилось откатываться на предыдущую версию.
  • Плохое разделение зон ответственности. Было очень сложно уследить, кто и за что отвечает в колоссальной кодовой базе. В условиях экспоненциального роста поспешность иногда приводила к стиранию границ между логикой и компонентами.
  • Неэффективная работа. Вышеперечисленные проблемы в совокупности приводили к тому, что командам было сложно работать автономно или независимо друг от друга.

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

Поэтому мы решили перейти на МА. В результате наши системы стали более гибкими и позволили командам стать более автономными.

  • Надежность системы. Общая надежность системы растет при переходе на МА. Отдельный сервис может упасть (и его можно откатить на предыдущую версию), не рискуя обрушить всю систему.
  • Разделение ответственности. Из-за своего строения микросервисные архитектуры провоцируют вопросы вроде: «Почему этот сервис вообще существует?», — позволяя более четко определять роли различных компонентов.
  • Прозрачное владение. Становится гораздо проще отследить, кто и каким кодом владеет. Сервисами, как правило, владеют отдельные лица, команды или организации, что обеспечивает более быстрый рост.
  • Автономная работа. Возможность проводить независимые развертывания вкупе с более прозрачной принадлежностью кода способствует автономной работе различных продуктовых и платформенных команд.
  • Свободный темп. Возможность развертывать свой код независимо от других позволяет командам разработчиков работать в своем собственном темпе.

Без преувеличения можно сказать, что Uber не смог бы достичь нынешних масштабов и качественного уровня без МА.

Однако по мере дальнейшего роста компании и увеличения числа инженеров с сотен до тысяч человек, мы стали замечать ряд проблем, связанных с изрядно возросшей сложностью системы. В случае МА мы приносим в жертву единую монолитную кодовую базу в обмен на некоторое количество «черных ящиков», функциональность которых может измениться в любой момент и привести к неожиданному поведению.

Например, инженерам приходилось проанализировать ~50 сервисов в 12 различных командах, чтобы добраться до причины возникновения проблемы.

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


Микросервисная архитектура Uber по состоянию на середину 2018 года по версии Jaeger

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


Пример такой сложной системы в Uber (~2018 год) с десятью точками соприкосновения для простой интеграции (еще до появления DOMA).

В результате мы имеем замедление процесса разработки, нестабильность, бьющую по владельцам сервисов, более трудоемкие миграции и т.д. Увы, для организаций, которые уже перешли на МА, обратного пути нет. Ситуацию прекрасно иллюстрирует известная фраза: «И жить с ними невозможно, и пристрелить нельзя».

Предметно-ориентированная микросервисная архитектура


Микросервисы можно представить в виде библиотек, связанных по I/O, а микросервисную архитектуру — как огромное, распределенное приложение. В этом случае можно воспользоваться хорошо известными архитектурными решениями, чтобы подумать о том, как лучше организовать наш код.

Таким образом, Предметно-ориентированная микросервисная архитектура (Domain-Oriented Microservice Architecture, DOMA) может опираться на устоявшиеся способы организации кода, такие как Предметно-ориентированное проектирование, Clean Architecture, Сервис-ориентированная архитектура, объектно- и интерфейс-ориентированные паттерны разработки. Мы считаем DOMA инновационным подходом в том смысле, что это сравнительно новый способ задействовать имеющиеся принципы разработки в глобальных распределенных системах крупных организаций.

Вот некоторые базовые принципы DOMA и связанная терминология:

  1. Вместо того, чтобы рассматривать отдельные микросервисы, мы рассматриваем их группы. И называем их доменами (domains).
  2. Далее, мы объединяем домены в так называемые слои (layers). Слой, к которому принадлежит домен, определяет, какие зависимости доступны для микросервисов в этом домене. Получившуюся архитектуру мы называем многослойной (layer design).
  3. У доменов имеются четкие интерфейсы, которые служат единой точкой входа в группу микросервисов. Это так называемые шлюзы (gateways).
  4. Наконец, мы исходим из того, что каждый домен не должен зависеть от других доменов, то есть в его кодовой базе или моделях данных не должна присутствовать за'hardcode'нная логика, имеющая отношение к другому домену. Поскольку часто возникает необходимость включить логику в домен другой команды (например, некую логику проверки или мета-контекст в модели данных), мы разработали соответствующую архитектуру расширений (extension architecture) с четко определенными точками расширения внутри домена.

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

В остальной части этой статьи пойдет речь о реализации DOMA в Uber, ее преимуществах. Также будут даны практические советы компаниям, желающим перенять этот подход.

Реализация в Uber


Домены


Домены Uber представляют собой наборы из одного или нескольких микросервисов, связанных на основе логического объединения функций. Естественным образом возникает вопрос, насколько большим должен быть домен. В данном случае мы не даем никаких указаний. Некоторые домены могут включать десятки сервисов, другие — лишь один. Здесь важно хорошенько подумать о логической роли каждого объединения. Например, у нас в отдельные домены сгруппированы сервисы поиска на карте, сервисы платы за проезд, сервисы подбора (сопоставляющие водителей и пассажиров). Кроме того, они далеко не всегда повторяют организационную структуру компании. Подразделение Uber Maps разбито на три домена с 80 микросервисами, скрытыми за тремя различными шлюзами.

Архитектура на основе слоев


Многослойная архитектура отвечает на вопрос, какой сервис и с каким может связываться в границах МА Uber. То есть её можно рассматривать как глобальное распределение зон ответственности или же как механизм глобального управления зависимостями.

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


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

Стоит отметить, что функциональность часто перемещается вниз на этом графике: от узкой к более широкой. Можно представить себе некую простую функцию, которая со временем становится все более важной (»платформенной») по мере развития требований. На самом деле, такого рода миграция вниз ожидаема, и многие из ключевых бизнес-платформ Uber начинались как некая функция для водителей или пассажиров, и со временем она разрасталась и становилась более обобщенной по мере появления новых направлений бизнеса (таких как Uber Eats или Uber Freight) и подключения к ним большего числа зависимостей.

Внутри Uber мы выделяем следующие пять уровней.

  1. Инфраструктурный уровень. Открывает доступ к функциям, которые будут востребованы в любой инженерной организации. Этот уровень — ответ Uber на глобальные инженерные вопросы, такие как хранение данных или сетевое взаимодействие.
  2. Уровень бизнес-логики. На нем сосредоточен функционал, который может использовать Uber как компания, при этом он не специфичен для конкретной категории продуктов или вида деятельности, такого как Rides (поездки), Eats (доставка продуктов питания) или Freight (перевозка грузов).
  3. Уровень продукта. Включает функционал, который относится к определенной категории продукта или виду деятельности, но не зависит от мобильного приложения. Например, функция «request a ride» (запросить поездку) обрабатывается множеством приложений, работающих с Rides: Rider, Rider «Lite», m.uber.com, и т.д.
  4. Уровень представления. Включает функционал, который напрямую связан с функциями приложений (мобильных/веб), непосредственно взаимодействующих с потребителем.
  5. Пограничный уровень. Безопасно открывает сервисы Uber для внешнего мира. Этот уровень также учитывает особенности мобильного приложения.

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

Шлюзы


Термин «API-шлюз» уже хорошо укоренился в микросервисных архитектурах. Наше определение не сильно отличается от устоявшегося — за исключением того, что мы склонны рассматривать шлюзы как единую точку входа в соответствующую группу сервисов (которую называем доменом). Успех функционирования шлюза зависит от грамотной архитектуры API:


Эта схема иллюстрирует высокоуровневое строение шлюза. Он абстрагируется от деталей внутреннего устройства доменов: множества сервисов, таблиц с данными, ETL-пайплайнов и т.д. Другим доменам доступны только интерфейсы: API для удаленного вызова процедур, события и запросы в системе сообщений.

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

Расширения


Расширения (extensions), как и следует из названия, представляют собой механизм расширения доменов. Базовое определение такого дополнения: оно предоставляет механизм расширения функциональности некоего сервиса, не меняя внутренности этого сервиса и не влияя на его общую надежность. У нас в Uber имеются две модели расширения: логическая (logic extensions) и на основе данных (data extensions). Концепция расширений позволила нам масштабировать архитектуру с тем, чтобы множество команд могли работать независимо друг от друга.

Логические расширения


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

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

В случае же логических расширений endpoint под названием go online определит интерфейс, которому, как ожидается, будет соответствовать каждое расширение с заранее определенным типом запроса и ответа. Каждая команда зарегистрирует расширение, которое будет отвечать за реализацию этой логики. В данном случае они могут просто брать некоторую информацию о водителе и возвращать логическое значение (bool), которое определит, «достоин» водитель онлайн-статуса или нет. А сам endpoint (go online) просто переберет эти ответы и установит, являются ли какие-либо из них false.

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

Расширения на основе данных


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


Произвольные дополнения


Помимо логических и data-расширений, многие команды в Uber разработали собственные шаблоны расширений, подходящие к их доменам. Например, большинство интеграций, имеющих отношение к архитектуре представления, используют логику выполнения задач на основе DAG.

Преимущества


DOMA в той или иной степени повлияла почти на каждое крупное направление в Uber. За последний год мы преимущественно концентрировались на бизнес-слое. Он предоставляет обобщенную логику для различных направлений бизнеса компании.

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

Продукты и платформы


DOMA стала результатом совместных усилий различных команд, отвечающих за продукты и платформы в Uber. Во многих случаях затраты на поддержку платформ снижались на порядок. Продуктовые команды выиграли от конкретики и ускоренной разработки.

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

Снижение сложности


Ранее продуктовым командам приходилось работать со множеством нижестоящих сервисов в пределах домена, а теперь им достаточно вызвать только один. Благодаря сокращению числа точек соприкосновения при внедрении новой функции время внедрения сократилось на 25-30%. Кроме того, мы смогли распределить 2200 сервисов по 70 доменам. Примерно половина из них была реализована, и для большинства имеется план по внедрению в той или иной форме.

Будущие миграции


Мы в Uber подсчитали, что «период полураспада» микросервиса составляет 1,5 года. Другими словами, каждые полтора года 50% наших сервисов теряют актуальность. Без шлюзов микросервисная архитектура может превратиться в настоящий «миграционный ад». Постоянно меняющиеся микросервисы требуют постоянных upstream-миграций. Шлюзы позволяют командам избегать зависимостей от нижестоящих доменных сервисов, что означает, что эти сервисы могут меняться без необходимости проводить миграцию в upstream.

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

Новые направления бизнеса и продукты


Платформы на основе DOMA оказались гораздо более расширяемыми и легкими в обслуживании. Большинство команд в Uber, перешедших на DOMA, поступили так из-за того, что поддерживать новые направления деятельности стало слишком дорого.

Практические советы


В этом разделе я собрал некоторые практические советы для компаний, которые могут заинтересоваться DOMA. Руководящий принцип здесь состоит в том, что, по нашему опыту, зрелая и продуманная микросервисная архитектура базируется на постепенных сдвигах в нужном направлении в правильное время. В реальности полностью «переписать» МА практически невозможно.

Поэтому мы рассматриваем эволюцию МА скорее как этакий процесс «подрезки живой изгороди», благодаря которому та растет в верном направлении, а не как некое разовое, волевое усилие. Это динамический и постепенный процесс.

Стартапы


Ключевые вопросы здесь: «Когда нам следует переходить на МА?» и «Имеет ли это смысл для нашей организации?». Как мы видели выше, в то время как микросервисы обеспечивают эксплуатационное преимущество в организациях с большим количеством инженеров, они также увеличивают общую сложность, которая может затруднить реализацию новых функций.

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

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

Средний бизнес


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

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

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

Учитывая, что число микросервисов еще невелико, возможно, пока не имеет смысла объединять их вместе. Впрочем, здесь стоит отметить, что домен в контексте реализации DOMA в Uber вполне может включать единственный сервис, так что «домен-ориентированный» ход мыслей все же не помешает.

Крупный бизнес


Крупные инженерные организации могут насчитывать сотни специалистов, микросервисов и множество зависимостей. Именно в таких условиях DOMA полностью раскрывает свой потенциал. Наверняка в таких компаниях будут очевидные кластеры микросервисов, которые легко объединить в домены со шлюзами перед ними. Legacy-сервисы часто нуждаются в рефакторинге/переписывании и последующей миграции. Это означает, что шлюзы скоро начнут приносить реальную пользу в плане простоты миграции (если, конечно, они уже развернуты).

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

Заключительные мысли


Мы в Uber по-прежнему продолжаем активно развивать DOMA по мере перехода на нее все новых команд. Главная идея DOMA состоит в том, что микросервисная архитектура — это всего лишь одна большая распределенная программа. И к ее эволюции можно применять те же принципы, что и любому другому программному обеспечению. DOMA — это просто подход для практического размышления об этих принципах. Надеемся, что он будет вам полезен, и с нетерпением ждем обратной связи!

Сама DOMA стала результатом межфункциональных усилий почти 60 инженеров из всех подразделений Uber. Хочу выразить особую признательность следующим людям за их вклад в эту работу за последние 2 года:

Alex Zylman, Alexandre Wilhelm, Allen Lu, Ankit Srivastava, Anthony Tran, Anupam Dikshit, Anurag Biyani, Daniel Wolf, Deepti Chedda, Dmitriy Bryndin, Gaurav Tungatkar, Jacob Greenleaf, Jaikumar Ganesh, Jennie Ngyuen, Joe McCabe, Joshua Shinavier, Julia Law, Kusha Kapoor, Linda Fu, Madan Thangavelu, Nimish Sheth, Parth Shah, Shawn Burke, Simon Newton, Steve Sherwood, Uday Kiran Medisetty и Waleed Kadous.

Благодарность: эта работа объединила множество существующих в отрасли паттернов проектирования ради решения проблем в Uber, а также предложила некоторые новые паттерны (вроде расширений). Мы благодарны отрасли за работу над ними. Мы также признательны инженерам Linkedin, работавшим над Superblocks, которые рассказали нам о своем опыте.

P.S. от переводчика


Читайте также в нашем блоге: