Парадигмы программирования. Data Driven vs Domain Driven

в 8:27, , рубрики: Программирование, разработка

Информационные технологии развиваются семимильными шагами, появляются новые устройства, платформы, операционные системы, и вместе с этим растет спектр задач, который приходится решать разработчикам. Но, не все так плохо — на помощь программистам спешат новые средства разработки, ide’шки, новые языки программирования, методологии и т.д. Один только список парадигм программирования впечатляет, а с учетом современных мультипарадигменных ЯП (например, C#) резонно встает вопрос: «Как с этим всем быть? Что выбрать?».

Попробуем немного разобраться.

Откуда взялось так много парадигм?

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

Что выбрать?

Все зависит от того, что требуется сделать. Стоит отметить, что все средства разработки отличаются, одни поддерживают одно, другие — другое. К примеру, PHP со «стандартным» набором модулей не поддерживает аспектно-ориентированное программирование. Поэтому выбор методологии довольно тесно связан с платформой разработки. Ну, и не стоит забывать, что можно комбинировать разные подходы, что приводит нас к выбору стека парадигм.

Для категоризации парадигм я использую 4 измерения, которые присущи практически любой задаче:

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

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

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

Рассмотрим примеры:

  • Ориентация на данные (Data Driven Design).Нам важны в первую очередь данные, а не то, как они между собой связанны.
    Типы подходящих приложений:
    • грабберы (собираем данные из разных источников, сохраняем куда-нибудь)
    • различные админки, интерфейсы к базам данным, все, где много простых CRUD операций
    • случаи, когда работа с данными уже определена, например, требуется разработать программу, а база данных уже существует и схему данных не изменить. В таком случае возможно проще ориентироваться на то, что уже есть, чем создавать дополнительные обертки над данными и уровнями доступа к данным
    • часто ориентация на данные появляется при использовании ORM, но нельзя заранее сказать хорошо это, или плохо (об этом ниже)

  • Ориентация на действия — императивные подходы к разработке. Думаю, что сюда можно отнести такие парадигмы как Event Driven Programming, Aspect Oriented Programming.
  • Ориентация на логику — Domain Driven Design (DDD) и все, что с этим связанно. Здесь нам важна предметная область задачи, мы уделяем внимание моделированию объектов, анализу связей и зависимостей. Применяется преимущественно в бизнес приложениях.
    Так же, сюда относится декларативный подход и отчасти функциональное программирование (решение задач, которые хорошо описываются математическими формулами)
  • Ориентация на интерфейс. Используется, когда в первую очередь важно как программа взаимодействует с внешним миром.
    Разработка приложения с ориентацией только на интерфейс — ситуация довольно редкая. Хотя в некоторых книгах я встречал упоминание о том, что такой подход рассматривался в серьез, причем базируясь на пользовательском интерфейсе — брали, что непосредственно видит пользователь и, исходя из этого, проектировали структуры данных и все остальное.
    Ориентация на пользовательский интерфейс в бизнес приложениях часто проявляется косвенно: к примеру, пользователю требуется видеть определенные данные, которые получить сложно, за счет чего архитектура обрастает дополнительными конструкциями (например, вынужденной избыточностью данных).
    Формально сюда можно отнести Event Driven Programming

А, что на практике?

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

На основе моего опыта могу сказать, что в данной сфере превалируют два подхода: ориентация на данные (Data Driven) и ориентация на логику (Domain Driven). По сути, они являются конкурирующими методологиями, но на практике могут объединяться в симбиозы, которые часто являются известными анти-паттернами.

Одним из преимуществ Data Driven в сравнении с Domain Driven является простота использования и внедрения. Поэтому Data Driven начинают использовать там, где надо применить Domain Driven (причем часто это происходит бессознательно). Проблемы же возникают с тем, что Data Driven плохо совместим с концепциями объектно-ориентированного программирования (конечно, если вы вообще используете ООП). На небольших приложениях эти проблемы почти незаметны. На средних по размеру приложениях эти проблемы уже заметны и начинают приводить к анти-паттернам, ну, а на крупных проектах проблемы становятся серьезными и требуют соответствующих мер.

В свою очередь Domain Driven выигрышен на крупных проектах, а на небольших — приводит к усложнению решения и требует больше ресурсов для разработки, что часто бывает критичным с точки зрения бизнес требований (вывести проект на рынок «asap», за небольшой бюджет).

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

  • Продукт
  • Клиент
  • Квота (отправляется клиенту как предложение)
  • Ордер (заказ)
  • Инвойс (платежка)
  • Ордер поставщику
  • Билл (по сути, платежка от поставщика)

Решив, что контекстная область у нас как на ладони, мы начинаем проектировать базу данных. Создаем соответствующие таблицы, запускаем ORM-ку, генерируем сущностные классы (ну, или в случае «умной» orm-ки прописываем схему где-то отдельно, например, в хml, и уже по ней генерируем и базу и сущностные классы). В итоге получаем на каждую сущность отдельный, независимый класс. Радуемся жизни, работать с объектами легко и просто.

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

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

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

На этом этапе мы полностью ушли от концепции ООП — объекты хранят только данные, вся логика находится в отдельных классах, ни инкапсуляции, ни наследования.

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

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

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

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

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

Итак, подведем итог Data Driven vs Domain Driven:
Data Driven:

  • Плюсы
    • Позволяет быстро разработать приложение или прототип
    • Удобно проектировать (кодогенерация по схеме и тп)
    • На небольших или средних по размеру проектах может быть вполне себе решением

  • Минусы
    • Может приводить к анти-паттернам и уходу от ООП
    • На больших проектах приводит к хаосу, сложной поддержке и т.п.

Domain Driven:

  • Плюсы
    • Использует всю мощь ООП
    • Позволяет контролировать сложность контекстной области (домена)
    • Есть еще ряд преимуществ, не описанных в статье, например, создание доменного языка и внедрение BDD.
    • Дает мощный инструмент для разработки сложных и больших решений

  • Минусы
    • Требует значительно больше ресурсов при разработке, что приводит к удорожанию решения
    • Определенные части становятся сложнее в поддержке (мепперы данных и т.п.)

Так, что же, черт возьми, мне выбрать?

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

Автор: primepix

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js