Введение в Rich Domain Model

    В последнее время можно услышать много аббревиатур, которые оканчиваются на DD: TDD, BDD, FDD, etc. Меня заинтересовал один из представителей «DD-семейства» — DDD, Domain Driven Development. Я не стану описывать здесь все тонкости этой методологии, ведь всю необходимую информацию можно легко найти в сети. Моя цель — рассказать о наиболее важной концепции DDD, о Rich Domain Model и на небольшом примере показать основные нюансы реализации.

    Rich Domain Model противопоставляют Anemic Domain Model. «Толстая» модель характеризуется состоянием и поведение, в отличии от «худой», где есть только состояние. По теме могу порекомендовать презентацию. В конечном итоге для себя решил так: ничего плохого в anemic нету, но это процедурный подход со всеми вытекающими последствиями. Более глубоко вдаваться в дебри я пока не хочу, просто примем, что Rich Domain Model — это модель с состоянием и поведением (бизнес-логикой).
    При более близком рассмотрении я столкнулся со сложностями понимания паттерна Repository и его отличием от DAO, но об этом позже.
    Азы использования Rich Domain Model рассмотрим на примере мини-сервис блога (мне кажется блог последнее время претендует на роль Hello, World! применительно к вебу).

    Начнем, как и полагается, с предметной области:

    Сущность «Пост»:
    public class Post {
      private String text;
      private Date date;
      private List<Comment> comments;
      
      public Post(String text) {
        this.text = text;
        this.date = new Date();
        this.comments = new ArrayList<Comment>();    
      }
      
      // getters and setters...
    }

    * This source code was highlighted with Source Code Highlighter.



    Сущность «Комментарий»:
    public class Comment {
      private String text;
      private Date date;
      
      public Comment(String text) {
        this.text = text;
        this.date = new Date();
      }

      // getters and setters...
    }


    * This source code was highlighted with Source Code Highlighter.



    Пользователи могут оставлять комментарии к постам, поэтому добавим метод comment к модели поста. На этом методе ограничимся всей бизнес-логикой нашего прототипа:
    public Comment comment(String text) {
      Comment comment = new Comment(text);
      this.comments.add(comment);
      return comment;
    }

    * This source code was highlighted with Source Code Highlighter.



    Наше приложение должно обеспечить сохранение объектов, так что очередь за Repository. Поведение Repository похоже на поведение обычной java-коллекции. Беглый взгляд на этот паттерн создает впечатление, что никакого отличия от DAO нету, но это не так. Рассмотрим пример сеанса работы с DAO:
    Item item = new Item();
    item.set...
    itemDao.insert(item);
    ...
    Item item = itemDao.select(itemId);
    item.set..
    itemDao.update(item);


    * This source code was highlighted with Source Code Highlighter.


    А вот пример работы с Repository:
    Item = new Item();
    item.set...
    itemRepository.add(item);
    ...
    Item item = itemRepository.get(itemId);
    item.set...

    * This source code was highlighted with Source Code Highlighter.


    И все! Никакого update нету. Как видите, работа с repository подобна работе с коллекцией объектов, в то время как работая с DAO мы все время должны вручную фиксировать состояние объекта в хранилище.

    Вот собственно интерфейс:

    public interface PostRepository {
      void add(Post post);
      void remove(PostId postId);
      Post get(PostId postId);
      List<Post> getAll();
    }

    * This source code was highlighted with Source Code Highlighter.



    Для доступа к элементом нужен идентификатор, поэтому я создал вспомогательный класс PostId:
    public class PostId {}

    public class Post {
      private PostId postId;
      private String text;
      private Date date;
      private List<Comment> comments;
      ...
    }


    * This source code was highlighted with Source Code Highlighter.



    Я нарочно не использую численные идентификаторы, которые обычно используют в качестве первичных ключей в БД. Мы идем от домена и о БД пока ничего знать не должны. В последствии может быть реализован любой repository и не обязательно на основе БД, где к примеру может не быть числовых идентификаторов.

    А вот простая реализация PostRepository:

    public class InMemoryPostRepository implements PostRepository {
     private Map<PostId, Post> identityMap = new HashMap<PostId, Post>();

     @Override
     public Post get(PostId postId) {
      return this.identityMap.get(postId);
     }

     @Override
     public List<Post> getAll() {
      return new ArrayList<Post>(this.identityMap.values());
     }

     @Override
     public void add(Post post) {
      PostId postId = new PostId();
      post.setPostId(postId);
      this.identityMap.put(postId, post);
     }

     @Override
     public void remove(PostId postId) {
      this.identityMap.remove(postId);
     }
    }


    * This source code was highlighted with Source Code Highlighter.



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

    public class BlogService {
      private PostRepository posts;
      public void setPostRepository(PostRepository posts) {
        this.posts = posts;
      }
      
      public Post post(String text) {
        Post post = new Post(text);
        this.posts.add(post);
        return post;
      }
      
      public Comment comment(PostId postId, String text) {
        Post post = this.posts.get(postId);
        return post.comment(text);    
      }
      
      public Post get(PostId postId) {
        return this.posts.get(postId);
      }
      
      public List<Post> getAll() {
        return this.posts.getAll();
      }
      
      public void delete(PostId postId) {
        this.posts.remove(postId);
      }
    }

    * This source code was highlighted with Source Code Highlighter.



    Вот собственно и все. Конечно на этом примере сложно увидеть все преимущества Rich Domain Model, потому как пример слишком уж тривиальный. Но я надеюсь, что он поможет кому-нибудь в практической реализации. Когда я пытался разобраться самостоятельно, то запрос в гугле «rich domain model example» не давал ничего вразумительного. Но теперь, когда в голове уже есть более или менее целостная картина, решил поделиться своими выводами с сообществом. Если статья понравится, то я могу написать продолжение, в котором будет уже более реальная реализация на основе Hibernate и постараюсь показать на практике такое важное свойство, как Persistence Ignorance.

    Отдельно хотелось бы сказать о 2-х открытиях, которые я сделал для себя ища материалы по теме в сети:
    1) Если используется Hibernate или любой другой ORM, то использование DAO неуместно.
    2) Учитывая большую популярность Anemic Domain Model, можно сделать смелое заявление, что ООП используется довольно редко.

    Вот сам проект.
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 11
    • 0
      Когда я искал по поводу необходимости реализации слоя DAO для ORM фреймворков типа Hibernate, то наткнулся на просто жесточайшией холивары по этому вопросу. Пока придерживаюсь мнения, что слой DAO нужно делать. Когда на практике обкатают теория без DAO, тогда и посмотрим.
      • +1
        Не знаю, по моему схема, описанная в этом посте, это классический подход.
        А совмешать CRUD с Hibernate — извращение, потому что тогда не понятно зачем вообще Hibernate.
      • 0
        Хорошая статья, всё по делу. Кстати, а стоит ли вообще делать PostId в посте? Мне кажется, это подробности базы.
        С NHibernate я обычно не делаю Id (если нужно на Web, можно запросить у репозитория).

        Ещё я не совсем согласен, что в доменных сервисах нет собственной логики.
        Мне кажется, что если следовать SRP, нельзя все задачи помещать в домен.
        • +1
          > Кстати, а стоит ли вообще делать PostId в посте? Мне кажется, это подробности базы.

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

          >С NHibernate я обычно не делаю Id (если нужно на Web, можно запросить у репозитория).

          А можно по-подробней?

          >Ещё я не совсем согласен, что в доменных сервисах нет собственной логики.

          В сервисом не должно быть только логики домена, а своя собственная логика вполне может быть. Насколько я знаю такую логику называют application logic.

          • 0
            >>С NHibernate я обычно не делаю Id (если нужно на Web, можно запросить у репозитория).
            >А можно по-подробней?

            Ну если это не web, то id не нужен вообще, потому что identity map.
            Если web, то либо у репозитория GetKey(entity), либо отдельный KeyService.

            Про логику согласен, но тогда не вижу необходимости в тех сервисах, которые просто middle men.
        • 0
          > В конечном итоге для себя решил так: ничего плохого в anemic нету, но это процедурный подход со всеми вытекающими последствиями.
          Не вижу в ADM ничего процедурного (в смысле не объектного) — она всего лишь следствие использования SRP при реализации персистентности.
          • 0
            У меня есть маленькое подозрение, что вы путаете с ActiveRecord=) И ADM, и RDM соответствуют принципу SRP в реализации персистентности, а именно они ничего «не знают» о персистентности.

            Есть более или менее устоявшееся мнение, что любая сущность характеризуется 3 свойствами: идентичностью, состоянием и поведением. ADM — это идентичность + состояние, в этом и заключается ее «недообъектность». Можете у Фаулера про Transaction script почитать — это как раз типичный прием реализации бизнес-логики с применением ADM.
            • 0
              >У меня есть маленькое подозрение, что вы путаете с ActiveRecord
              Не путаю.
              >ADM — это идентичность + состояние, в этом и заключается ее «недообъектность».
              Нет — в данном случае предельно упрощенное поведение (тривиальный доступ к состоянию) есть сознательное решение с целью упрощения реализации персистентности: объект, который может пережить программу, по определению должен иметь представление в виде данных и только данных. Если он и в «живом» виде будет только данными, то персистентность реализуется тривиально, в противном же случае — задача не имеет общего решения даже в виде монструозных фреймворков. Это не процедурный подход, а объектный. Да, такой результат можно получить и с процедурным, но приведет к нему совсем другой путь. Точно так же как процедура в рамках объектного подхода — это объект без состояния, но с поведением.
              В общем случае у сущности могут быть все три свойства — но любая из них (кроме ИМХОидентичность) не является необходимой (точнее, необходимой в нетривиальном виде) для каждого конкретного экземпляра.
              • 0
                В целом у «худых» сущностей есть некие зачатки объектности: поведение заключается в инкапсуляции состояния. Однако я считаю это притягиванием за уши. Я против того, что бы процедуру называть объектом без состояния — для меня это всегда просто процедура, а объект без поведения для меня в голове это просто record или struct. При использовании «худых» моделей для реализации бизнес-логики не обойтись без написанию процедур. ADM вынуждает писать процедуры (Transaction scripts, Flat Service Layers и прочие красивые слова для обозначения банальных процедур), именно поэтому я назвал ее процедурным подходом.

                Что касается реализации персистентности. Я так и не понял что же за проблемы могут быть при сохранении толстых моделей и какие нужны для этого монструозные фреймворки. Может быть вы имеете ввиду Dependency Injection, с помощью которых нужно подтягивать сервисы в модели? Согласен, что такое желание может возникать, однако это противоречит идеям DDD — слой домена ничего не должен знать ни о персистентности, ни о сервисах, что бы соблюдался тот самый SRP. По «дзену» всю внешнюю логику нужно вытаскивать из моделей в отдельные сервисы.

                Я думаю корни анемичной модели скорее всего идут к потребности написания большого количества кода с логикой типа CRUD. В этом случае у бизнес-сущностей единственное поведение это действительно хранение состояния. В приложениях, где много инфраструктурной логики anemic так же оптимальный выбор.
                • 0
                  > Однако я считаю это притягиванием за уши. Я против того, что бы процедуру называть объектом без состояния — для меня это всегда просто процедура, а объект без поведения для меня в голове это просто record или struct
                  Это уже психологический барьер. Объект без поведения — это совсем не обязательно struct. Объект никому и никогда не дает доступа к своим полям иначе как через методы доступа. И объектом без поведения он является тогда и только тогда, когда внутреннее состояние целиком определяется и наблюдается внешним, т.е. методов доступа (свойств) достаточно для установки и наблюдения полного состояния и ни в каких других объект не нуждается. При этом само внутреннее состояние может храниться иначе чем внешнее (например, в сжатом виде). Структура — это лишь эмуляция такого объекта с ограниченными возможностями (внутреннее состояние физически совпадает с внешним, а не просто однозначно соответствует ему).
                  >Я так и не понял что же за проблемы могут быть при сохранении толстых моделей и какие нужны для этого монструозные фреймворки.
                  Уже в приведенном примере есть проблема — грузить ли комментарии вместе с постом, решение которого выливается либо в геморрой с ленивой загрузкой либо в тормоза, если комментариев много. Объектно-реляционный импеданс для «толстой» модели проявляется в всей красе, а для «худой» — минимален.
          • 0
            мне кажется, или код превратился во что-то нечитаемое в этом посте?

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.