Автор Шэн Хэ, по прозвищу Байча на Алибабе. Шэн Хэ из отдела исследований и разработок местного среднего бизнеса Alibaba. У него многолетний опыт разработки транзакционных систем.

Я присоединился к бизнес-отделу Ele.me, службы доставки еды Alibaba, в мае 2017 года и разработал ряд систем, связанных с поиском, заказом, тайм-аутом, компенсацией, соглашениями, доставкой, подсчетом суммы и рейтингом. Позже я также занимался обновлением системы.

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

Сотрудник Alibaba Биксуан написал следующие слова в статье «Процедура проектирования системы».

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

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

"Программное обеспечение

Как однажды сказал Роберт Сесил Мартин, термин «программное обеспечение» значимо включает в себя понятие «программное обеспечение», что подразумевает, что программное обеспечение является фундаментально гибким продуктом.

Код первой редакции системы транзакций можно проследить до восьми лет назад, когда система была разобрана и реконструирована. Когда я пришел в Ele.me в 2017 году, основная система выглядела примерно так:

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

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

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

Это требование повлекло за собой несколько модификаций нескольких подмодулей рейтинга. Эта рабочая нагрузка намного превзошла наши первоначальные ожидания. Естественно, что бизнес-команда не удовлетворилась. Подобные конфликты часто возникают и в других системах. Команда R&D не смогла разработать новые функции, внесла лишь незначительные изменения в систему.

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

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

Исходный код - это дизайн

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

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

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

Парадигмы программирования

Кодирование - это отправная точка для проектирования системы снизу вверх. Система транзакций Ele.me была разработана на основе Python, который достаточно гибок, чтобы быстро создавать версии системы MVP. Это было идеально адаптировано к статусу разработки компании в то время: быстрая итерация продукта и высокое давление со стороны новых проектов.

Недавняя реконструкция использует Java, чтобы соответствовать тенденциям развития компании. В конце 2017 года мы скомпилировали несколько новых сервисов, используя Go, потому что мы ожидали, что существующая системная структура столкнется с узким местом, когда количество заказов достигнет следующего уровня. Однако некоторые разработчики не привыкли компилировать сервисы с помощью Go. Это было связано с отсутствием рамок, общей модели и пробной версии. Go - не лучший выбор для решения бизнес-задач. Однако простой синтаксис Go может минимизировать вероятность ошибок со стороны программистов.

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

Опираясь на свой опыт программирования на C ++, Go, Python и Java, я считаю, что парадигмы программирования являются важным способом изучения любого языка программирования. Проще говоря, парадигмы программирования помогают программистам определять, что такое программа. К сожалению, их часто игнорируют. Старая система транзакций была скомпилирована исключительно с помощью процедурно-ориентированного программирования (POP), без учета бизнес-логики, и подобный код имеется в большом количестве.

Мы, кажется, забыли об объектно-ориентированном программировании (ООП), похоже, уменьшается, но я не хочу сказать, что ООП является оптимальной парадигмой программирования. Мы сторонники проблемно-ориентированного программирования. Например, ООП является основным компонентом Java, но не обязательно требуется бизнес-процессами. POP подходит для бизнес-процессов, когда каждый шаг четко определен. В этом случае сложный дизайн классов не нужен и может даже вызвать проблемы.

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

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

Принципы и закономерности

Разница между плохим программистом и хорошим состоит в том, считает ли он свой код или свои структуры данных более важными. Плохие программисты беспокоятся о коде. Хорошие программисты беспокоятся о структурах данных и их отношениях. - Линус Торвальдс

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

  • Жесткий. Модификация программного обеспечения сложна и часто требует дополнительных изменений. Например, когда к услуге заказа добавляется новый тип маркетинга, центр заказов и связанные с ним восходящие и нисходящие элементы должны воспринимать изменение и вносить соответствующие изменения.
  • Уязвимо. Простые изменения могут вызвать непредвиденные проблемы, которые могут не иметь отношения к их целям.
  • Неподвижный: конструкция включает полезные части других систем, но разделение этих частей рискованно и дорого. Для Ele.me проблема заключалась в том, что центр заказов может не поддерживать оплату членскими картами или другими виртуальными средствами доставки на вынос.
  • Неоправданно сложная. Система чрезмерно спроектирована.
  • Неизвестно: со временем становится все труднее понимать модули и код. Опять же, для Ele.me проблема заключалась в том, что основной код функции корзины покупок превратился в большую функцию, охватывающую почти 1000 строк.

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

