В недавнем выпуске подкаста DotNet&More мы обсуждали с Максимом Аршиновым его предстоящий доклад на Московский .Next "Блеск и нищета предметной модели". С позицией Максима можно будет легко познакомиться непосредственно на конференции. И, в качестве дополнения, я бы хотел рассмотреть видение великого спора Анемичная VS "Богатая" ("Насыщенная") доменные модели через призму некогда популярных GRASP шаблонов


Дискуссии идут достаточно давно, например:



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


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


GRASP


Как и отмечалось в начале статьи, рассматривать данный вопрос мы будем в контексте GRASP паттернов. Данный набор шаблонов был представлен в книге Крэга Лармана «Применение UML и шаблонов проектирования» и сильно повлиял на современное программирование, например, правила Low Coupling/High Cohesion были объявлены именно в GRASP.


Для людей, знакомых с GoF паттернами данный набор шаблонов может показаться слишком размытым. Все дело в том, что GRASP паттерны сфокусированы не на решении прикладных задач, а на распределении ответственности за те или иные действия и операции между объектами:


  • Creator отвечает за создание объектов
  • Controller отвечает за операции от пользователей
  • Indirection отвечает за организацию слабого зацепления между объектами
    И так далее.

В контексте данной статьи хотелось бы сфокусироваться на следующих паттернах:


  • Information Expert (Информационный эксперт)
  • Pure Fabrication (Чистая выдумка)

Information Expert (Информационный эксперт)


Проблема: Каков базовый принцип распределения обязанностей между объектами?
Решение: Назначить эту обязанность тому классу, который обладает достаточной информацией для ее выполнения.


Другими словами:


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

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


Pure Fabrication (Чистая выдумка)


Проблема: Какой класс должен обеспечить реализацию High Cohesion и Low Coupling, если шаблон Information Expert не обеспечивает подходящего решения.
Решение: Присвоить группу обязанностей с высокой степенью зацепления искусственному классу, не представляющему конкретного понятия предметной области.


Другими словами:


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

В контексте C#: если реализация метода предусматривает равномерное использование нескольких сущностей или внешних зависимостей, то данный метод стоит объявить в отдельном классе. И эти классы называют сервисами.
Например, если для расчета суммы долга необходимо знать не только атрибуты должника, но и параметры банка (процентная ставка, допустимый период просрочки), то стоит создать отдельный сервис, содержащий соответствующий метод.


И что это все значит?


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


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


Резюме


GRASP паттерны нельзя рассматривать как истину в последней инстанции. Но в они могут помочь легко понять, какому объекту назначить то или иное поведение. Соответственно, спор анемичная или "богатая" модель может не иметь смысла, так как решение принимается в каждом конкретном случае, в зависимости от модели предметной области и поведения, связанного с ней. В процессе дизайна системы будут появляться как сущности с поведением, так и без него. И это вполне корректно.

Комментарии (5)


  1. FanatPHP
    30.10.2019 11:12

    Why it is shown in the English language feed?


    1. KAW Автор
      30.10.2019 12:14

      Sorry, misclick.


  1. emacsway
    30.10.2019 16:02
    +1

    Автору спасибо за статью, — интересная точка зрения.

    то данный метод стоит объявить в отдельном классе. И эти классы называют сервисами.

    Стоит добавить, что Сервисы бывают трех уровней: Application, Domain и Infrastructure. В данном примере Вы говорите именно о Доменном Сервисе. Ограниченный список оснований для создания Доменных Сервисов хорошо выразил Vaughn Vernon в «Implementing Domain-Driven Design».


    1. KAW Автор
      30.10.2019 16:06

      Спасибо, статья Вернона замечательна


      1. emacsway
        30.10.2019 16:16

        Я имел ввиду книгу Вернона, она сегодня, пожалуй, вторая, если даже не первая, по значимости в сообществе DDD. А в статью цитату Вернона вставил уже я. Но, рад, если понравилось.