Статьи

Моделирование агрегата с помощью Eloquent

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

Архитектурный совет рекомендует , чтобы слой, содержащий модель предметной области, был независим от инфраструктурных проблем. Хотя это хороший совет, шаблон Active Record, с другой стороны, переносит строку в базе данных. Из-за этого практически невозможно отделить постоянный слой.

Смешивание проблем постоянства в модель предметной области может стать сложным и привести к принятию множества неверных решений. Это не означает, что невозможно создать модель домена Active Record. В этой статье мы рассмотрим пример построения Aggregate, который также расширяет Eloquent: популярную Active Record ORM.

Что такое агрегат?

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

A set of elements joined into one

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

В своей книге « Внедрение доменного дизайна» Вон Вернон излагает ряд рекомендаций, в которых он называет «правила совокупного проектирования»:

  1. Защита истинных инвариантов в границах согласованности
  2. Дизайн малых агрегатов
  3. Ссылка на другие агрегаты только по идентификатору
  4. Использовать возможную согласованность вне границы согласованности

Пример блога

Поскольку это технически пост в блоге, в качестве контекста целесообразно использовать только блог. Нам понадобится Почта, чтобы иметь свою собственную личность. Post потребуется Title вместе с Copy . Посты пишутся Author и могут быть прокомментированы, но только если Post заблокирован.

Post является хорошим кандидатом в качестве Aggregate Root с объектами Title и Copy Value. Однако Author будет находиться за пределами нашей границы и на него будет ссылаться только личность. Но как насчет Comment с?

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

Давайте сначала посмотрим, как может выглядеть наш агрегат Post без расширения ORM:

 final   class   Post 
 { 
     /** * @var PostId */ 
     private  $postId ; 

     /** * @var AuthorId */ 
     private  $authorId ; 

     /** * @var Title */ 
     private  $title ; 

     /** * @var Copy */ 
     private  $copy ; 

     /** * @var Lock */ 
     private  $locked ; 

     /** * @var array */ 
     private  $comments ; 

     /** * @param PostId $postId * @param AuthorId $authorId * @param Title $title * @param Copy $copy */ 
     public   function  __construct ( PostId  $postId ,   AuthorId  $authorId ,   Title  $title ,   Copy  $copy ) 
     { $this -> postId =  $postId ; $this -> authorId =  $authorId ; $this -> title =  $title ; $this -> copy =  $copy ; $this -> locked =   Lock :: unlocked (); $this -> comments =   []; 
     } 

     public   function   lock () 
     { $this -> locked =   Lock :: locked (); 
     } 

     public   function  unlock () 
     { $this -> locked =   Lock :: unlocked (); 
     } 

     /** * @param Message $message */ 
     public   function  comment ( Message  $message ) 
     { 
         if   ( $this -> locked -> isLocked ())   { 
             throw   new   PostIsLocked ; 
         } $this -> comments []   =   new   Comment ( 
             CommentId :: generate (), $this -> postId , $message ); 
     } 
 } 

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

У нас также есть поведение для блокировки и разблокировки сообщения — наряду с добавлением комментария. Новые комментарии будут предотвращены, если сообщение заблокировано, PostIsLocked исключение PostIsLocked . Это инкапсулирует бизнес-правило в Агрегате и делает невозможным комментировать заблокированные записи.

Представляем Eloquent

В качестве отправной точки, мы моделировали наш домен без расширения ORM. Давайте представим Eloquent в нашем классе Post и обсудим изменения:

 final   class   Post   extends   Eloquent 
 { 
     public   function   lock () 
     { $this -> locked =   Lock :: locked (); 
     } 

     public   function  unlock () 
     { $this -> locked =   Lock :: unlocked (); 
     } 

     /** * @param Message $message */ 
     public   function  comment ( Message  $message ) 
     { 
         if   ( $this -> locked )   { 
             throw   new   PostIsLocked ; 
         } $comment =   new   Comment ; $comment -> postId =  $this -> postId ; $comment -> message =   ( string )  $message ; $this -> comments -> add ( $comment ); 
     } 

     public   function  comments () 
     { 
         return  $this -> hasMany ( Comment :: class ); 
     } 

     public   function  getLockedAttribute ( $value ) 
     { 
         return   Lock :: fromString ( $value ); 
     } 

     public   function  setLockedAttribute ( Lock  $lock ) 
     { $this -> attributes [ 'locked' ]   =  $lock -> asBool (); 
     } 
 } 

Ну, мы определенно написали меньше кода. Давайте внимательнее посмотрим.

Первое, на что нужно обратить внимание — это удаление свойств класса. Eloquent сохраняет все свойства в защищенном массиве $attributes для каждого класса с помощью магических методов __get() и __set() для доступа и манипуляций.

У нас также больше нет конструктора. Eloquent увлажняет защищенный массив $attributes объекта, передавая данные строк через конструктор. Eloquent использует эту возможность для предоставления нескольких функций, таких как загрузка отношений.

Наконец, мы добавили дополнительный метод: comments() . Реализация этого нового метода позволяет нам быстро использовать отношения Eloquent . Мы могли бы hasMany() внутренне в comment() не раскрывая публичный метод, но это предотвратило бы активную загрузку, если бы мы решили добавить репозиторий запросов Active Record .

Атрибуты

Давайте поговорим об удалении свойств класса. Это имеет огромное значение? В любом случае, свойства в нашем исходном классе были частными — действительно ли сильно изменилось их хранение в защищенном массиве $attributes ?

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

Eloquent предоставляет доступ к данным с помощью открытого магического метода __get() . Поскольку мы можем получить доступ к нашим данным напрямую, было бы очень легко вывести поведение, которое должно быть в нашей модели. Помните бизнес-правило, которое гласило: «Вы не можете комментировать заблокированные сообщения»? Представьте себе следующую реализацию:

 if   ( $post -> locked -> isLocked ())   { 
     throw   new   PostIsLocked ; 
 } $post -> comment ( $message ); 