ТВЕРДЫЙ

Эти принципы подразделяются на принцип единой ответственности (SRP), принцип открытого закрытия (OCP), принцип замещения Лискова (LSP), принцип разделения интерфейса (ISP) и принцип инверсии зависимостей (DIP), которые вместе обозначаются аббревиатурой SOLID. Далее мы приведем примеры некоторых из этих принципов.

  • Принцип единой ответственности (SRP): один программный модуль отвечает только за один тип пользователей. Следовательно, код и данные, тесно связанные с типом пользователя, должны быть организованы вместе. В большинстве случаев мы обнаруживаем, а затем разделяем обязанности.

На мой взгляд, определение пользователя лежит в основе SRP. Я хотел бы процитировать определение пользователя Ю Цзюнь из QCon 2018: «Пользователь - это не человек, а набор требований». В процессе реконструкции у нас были дебаты о процедуре доставки. системы транзакций. В настоящее время Ele.me поддерживает распространение по следующим показателям: продавцы, распространение, управляемое платформой, и выборочное распространение, например по поручениям. Эти режимы распределения имеют разные модели ценообразования, логику распределения и сценарии. Поэтому изначально мы разделили наш код на основе этих различий.

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

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

  • Принцип инверсии зависимостей (DIP): Некоторые считают, что инверсия зависимостей различает ООП и POP на основе зависимости, созданной во время процедурного проектирования. Политика зависит от деталей. Я имею в виду, что верхний уровень зависит от нижнего уровня. В результате политика может быть изменена деталями. Например, вы можете выполнить POP следующим образом, чтобы позволить продавцам раздавать купоны пользователям, которые не могут получать заказы на вынос.

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

Однако проблема может быть решена еще более элегантно на основе DIP.

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

  • Открыто-закрытый принцип (OCP): OCP - это первая цель проектирования системы и конечный результат других принципов. Например, модули каждой бизнес-линии разделяются на основе SRP, что меняет статус изоляции. Платформа реализует абстракцию для фильтрации основных бизнес-процессов, определяемых каждым бизнес-направлением. Для этого требуется DIP.
  • Инверсия управления (IoC) - еще один принцип программирования. Например, на платформе транзакций на вынос пользователь платит за еду, а затем продавец доставляет еду. Это требует прочной связи между пользователем и продавцом, которые должны встретиться лично для завершения транзакции. Ele.me предоставляет гарантию на сделку. Платеж пользователя переводится на Ele.me, а затем продавец принимает заказ и доставляет блюдо. Наконец, Ele.me переводит платеж продавцу после того, как пользователь подтверждает получение еды. Прямая зависимость и контроль между продавцом и покупателем инвертируются, так что другая сторона зависит от интерфейса стандартной модели транзакции.

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

Эволюция шаблонов

Здесь под шаблонами я подразумеваю шаблоны проектирования. Я использую слово «эволюция», потому что шаблоны проектирования - это не начало, а цель дизайна. Книга Шаблоны проектирования не основана на оригинальных идеях автора, но содержит общие практики, накопленные во многих реальных системах. Эти практики систематически организованы и впервые представлены в этой книге. Шаблоны проектирования могут естественным образом отражаться в системном коде, если мы соблюдаем предыдущие принципы. В статье Гибкая разработка программного обеспечения: принципы, шаблоны и практики в одной главе описывается процесс, с помощью которого сегмент кода медленно эволюционирует до шаблона Observer посредством настройки.

Шаблоны проектирования полезны. Например, мы можем использовать шаблон Template Method для определения полного набора шаблонов синтаксического анализа параметров поиска для поисковой системы и настройки различных требований к запросам, просто добавляя конфигурации. Не используйте шаблоны проектирования для программирования. Рассмотрим в качестве примера конечный автомат системы транзакций. Конечный автомат похож на лампу с переключателем Вкл. / Выкл., Но более сложен в сценариях транзакций. В сценарии транзакции вывода существует следующая модель перехода между состояниями:

Вложенный переключатель и операторы case могут реализовать этот ограниченный конечный автомат. Упрощенный пример кода выглядит следующим образом.

public class Order {
    // States
    public static final int ACCEPT = 5;
    public static final int SETTLED = 9;
    ..
    // Events
    public static final int ARRIVED = 1; // 订单送达
    
    public void event(int event) {
        switch (state) {
            case ACCEPT:
                switch (event) {
                    case ARRIVED:
                        state = SETTLED;
                        //to do action
                        break
                    case 
                            
                 }
        }  
    }
}

