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

Источник: habrahabr
primepix

Информационные технологии развиваются семимильными шагами, появляются новые устройства, платформы, операционные системы, и вместе с этим растет спектр задач, который приходится решать разработчикам. Но, не все так плохо - на помощь программистам спешат новые средства разработки, 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.
    • Дает мощный инструмент для разработки сложных и больших решений

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

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


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

Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=31041