Это может легко привести к анемичной доменной модели с немногим больше, чем некоторыми «получателями и установщиками». Вместо этого примените принцип Tell-Dont-Ask и скажите модели, что ей нужно сделать:

 try   { $post -> comment ( $message ); 
 } 
 catch   ( PostIsLocked  $e ) 
 { 
     // Nope! 
 } 

Будьте осторожны, чтобы не лишить себя объектов поведения. То, что вы можете напрямую обращаться к своим данным и манипулировать ими, вовсе не означает, что вы должны это делать.

Объекты значения

Несколько дополнительных методов были добавлены в нашу версию Active Record, чтобы разрешить использование Lock . Данные в нашем объекте должны преобразовываться в скалярные типы, но мы все равно можем использовать объекты-значения, если воспользуемся методами доступа и мутатора Eloquent.

 public   function  getLockedAttribute ( $value ) 
 { 
     return   Lock :: fromString ( $value ); 
 } 

 public   function  setLockedAttribute ( Lock  $lock ) 
 { $this -> attributes [ 'locked' ]   =  $lock -> toBool (); 
 } 

Как вы можете видеть, это добавляет дополнительные открытые методы к объекту, ориентированному на отображение постоянства, вместо предоставления поведения. Обычно это не то, о чем нужно беспокоиться с Active Record, особенно если учесть, что мы наследуем базовый класс, который уже содержит открытые методы, такие как save() и forceDelete() . Вы привыкнете к этому конкретному компромиссу при моделировании с Active Record.

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

Инварианты

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

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

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

Уди Дахан предлагает, чтобы мы никогда не создавали агрегированные корни напрямую, а вместо этого использовали фабричный метод из другого объекта для возврата нашего нового экземпляра:

 final   class   Author   extends   Eloquent 
 { 
     // snip… 

     /** * @param Title $title * @param Copy $copy * @return Post */ 
     public   function  draftPost ( Title  $title ,   Copy  $copy ) 
     { 
         return   new   Post ([ 
             'title'   =>  $title , 
             'copy'   =>  $copy , 
             'author_id'   =>  $this -> id ]); 
     } 
 } 

В этом примере мы принимаем совет Уди Дахана и используем фабричный метод в другом объекте для построения нашего Поста. Это позволяет нам использовать язык бизнеса для объяснения того, что автор готовит сообщение, но также позволяет нам применять инварианты, когда мы не могли этого сделать. Использование этого подхода при моделировании с помощью Active Record требует более мягких навыков для реализации и применения — поскольку мы не можем предотвратить непосредственное создание экземпляров объекта, команда должна знать «что и как» при создании агрегатов.

Другой альтернативой является использование именованного конструктора при создании нового экземпляра:

 final   class   Post 
 { 
     // snip… 

     /** * @param AuthorId $authorId * @param Title $title * @param Copy $copy * @return static */ 
     public   static   function  draft ( AuthorId  $authorId ,   Title  $title ,   Copy  $copy ) 
     { 
         return   new   Post ([ 
             'title'   =>  $title , 
             'copy'   =>  $copy , 
             'author_id'   =>  $authorId ]); 
     } 
 } 

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

Опять же, этот подход имеет существенный недостаток, когда мы рассматриваем наследование от Active Record ORM. Поскольку мы расширяем Eloquent, в графе объектов уже есть ряд статических методов. Не у всех концепций домена будут хорошие заголовки рабочего процесса, такие как «черновик». Часто наиболее подходящим названием метода для описания создания является просто «создать». К сожалению, в Eloquent уже есть статический метод create() .

Существуют и другие шаблоны творчества, которые могут помочь, если разделение ответственности за создание объектов имеет смысл в вашем контексте.

Отношения

Агрегаты позволяют нам рассматривать группу объектов как единое целое. В нашем примере пост может иметь много комментариев. Как мы можем написать код Eloquent?

 $post -> comments ()-> save ( $comment ); 

Конечно, это сделает работу, но есть проблема. Запрашивая у поста соответствующие комментарии, мы полностью обходим Aggregate Root. Все общение с Агрегатом должно проходить через Корень, чтобы обеспечить соблюдение правил ведения бизнеса. Бизнес сообщил нам, что «заблокированные записи нельзя комментировать», однако мы только что смогли сохранить новый комментарий для записи, не проверяя ее заблокированный статус.

Наш пример помещает эту логику в класс Post , где он инкапсулирует создание Comment вместе с проверкой заблокированного статуса:

 $post -> comment ( $message ); 

При таком подходе мы больше не пропускаем проблемы постоянства. Код вне объекта $post не должен явно вызывать save() как он уже был вызван внутри.

Вывод

Мы показали, что моделирование Агрегата с помощью Active Record возможно, но это сложно и имеет много подводных камней. Это может стать довольно грязным, если вы попытаетесь обработать активную запись как традиционный объект. Active Record касается исключительно данных, тогда как объекты демонстрируют поведение и скрывают данные. Мы принципиально неправильно используем шаблон Active Record — и лучшее, что мы можем сделать, — это добавить поведение в структуру данных.

Эта статья не пытается сказать, что Active Record — это плохо, и при этом это не призыв к тому, чтобы всегда использовать его. Это просто инструмент, который мы можем использовать в зависимости от ситуации. Active Record — отличный инструмент для фокусировки на RAD, однако в некоторых ситуациях он просто не подходит. Я думаю, что из-за количества обсуждаемых компромиссов, моделирующие агрегаты подпадают под последнюю.

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

Кто прав? Вековой ответ на все вопросы: это зависит.