Пример кода кажется приемлемым, потому что процесс упрощен. Однако для сложного конечного автомата, который управляет статусом заказа, операторы switch и case могут неограниченно расширяться, что затрудняет чтение кода. Другая проблема заключается в том, что логика и действие конечного автомата не разделены. Шаблоны проектирования предлагает реализовать шаблон состояния следующим образом.

Шаблон состояния разделяет действие и логику конечного автомата. По мере увеличения числа состояний новые классы состояний усложняют систему. OCP эффективно не поддерживается. Новые классы вызывают изменения в классе перехода состояний, а логика конечного автомата скрыта в дискретном коде.

Старая система транзакций была реализована путем анализа таблиц миграции. Упрощенный пример кода выглядит следующим образом.

# 完结订单
add_transition(trigger=ARRIVED,
               src=ACCEPT,
               dest=SETTLED,
               on_start=_set_order_settled_at,
               set_state=_set_state_with_record, // 变更状态
               on_end=_push_to_transcore)
...

# 引擎
def event_fire(event, current_state):
    for transition in transitions:
        if transition.on_start == current_state && transition.trigger == event:
            transition.on_start()
            current_state = transition.dest
            transition.on_end()

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

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

Богатые значения домена

Трудно объяснить значение слова «красивый» , не упоминая вещи с этим качеством.

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

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

Языки программирования общего назначения

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

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

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

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

Граничный контекст

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

Граничный контекст может эффективно изолировать различные коннотации одного и того же. Мы можем получить доступ к объектной модели контекста на основе строгих спецификаций для защиты согласованности поведения бизнес-абстракции. В области транзакций Ele.me выступил с инициативой поддержки SVIP. Расчет по SVIP должен осуществляться системой транзакций. Мы сделали его менее сложным, проанализировав и разделив проблемы на домен участника и домен транзакции. Мы разработали отображение для защиты внутренней бизнес-логики транзакций, когда карты SVIP были введены в область транзакций.

Расщепление

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

  • Расширение: пакет расширения хранит пакет бизнес-настроек. Ориентированный на объекты, пакет расширения поддерживает переключение логики программы с помощью полиморфного языка программирования и плагина. История технологий разработки программного обеспечения - это процесс добавления простых в использовании плагинов для построения масштабируемой и поддерживаемой системной архитектуры.
  • Домен: пакет домена хранит основной бизнес-пакет на языке общего назначения домена. Пакет домена - самый стабильный из всех пакетов.
  • Бизнес: бизнес-пакет хранит определенную бизнес-логику. Если пакет домена предоставляет метод people.run(), бизнес-пакет использует этот метод для доставки еды на вынос или тренировок.
  • Инфра: пакет инфраструктуры хранит зависимости от баз данных и промежуточного программного обеспечения, то есть детали, выходящие за рамки бизнес-логики.

Теперь перейдем к наслоению зависимостей. Мартин Фаулер предлагает классическую модель многоуровневой инкапсуляции. В качестве примера используется упрощенный модуль заказа.

Если вы не хотите выполнять различные типы преобразования или соблюдать строгие уровни зависимости и думаете, что некоторые запросы (Query, Query! = Read) могут обходить уровень домена, то вы можете использовать модель CQRS.

В идеале уровень домена как основная бизнес-логика не зависит от деталей инфраструктуры. Это делает код более предсказуемым.

После того, как одно приложение разделено на отдельные компоненты, мы сосредоточимся на четырех основных сервисах верхнего уровня. Бронирование делится на "Корзина", "Купить" и "Рассчитать". Eos делится на процесс, запрос и тайм-аут. Функции Blink, связанные с торговыми заказами, разделены на Process и Query. Функции Blink, связанные с логистикой и доставкой, сгруппированы в Delivery. На следующем рисунке показано, как разделяются основные службы транзакций.

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

«Нет серебряной пули»

Первая основная ценность Agile Manifesto - ценить людей и взаимодействие выше процессов и инструментов.

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

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

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

использованная литература

  • Философия дизайна программного обеспечения Джона Остерхаута
  • Дзен и искусство ухода за мотоциклами: исследование ценностей Роберта Пирсига
  • Доменно-ориентированный дизайн Эрика Эванса
  • Гибкая разработка программного обеспечения: принципы, шаблоны и практики Роберта Сесила Мартина
  • Чистая архитектура Роберта Сесила Мартина
  • Команда компьютерщиков Брайана В. Фицпатрика

Первоисточник: