company_banner

Внедрение предметно-ориентированного проектирования в PHP

Автор оригинала: Alireza Rahmani Khalili
  • Перевод
И снова здравствуйте!

Что ж очередной «новый» курс, который стартовал в конце декабря, подходит к концу — «Backend разработчик на PHP». Учли разные мелкие шероховатости и запускаем новый. Осталось только посмотреть на выпуск и всё, поставим очередную галочку.

А счас пока давайте посмотрим на одну интересную статью.

Поехали.

В этой статье вы узнаете, как использовать PHP для управления следующим DDD-проектом вашей компании и эффективно моделировать реальные ситуации, чтобы помочь определить вашу бизнес-логику.

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



Что такое модель предметной области?


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

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

Единый язык


«Единый язык» («Ubiquitous Language») — это язык, используемый бизнес-специалистами для описания модели предметной области. Это означает, что команда разработчиков последовательно использует этот язык во всех взаимодействиях и в коде. Язык должен основываться на модели области. Позвольте мне привести пример:

$product = new Entity\Product();
$product->setTitle(new Title('Mobile Phone'));
$product->setPrice(new Price('1000'));
$this->em->persist($product);
$this->em->flush();

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

//add is a static method in product class
$product = Product::add(
    new Title('Mobile Phone'),
    new Price('1000')
);

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

Многослойная архитектура


В этой статье я не собираюсь говорить об объектно-ориентированном проектировании. Но DDD предполагает основы хорошего проектирования. Эрик Эванс (Eric Evans — автор книги “Предметно-ориентированное проектирование (DDD). Структуризация сложных программных систем”) считает, что разработка хорошей модели предметной области — это искусство.

Чтобы разработать хорошую модель предметной области, вам нужно знать о разработке, управляемой моделями (Model-Driven Design). Model-Driven Design — объединение модели и реализации. Многослойная архитектура является одним из ее блоков.

Многослойная архитектура (Layered Architecture) — это идея изоляции каждой части, основанная на многолетнем опыте и сотрудничестве разработчиков. Слои перечислены ниже:

  • Пользовательский интерфейс
  • Уровень приложения
  • Уровень домена
  • Уровень инфраструктуры

Пользовательский интерфейс отвечает за отображение информации пользователю и интерпретацию его команд. В Laravel отображение представляет собой слой пользовательского интерфейса (презентации). Уровень приложения — это способ общения с внешним миром (вне домена). Этот слой ведет себя как открытый API для нашего приложения. Он не содержит бизнес-правил или знаний. В Laravel контроллеры находятся именно здесь.

Уровень домена — это сердце бизнесс-ПО. Этот уровень отвечает за представление концепций бизнеса, информации о бизнес-ситуации и бизнес-правилах. Уровень инфраструктуры предоставляет общие технические возможности, которые поддерживают более высокие уровни, а также поддерживает структуру взаимодействий между четырьмя слоями (вот почему хранилища находятся в этом слое).

Связь между слоями является обязательной, но без потери преимуществ разделения. Связь происходит в одном направлении. Как видно из схемы выше, верхние слои могут взаимодействовать с более низкими уровнями. Если нижние слои должны соединяться с верхним слоем, они должны использовать такие шаблоны, как Callback или Observer.

Объекты-значения и сущности


Я большой поклонник объектов-значений (Value Objects). Я думаю, что они — суть ООП. Хотя объекты-значения DDD кажутся простыми, они являются серьезным источником замешательства для многих, включая меня. Я читал и слышал очень много разных способов описания объектов-значений с разных точек зрения. К счастью, каждое из разных объяснений, скорее помогло мне углубить понимание объектов-значений, нежели противоречило друг с другом.

Объекты-значения доступны по их значению, а не по идентификатору. Это неизменяемые объекты. Их значения не изменяются (или изменяются, но редко) и не имеют жизненного цикла (это означает, что они не как строки таблиц баз данных, которые можно удалить), например, валюты, даты, страны и т. д.

Вы можете создавать объекты-значения, которые вы не распознаете как объекты-значений. Например, адрес электронной почты может быть строкой, или это может быть объект-значение с собственным набором поведений.

Код, расположенный ниже, демонстрирует пример класса объекта-значения:

final class ImagesTypeValueObject 
{
    private $imageType;
    private $validImageType = ['JPEG', 'GIF', 'BMP', 'TIFF', 'PNG'];

    public function __construct($imageType) 
    {
        Assertion::inArray($this->validImageType, $imageType, 'Sorry The entry is wrong please enter valid image type');
        $this->imageType = $imageType;
    }

    public function __toString() 
    {
        return $this->imageType;
    }
}

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

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

Агрегаты


Модель может содержать большое количество объектов предметной области. Независимо от того, сколько всего мы предусмотрим при моделировании области, часто бывает, что многие объекты зависят друг от друга, создавая набор отношений, и вы не можете быть уверены в результате на 100%. Другими словами, вы должны знать о бизнес-правиле, которое всегда должно соблюдаться в вашей модели предметной области; только с этими знаниями вы можете с уверенностью рассуждать о своем коде.

Агрегаты помогают уменьшить количество двунаправленных ассоциаций между объектами в системе, потому что вам разрешено хранить ссылки только на корень. Это значительно упрощает проектирование и уменьшает количество слепых изменений в графе объектов. С другой стороны, агрегаты помогают с развязкой больших структур, устанавливая правила отношений между сущностями. (Примечание: агрегаты также могут иметь свойства, методы и инварианты, которые не вписываются в один класс)

Эрик Эванс в своей книге установил некоторые правила для реализации агрегатов, и я перечисляю их ниже:

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

Фабрики


В мире ООП Фабрика является объектом, который отвечает только за создание других объектов. В DDD фабрики используются для инкапсуляции знаний, необходимых для создания объектов, и они особенно полезны для создания агрегатов.

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

Бывают случаи, когда фабрика не нужна, и простого конструктора достаточно. Используйте конструктор, когда:

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

Хранилища


Хранилище — это слой, который находится между доменом вашего проекта и базой данных. Мартин Фаулер (Martin Fowler) в своей книге «Шаблоны корпоративных приложений» пишет, что хранилище является промежуточным взаимодействие между доменом и слоем сопоставления данных с использованием интерфейса, подобного коллекции, для доступа к объектам домена.
Это означает, что вы должны думать о доступе к данным в своей базе данных так же, как и к стандартным объектам коллекции.

Позвольте мне объяснить немного подробнее. Представьте себе, что в DDD вам может понадобиться создать объект либо с помощью конструктора, либо с помощью фабрики; вы должны запросить его из корня агрегата. Проблема в том, что разработчик должен иметь ссылку на корень. Для больших приложений это становится проблемой, потому что необходимо убедиться, что разработчики всегда имеют ссылку на необходимый объект. Это приведет к созданию разработчиками ряда ассоциаций, которые на самом деле не нужны.

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

Если у нас нет хранилища, фокус домена будет потерян, а дизайн будет скомпрометирован. Поэтому, если разработчики используют запросы для доступа к данным из БД или вытягивают несколько конкретных объектов, логика домена перемещается в запросы и код разработчика, поэтому агрегаты будут бесполезны.

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

Реализация в Laravel


Как вы уже могли знать, лучшим выбором для внедрения DDD в PHP является Doctrine ORM. Чтобы реализовать агрегаты и хранилища, нам нужно внести некоторые изменения в наши сущности и создать некоторые файлы на нашем доменном уровне.

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

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

namespace App\Domain\Repositories\Database\DoctrineORM;

use App\Domain\Events\Doctrine\DoctrineEventSubscriber;
use Doctrine\Common\Persistence\Event\LifecycleEventArgs;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\EntityManager;
use GeneratedHydrator\Configuration;
use Doctrine\Common\Collections\ArrayCollection;

abstract class DoctrineBaseRepository extends EntityRepository 
{
    public $primaryKeyName;
    public $entityName = null;

    public function __construct(EntityManager $em) 
    {
        parent::__construct($em, new ClassMetadata($this->entityClass));
        $this->primaryKeyName = $em->getClassMetadata($this->entityClass)->getSingleIdentifierFieldName();
    }
}

У нас есть два хранилища. Первый — это хранилище страниц, а второй — хранилище комментариев. Все хранилища должны иметь свойство entityClass для определения класса сущности. В этом случае мы можем инкапсулировать (private свойство) объект в наши хранилище:

namespace App\Domain\Repositories\Database\DoctrineORM\Page;
use App\Domain\User\Core\Model\Entities\Pages;
use App\Domain\Repositories\Database\DoctrineORM\DoctrineBaseRepository;

class DoctrinePageRepository extends DoctrineBaseRepository 
{
    private $entityClass = Pages::class;

    public function AddComments($pages) 
    {
        $this->_em->merge($pages);
        $this->_em->flush();
    }
}

Я использую командную строку Doctrine для генерации сущностей:

namespace App\Domain\Interactions\Core\Model\Entities;

use App\Domain\User\Comments\Model\Entities\Comments;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\ORM\Mapping as ORM;

/**
 * Pages
 *
 * @ORM\Table(name="pages")
 * @ORM\Entity
 */
class Pages
{
    /**
     * @var string
     *
     * @ORM\Column(name="page_title", type="string", length=150, nullable=false)
     */
    private $pageTitle;
    /**
     * @ORM\OneToMany(targetEntity="App\Domain\User\Comments\Model\Entities\Comments", mappedBy="pageId", indexBy="pageId", cascade={"persist", "remove"})
     */
    private $pageComment;
    /**
     * @var integer
     *
     * @ORM\Column(name="page_id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="IDENTITY")
     */
    private $pageId;

    public function __construct()
    {
        $this->pageComment = new ArrayCollection();
    }

    /**
     * @param Comment
     * @return void
     */
    public function addComments(Comments $comment)
    {
        $this->pageComment[] = $comment;
    }
//... other setters and getters.
}

namespace App\Domain\User\Comments\Model\Entities;

use Doctrine\ORM\Mapping as ORM;

/**
 * Comments
 *
 * @ORM\Table(name="comments")
 * @ORM\Entity
 */
class Comments
{
    /**
     * @ORM\ManyToOne(targetEntity="App\Domain\User\Core\Model\Entities\Users")
     * @ORM\JoinColumn(name="users_user_id", referencedColumnName="id")
     */
    private $usersUserId;
    /**
     * @ORM\ManyToOne(targetEntity="comments", inversedBy="children")
     * @ORM\JoinColumn(name="parent_id", referencedColumnName="comment_id")
     */
    private $parentId;
    /**
     * @ORM\ManyToOne(targetEntity="App\Domain\Interactions\Core\Model\Entities\pages", inversedBy="pageComment" )
     * @ORM\JoinColumn(name="page_id", referencedColumnName="page_id")
     */
    private $pageId;
    /**
     * @ORM\OneToMany(targetEntity="comments", mappedBy="parent")
     */
    private $children;

    /**
     * @param Page
     * @return void
     */
    public function __construct()
    {
        $this->children = new\Doctrine\Common\Collections\ArrayCollection();
    }
}

Как вы можете видеть, в приведенном выше коде я определяю отношения в аннотациях объектов. Реализация отношений в Doctrine ORM может показаться очень сложной, но на самом деле это не так сложно, когда вы познакомитесь с тем, как все работает. Единственный способ добавить комментарии — это вызвать addComments объекта page, и этот метод принимает только объект сущности comment в качестве входных данных. Это сделает нас уверенными в функциональности нашего кода.

Мой агрегат выглядит так:

namespace App\Domain\Comment;

use App\Domain\User\Comments\Model\Entities\Comments;
use App\Domain\Repositories\Database\DoctrineORM\User\DoctrineCommentRepository;
use App\Domain\Repositories\Database\DoctrineORM\Interactions\DoctrinePagesRepository;
use Assert\Assertion;

class PageAggregate
{
    public $Pages;
    public $pageResult;
    public $parentId = null;
    public $comments;
    public $DoctrineRepository = DoctrinePagesRepository::class;

    public function __construct($id, $comments = null, $administrative = null)
    {
        $this->DoctrineRepository = \App::make($this->DoctrineRepository);
        Assertion::notNull($this->pageResult = $this->DoctrineRepository->findOneBy(['pageId' => $id]), 'sorry the valid page id is required here');
        $commentFacory = new Commentfactory($this->pageResult, $comments);
        return $commentFacory->choisir($administrative);
    }
}

Нам нужен агрегат, который отвечает за ограничение доступа к комментариям, если PageId валидный; Я имею в виду, что без PageId доступ к comments невозможен. Скажем, comments без действительного page id не имеют смысла и недоступны. Кроме того, существует метод фабрики comment, который помогает нам инкапсулировать бизнес-правила.

Метод фабрики:

namespace App\Domain\Comment;
interface CommentTypeFactoryInterface
{
    public function confectionner();
}

namespace App\Domain\Comment;
interface CommentFactoryInterface
{
    public function choisir();
}

Я определил две фабрики. Первая — тип комментариев, а вторая — интерфейсы комментариев, которые делают ее обязательной для каждого комментария при реализации метода choisir.

namespace App\Domain\Comment;

use App\Application\Factory\Request\RequestFactory;

class Commentfactory implements CommentFactoryInterface
{
    private $page;
    private $comment;
    private $parentId;

    public function __construct($page, $comment = null)
    {
        $this->page = $page;
        $this->comment = $comment;
    }

    public function choisir($administrative = null)
    {
        // TODO: Implement choisir() method.
        if (is_null($administrative)) {
            $comment = new Comment($this->page, $this->comment);
            return $comment->confectionner();
        }
        $comment = new AdministrativeComments($this->page, $this->comment, $this->parentId);
        return $comment->confectionner();
    }
}

Метод фабрики Comment предоставляет внутренние части агрегата.

namespace App\Domain\Comment;

use App\Domain\User\Comments\Model\Entities\Comments;
use App\Domain\Repositories\Database\DoctrineORM\User\DoctrineCommentRepository;
use App\Domain\Repositories\Database\DoctrineORM\Interactions\DoctrinePagesRepository;
use App\Domain\Interactions\Core\Model\Entities\Pages;
use App\Application\Factory\Request\RequestFactory;
use Assert\Assertion;

class Comment implements CommentTypeFactoryInterface
{
    private $page;
    private $comments;
    public $DoctrineCommentRepository = DoctrineCommentRepository::class;
    public $DoctrineRepository = DoctrinePagesRepository::class;

    public function __construct(Pages $page, $comment)
    {
        $this->page = $page;
        $this->comments = $comment;
        $this->DoctrineCommentRepository = \App::make($this->DoctrineCommentRepository);
        $this->DoctrineRepository = \App::make($this->DoctrineRepository);
    }

    public function confectionner()
    {
        if (is_array($this->comments)) {
            \Request::replace($this->comments['data']);
            \App::make(RequestFactory::class);
            $this->addComments();
        } elseif (is_null($this->comments)) {
            return $this->retrieveComments();
        } elseif (is_int($this->comments)) {
            $this->deleteComment();
        }
        return true;
    }

    private function addComments()
    {
        if (isset($this->comments['id']) && !is_null($this->comments['object'] = $this->DoctrineCommentRepository->findOneBy(['commentId' => $this->comments['id']]))) {
            return $this->editComment();
        }
        $this->comments = $this->CommentObjectMapper(new Comments(), $this->comments['data']);
        $this->page->addComments($this->comments);
        $this->DoctrineRepository->AddComments($this->page);
    }

    private function editComment()
    {
        $comment = $this->CommentObjectMapper($this->comments['object'], $this->comments['data']);
        $this->page->addComments($comment);
        $this->DoctrineRepository->AddComments($this->page);
    }

    private function deleteComment()
    {
        $this->DoctrineCommentRepository->delComments($this->comments);
    }

    private function retrieveComments()
    {
        return $this->page->getPageComment();
    }
    //...
}

namespace App\Domain\Comment;

use App\Domain\Interactions\Core\Model\Entities\Pages;
use App\Domain\Repositories\Database\DoctrineORM\User\DoctrineCommentRepository;
use App\Domain\Repositories\Database\DoctrineORM\Interactions\DoctrinePagesRepository;
use App\Domain\User\Comments;
use Assert\Assertion;

class AdministrativeComments implements CommentTypeFactoryInterface
{
    private $page;
    private $comments;
    private $parentId;
    private $privilege;
    public $DoctrineCommentRepository = DoctrineCommentRepository::class;
    public $DoctrineRepository = DoctrinePagesRepository::class;

    public function __construct(Pages $page, $comment, $parentId)
    {
        $this->page = $page;
        $this->comments = $comment;
        $this->parentId = $parentId;
        $this->privilege = new Athurization(\Auth::gaurd('admin')->user());
    }

    public function confectionner()
    {
        $action = $this->comments['action'];
        Assertion::notNull($this->comments = $this->DoctrineCommentRepository->findOneBy(['commentId' => $this->comments['id']]), 'no Valid comment Id');
        $this->$action;
        return true;
    }

    public function approve()
    {
        $this->privilege->isAuthorize(__METHOD__);
        $this->DoctrineCommentRepository->approve($this->comments, \Auth::gaurd('admin')->user());
    }

    public function reject()
    {
        $this->privilege->isAuthorize(__METHOD__);
        $this->DoctrineCommentRepository->reject($this->comments, \Auth::gaurd('admin')->user());
    }

    public function delete()
    {
        $this->privilege->isAuthorize(__METHOD__);
        $this->DoctrineCommentRepository->delete($this->comments, \Auth::gaurd('admin')->user());
    }
}

Как вы видите в приведенном выше коде, у нас есть два класса: Comment и AdministrativeComments. Commentfactory будет решать, какой класс нужно использовать. Некоторые ненужные классы или методы, такие как класс Authorization и метод reject, здесь опущены. Как вы можете видеть в приведенном выше коде, я использую RequestFactory для валидации. Это другой метод фабрики в нашем приложении, который отвечает за проверку входных данных. Этот вид проверки имеет определение в DDD, а также добавлен в laravel 5+.

Заключение


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

THE END

Как всегда ждём комментарии, вопросы тут или у нас на Дне открытых дверей.
OTUS. Онлайн-образование
707,83
Цифровые навыки от ведущих экспертов
Поделиться публикацией

Комментарии 89

    +2
    У вас код немного поехал. Имеется пробел с двух сторон у указателя ->, тоже и в неймспейсах.
    Из-за этого, читабельность кода в некоторых местах сходит на нет.
      0
      Хм. Счас поправлю. Спасибо, почему-то на превью я этого не заметил
        0

        «Счас» — у вас и в тексте так...

          0
          Сломался вчера на половине :( Доделаю через несколько часов…
            0

            на половине чего? ни один блок кода не исправлен.

              0
              На половине того, что было тогда в черновике у меня.
      +1
      Уж простите придерусь.
      Как ужастно оформлен код
        +1
        Уехало почему-то. Поправлю.

        P.S. простите тоже придерусь, но «ужаСНо» :)
          +1
          Вечер, пятница, опять работа.
          Бессмысленный и стремный код.
          Сидишь читаешь тут газету,
          А стремный код опять и тут…

          =)
            +2
            О, там есть шедевры, вроде «Атхуизация гаурда»:
            $this - > privilege = new Athurization(\Auth::gaurd('admin') - > user());

            Это просто Epic Win, по-моему.
              0
              Да ладн спишем все на пятницу =)
              Но парсер в мозгу изрядно так сломался при прочтении.
                –1
                Я бы списал на автора, а не пятницу. Это уже не первый раз, когда выплёскивается подобное, кхм… Автор обещает поправить и кладёт большой болт.
                  0

                  Судя по источнику перевода, это у автора стиль такой, а не у переводчика

                    0
                    По одному случаю как-то странно судить. В тексте перед так же указали на ошибки — всё исправлено.
                    С кодом мне сложнее работать почему-то, глаза быстрее устают — вот и торможу.
          0

          а это что за кодстайл? в каких языках так принято?

            0
            В криворуких пятничных :(
            +1
            Както у вас инфраструктура в домен протекла
              0
              удалено
                0
                Сам офигел и проглядел. Счас сижу правлю, так что особо не напрягайтесь, скоро будет сделано нормально.
                  0
                  Ой, а я удалил коммент, т.к. увидел, что уже куча подобного рода набежало. А вы ответили. Некрасиво с моей стороны получилось.
                    0
                    Ничего страшного. Претензия-то по делу
                      0
                      Хорошо. А когда поправите вот это? Полтора месяца прошло: habrahabr.ru/company/otus/blog/350368
                        0
                        Фигассе… Вообще был уверен, что исправил, видимо задолбался и не сохранил. Завтра сделаю уже — у меня на это тексте глаза поплыли.

                        Прошу прощения
                0
                Пара вопросов:

                — Почему у вас какие то параметры именуются $comment, а какие то $DoctrineRepository? Почему разный стиль?
                — Почему бы не использовать, хотя бы при создании репозиториев принцип инверсии зависимостей? Куда приятнее работать с интерфейсами, а не с реализацией, благо в Laravel это достаточно легко реализуется.

                Вот кстати у меня вопрос, я как человек мало понимающий в DDD и в вашем коде,
                class Comment implements CommentTypeFactoryInterface {
                    private function addComments()
                    private function editComments()
                    private function deleteComment()
                    private function retrieveComments()
                }
                

                Кто и откуда запускает эти методы?

                В методе editComments() вызывает метод $this - > page - > addComments($comment);, но ведь DDD подразумевает, что мы общаемся на языке бизнеса, а получается, что редактирование комментария у нас приравнивается к добавлению?

                Хотелось бы более детального разбора кода.
                  +1

                  Было бы круто если бы вы форматировали код по PSR, читать сложно

                    0
                    Почему публичные свойства и почему \App::make в конструкторе вместо нормальной инъекции зависимостей?

                    Вот этот момент

                    public $DoctrineCommentRepository = DoctrineCommentRepository::class;
                    ...
                    $this - > DoctrineCommentRepository = \App::make($this - > DoctrineCommentRepository);


                    вообще убойный

                    Очень стремная организация кода… что за паттерн такой? :)

                      0
                      1. Стоит использовать Assertion в продакшене? В мануале говорится что нет php.net/manual/en/function.assert.php
                      2. Должен знать ValueObject о сушестыовании конфига? Если нет то как передать на пример допустимые расширения для картинки?
                      3. Можете привести реальный пример использования аггрегатов? На примере того же ПО доставки еды. Я как понимаю это сервисы где происходит валидация, вызивается моделы и репозитории
                        0
                        Стоит использовать Assertion в продакшене? В мануале говорится что нет


                        1. Это не те ассершены, что являются частью php.
                        2. Ассершены отключаются в продакшене с помощью конфигов в ini, а не с помощью вырезания их из кода.
                          +1
                          2. В целом не должны. Обычно это тупые объекты. Можно передавать значения из конфигов в конструкторы/фабрики. Можно передавать во внутренний валидатором типа Image.hasValidExt($validExts), можно во внешний. В целом можно сделать и конфиг, особенно в PHP, где конфиг может быть обычным PHP-кодом, но это должен быть конфиг конкретного VO, а не часть общего конфига приложения.

                          3. Агрегаты не сервисы, это сущности (корни агрегата), инкапсулирующие в себя другие сущности.
                          +1
                          Контроллеры Laravel и подобных MVC-like фреймворков — это не слой приложения, это слой UI. Слой приложения — это модель в MVC триаде, фасад (в широком смысле слова) к домену и инфраструктуре.

                          Общее правило, отклонения от которого должны быть очень обоснованы — один репозиторий на один агрегат. Репозитории страниц и комментариев, связанных друг с другом не скалярными ссылками, а обьектными — очень сильно попахивают нарушением границ агрегатов. Или комментарии — это отдельный агрегат, хранящий символьную ссылку на страницу (string/int pageId, а не Page page), или комментарий нельзя получить без получения страницы по id, id комментария должен использоваться только внутри Page/PageAgregate.
                            +1
                            В целом при использовании фреймворков общего назначения типа Laravel или Symfony, всему коду, связанному с ним место в слое UI/инфраструктуры, ни слой приложения, ни, тем более, слой домена не должны о фреймворке ничего знать. То же и с ORM — только это исключительно слой инфраструктуры. Репозитории Doctrine не должны быть репозиториями домена или их предками. В крайнем случае репозитории Доктрины должны реализовывать интерфейс, объявленный в домене, использующий только термины домена и в слоях UI, приложения и, тем более, домена, не должно использоваться знание того, что реализация этого интерфейса является классом, унаследованным от репозитория Доктрины. Обычно проще сделать репозиторий Доктрины свойством в реализации репозитория домена, используя паттерн адаптер, приводящий вызовы методов репозитория домена к вызовам репозитория Доктрины.
                              0

                              Скажите, а что по вашему мнению должно быть в Applicatoin layer?


                              Сущности и доменные сервисы в Domain layer.
                              Имплементация доменных сервисов и инфраструктурные сервисы в Infrastructure layer.
                              Миграции и Doctrine DBAL типы в Persistent layer.
                              Если следовать вашей логике, то контроллеры, консольные команды, формы, меня билдеры и наверное Query handlers в Presentation layer.
                              Тогда для Applicatoin layer останутся только Command handlers или Use Cases (Interactors)?

                                0
                                В целом, да, они и останутся. Хотя Query handlers тоже в нём. По сути Applicatoin layer это API для Presentation layer. Если в терминах MVC, то Applicatoin layer — это Model, инкапсулирующая для UI всё приложение кроме собственно UI.

                                Хотя, конечно, исходить надо из соображений практичности и можно пускать Presentation layer в Domain и Infrastructure layers, если Applicatoin layer сводится к тупомоу проксированию в каких-то сценариях.
                              +1
                              Вы забыли упомянуть еще один важный слой абстракции для DDD — службы предметные и прикладные. Именно они содержат логику вашего домена или всякие прикладные штуки, проверяют инварианты, и с ними взаимодействует слой UI.
                                +1

                                Эх… Как хорошо все начиналось, а скатилось к Anemic model, смешивании слоев Domain и Infrastructure, нарушению принципов агрегата, описанных в начале статьи, и двунаправленному связыванию (((
                                Про нейминг и говорить не хочется.

                                  0

                                  Если говорить о DDD, то по хорошему страницы и комментарии это два разных Bounded Context и соответственно они не должны иметь связей.


                                  По хорошему, сущность комментария должна иметь примерно такой вид:


                                  Comment class
                                  /**
                                   * @ORM\Table(name="comments")
                                   * @ORM\Entity
                                   */
                                  class Comment
                                  {
                                      // редактировать коментарий можно только 10 минут по аналогии с Хабром
                                      // константа публичная потому, что понадобится для валидации
                                      // на более высоких уровнях
                                      public const EDITING_PERIOD = '+10 minutes';
                                  
                                      /**
                                       * @ORM\Id
                                       * @ORM\GeneratedValue(strategy="NONE")
                                       * @ORM\Column(name="id", type="CommentId", nullable=false)
                                       *
                                       * @var CommentId
                                       */
                                      private $id;
                                  
                                      /**
                                       * @ORM\Column(name="page_id", type="PageId", nullable=false)
                                       *
                                       * @var PageId
                                       */
                                      private $page_id;
                                  
                                      /**
                                       * @ORM\Column(name="author_id", type="CommentAuthorId", nullable=false)
                                       *
                                       * @var CommentAuthorId
                                       */
                                      private $author_id;
                                  
                                      /**
                                       * @ORM\Column(name="reply_to_id", type="CommentId", nullable=true)
                                       *
                                       * @var CommentId|null
                                       */
                                      private $reply_to;
                                  
                                      /**
                                       * @ORM\Column(name="comment_at", type="DateTimeImmutable", nullable=false)
                                       *
                                       * @var \DateTimeImmutable
                                       */
                                      private $comment_at;
                                  
                                      /**
                                       * @ORM\Column(name="message", type="CommentMessage", nullable=false)
                                       *
                                       * @var CommentMessage
                                       */
                                      private $message;
                                  
                                      /**
                                       * @param PageId          $page_id
                                       * @param CommentAuthorId $author_id
                                       * @param CommentMessage  $message
                                       * @param CommentId|null  $reply_to
                                       */
                                      private function __construct(
                                          PageId $page_id,
                                          CommentAuthorId $author_id,
                                          CommentMessage $message,
                                          ?CommentId $reply_to = null
                                      ) {
                                          // конструктор приватный. создаем инстанс через фабрики
                                  
                                          $this->page_id = $page_id;
                                          $this->author_id = $author_id;
                                          $this->reply_to = $reply_to;
                                          $this->message = $message;
                                          $this->comment_at = new \DateTimeImmutable();
                                  
                                          // нам не нужен инкрементный идентификатор
                                          // мы можем сформировать естественный ключ
                                          $id = implode('_', [
                                              $page_id->id(),
                                              $author_id->id(),
                                              $this->comment_at->getTimestamp()
                                          ]);
                                          // хеш может давать коллизии и потому смотрите здесь сами
                                          $id = sha1($id);
                                          // упаковываем в Base64 для компактности
                                          $id = hex2bin($id);
                                          $id = base64_encode($id);
                                          $id = str_replace(['=', '+', '/'], ['', '-', '_'], $id);
                                  
                                          $this->id = new CommentId($id);
                                      }
                                  
                                      /**
                                       * @param PageId          $page_id
                                       * @param CommentAuthorId $author_id
                                       * @param CommentMessage  $message
                                       *
                                       * @return Comment
                                       */
                                      public static function comment(
                                          PageId $page_id,
                                          CommentAuthorId $author_id,
                                          CommentMessage $message
                                      ): self {
                                          return new self($page_id, $author_id, $message);
                                      }
                                  
                                      /**
                                       * @param CommentAuthorId $author_id
                                       * @param CommentMessage  $message
                                       *
                                       * @return Comment
                                       */
                                      public function reply(
                                          CommentAuthorId $author_id,
                                          CommentMessage $message
                                      ): self {
                                          return new self($this->page_id, $author_id, $message, $this->id);
                                      }
                                  
                                      /**
                                       * @param CommentMessage $new_message
                                       */
                                      public function edit(CommentMessage $new_message): void
                                      {
                                          // валидация должна быть многоуровневой
                                          // и на доменном уровне ее тоже нужно выполнять
                                          // сюда должны прийти уже валидные данные, но если они не валидны,
                                          // то это полный аллес капут
                                  
                                          $expire_at = $this->comment_at->modify(self::EDITING_PERIOD);
                                          if ($expire_at < new \DateTimeImmutable()) {
                                              throw new CommentException('Comment editing period has expired.');
                                          }
                                  
                                          $this->message = $new_message;
                                      }
                                  
                                      // Никаких сетеров. Далее толко геттеры по необходимости...
                                  }
                                    0

                                    На самом деле спорно, два это контекста или один, два агрегата в разных контекстах, два в одном или один в одном.


                                    А @ORM/... — протечка инфраструктуры в домен :)

                                      0
                                      На самом деле спорно, два это контекста или один

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


                                      А ORM/… — протечка инфраструктуры в домен :)

                                      нет, так как это только комментарии и в коде это не отражено)))
                                      замена Doctrine на что-то еще ни как не повлияет на доменные сущности
                                      в этом и суть переносимости домена

                                        +1
                                        1. Почему бы не воспользоваться просто UUID-ом для генерации айдишки?
                                        2. Аннотации от доктрины хоть и комменты, но все же часть инфраструктуры. При смене ОРМ-ки вы же их удалите? Или с какой целью они там лежат?
                                        3. Вообще доктриновские сущности не должны быть доменными сущностями. Как вы соберете сущность, которая хранится в 2+ таблицах, а то и в разных хранилищах (к примеру каунтеры в редисе)?
                                          0
                                          Почему бы не воспользоваться просто UUID-ом для генерации айдишки?

                                          Можно и UUID. Кому как больше нравится. Но не стоит пренебрегать преимуществами естественных идентификаторов.


                                          Аннотации от доктрины хоть и комменты, но все же часть инфраструктуры. При смене ОРМ-ки вы же их удалите? Или с какой целью они там лежат?

                                          Можно и не удалять при смене ORM. Просто в этом случае они будут висеть мертвым грузом.
                                          А можно мапинг описать не в аннотациях, а например в XML файле или в репозитории. От этого принципиально ничего не поменяется. Разница только в удобстве сопровождения.


                                          Как вы соберете сущность, которая хранится в 2+ таблицах

                                          В этом случае у нас связь OneToOne и связанная "сущность" это VO в агрегате. Эта "сущность" не будет иметь смысла вне агрегата как нормальный VO.


                                          в разных хранилищах (к примеру каунтеры в редисе)

                                          А каунтеры и не нужны в сущностях.
                                          Они, как правило, относятся к уровню представления.
                                          Уже как-то обсуждали эту тему с Fesor.
                                          Если на каунтерах завязаны бизнес ограничения, то ограничения описываются в сервисах доменного слоя, а сами каунтеры подтягиваются из репозитория.
                                          На крайний случай, в репозитории можно смапить данные агрегата из разных хранилищ.


                                          Я советую обратить ваше внимание на схему CQRS.
                                          Реально поднимать домен нужно только на запись.


                                          Хотя надо признать. Doctine накладывает ряд неприятных ограничений на структуру сущностей.

                                            0
                                            Уже как-то обсуждали эту тему с Fesor.


                                            А киньте ссылкой в меня =) Интересно почитать
                                              0

                                              Боюсь не найду обсуждение((
                                              По моему обсуждение было в личке в контексте CQRS.
                                              Могу процитировать его мнение по поводу маппинга в аннотациях из нашей переписки:


                                              Часто встречаю что правила хранения (маппинг) описываются в YML файле.

                                              С одной стороны правильно не смешивать домен и правила его хранения, с другой стороны аннотации удобнее и читабильней yml конфигов. И аннотации проще поддерживать в актуальном состоянии.

                                              это делается для того что бы ваши сущности не зависили от доктрины (у вас в коде нет use Doctrine\ORM\Mapping и все такое). Это единственная причина для этого. Все остальное — вкусавщина. Авторы доктрины например настоятельно рекомендуют хранить это добро в xml и только если вы планируете шарить сущности между проектами. В пределах одного проекта норм и аннотации.

                                                0
                                                В Doctrine 3 поддержку YAML выпилили как я понимаю. Либо XML, либо аннотации, либо на PHP метаданные. Есть подозрение, что «пуристы» типа меня, предпочитающие YAML, перейдут на аннотации, как на наименьшее зло. :)
                                                  0

                                                  Обсуждение было давно и я уже давненько не встречал маппинга в YAML.
                                                  А вот XML вполе используется, например в FOSUserBundle или SonataUserBundle.

                                                    0

                                                    Для Symfony бандлов использование XML если не требование, то официальная лучшая практика. Для обычных проектов такого нет.


                                                    Хотя, вот сейчас почитал официальный гайдгайд по Doctrine для Symfony 4.0 — уже упоминания о YAML нет, XML описывается кратко, без примеров, а в доках 3.4 — как равноправные аннотации, YAML и XML с примерами.


                                                    Можно сделать вывод, что аннотации рекомендуются для описания маппинга в проектах, а XML в бандлах.

                                            0
                                            > Вообще доктриновские сущности не должны быть доменными сущностями. Как вы соберете сущность, которая хранится в 2+ таблицах, а то и в разных хранилищах (к примеру каунтеры в редисе)?

                                            Использовать сущности Доктрины как простые DTO для чтения/записи в базу — очень часто стрельба из пушки по воробьям, проще какой-то ActiveRecord заюзать или вообще PDO, если возможностей Doctrine не хватает для желаемого маппинга.
                                            0
                                            Но кейсы представить можно :) Создаешь страницу и «первый...» комментарий :) Или отвечаешь на комментарий «поправил» с сообщением об опечатке и одной кнопкой сохраняешь. На самом деле, в данном виде я не вижу смысла разделять контексты или даже выносить комментарии отдельной сущностью, а не листьями агрегата, пока комментарии у нас ничего не значат без страницы и никому не нужны без неё. Можно разделить, но скорее это лишняя работа в расчёте на будущее, причём такая, что может быть произведена в любой момент без нарастания технического долга. Вот как возникнут задачи «а давайте кроме страниц дадим возможность комментировать картинки», вот тогда и делить.

                                            А там у вас use Doctrine\… as ORM нигде не затесалось? )) А вообще это не просто комментарии, а аннотации — по нынешним временам полноправная часть кода, сильно влияющая на его логику.
                                              0
                                              Вот как возникнут задачи «а давайте кроме страниц дадим возможность комментировать картинки», вот тогда и делить.

                                              как я сказал ранее:


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

                                              Контекст страниц и контекст комментариев к страницам находятся в одном контексте более высокого уровня.
                                              То есть комментарии к странице это комментарии к странице и комментариями к картинкам они никогда не станут.
                                              Разделять эти контексты или нет конечно дело каждого, но я вижу преимущества такого подхода.
                                              Контекст из Ubiquitous Language можно организовать в независимый модуль в коде, который легко можно включать, отключать, переносить и изменять не затрагивая весь остальной проект и модуль страниц (контекст странц) в том числе.


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


                                              Но конечно, кому-то это может показатся переусложнением и загромождением кода.
                                              Я ни кого не принуждаю так делать.

                                                0
                                                Как по-мне, то ваши вложенные контексты очень напоминают техническое разделение на модули, сервисы и т. п., сгруппированные вокруг сущностей, сильно или слабо изолированными, но не разделение предметной области на самодостаточные части. Интуитивно для меня признаком недостаточного разбиения на контексты являются сложные определения в Ubiquitous Language. Пока определения простые и однозначные, то разбиение на контексты выглядит искусственным, «потому что можем». Собственно разделение на контексты надо производить когда в определениях терминов появляется «или», «а также», «с точки зрения», «в рамках» и т. п… В примере страницы с комментариям я не вижу такого разделения, даже если разные типы пользователей создают, редактируют и комментируют страницы.

                                                Пока суть предметной области сводится исключительно с работой с иерархией текстовых и близких к нему объектов, то это один контекст. Нет другого контекста, в котором страница или комментарий значили бы что-то другое, кроме того, что они значат в начальной постановке.
                                                  0
                                                  Собственно разделение на контексты надо производить когда в определениях терминов появляется «или», «а также», «с точки зрения», «в рамках» и т. п…

                                                  Если так рассуждать, то пол проекта может оказаться в одном контексте.
                                                  Например авторизация и аунтификация может оказаться в одном контексте с комментариями.


                                                  Комментариям не нужен весь объект пользователя. Им достаточно идентификатора пользователя. Тоже самое с страницами. Комментариям от страницы ни чего не надо кроме идентификатора.
                                                  Поэтому я комментарии и выделил в отдельный контекст.


                                                  У вас могут быть другие бизнес требования и у вас страницы с комментариями будет в одном контексте.


                                                  Так же ни кто не запрещает создавать комментарии в статье.


                                                  class Page
                                                  {
                                                      public function comment(
                                                          CommentAuthorId $author_id,
                                                          CommentMessage $message
                                                      ): Comment {
                                                          $new_comment = Comment::comment(
                                                              $this->id,
                                                              $author_id,
                                                              $message
                                                          );
                                                          $this->comments[] = $new_comment;
                                                  
                                                          return $new_comment;
                                                      }
                                                  }
                                                    0

                                                    Вот авторизация с аутентификацией как раз оказываются в разных контекстах из-за появления слов "также" и т. п. Что-то вроде:


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

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

                                                      0

                                                      У вас не совсем корректная формулировка


                                                      набор прав на различные действия в системе

                                                      Под различными действиями может быть все что угодно.
                                                      В вашем примере лучше сформулировать так:


                                                      имеет набор прав на различные действия в системе такие как создание страниц и их комментирование.

                                                      Вот уже и нет никаких "так же".
                                                      Вообще, права пользователя зависят от контекста и контекст определяет права пользователя, а не наоборот.
                                                      Например: read-only пользователь на Хабре не может голосовать за статью. Это ограничение диктуется контекстом статей, а от пользователя здесь только его тип.


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

                                                      Вот об этом я и говорю. Если пользователь это VO по отношению к комментарию и находится с комментариями в разных контекстах, то почему страница не может быть так же VO по отношению к комментарию и быть с ними в разных контекстах?


                                                      Может комментарии и страницы нужно разделять, а может и нет. Все зависит от проекта. В контексте маленького бложика я бы их не разделял. В контексте Хабра я бы их выделил в отдельный контекст потому, что у комментариев много своей бизнес логики ни как не связанной со статьями. Собственно, вся связь ограничивается одной фразой из Ubiquitous Language — "прокомментировать статью".

                                                        0
                                                        Вот уже и нет никаких "так же".

                                                        Нет, "также" остаётся. Ключевые слова "также является автором страниц и комментариев", а не про права.


                                                        Вообще, права пользователя зависят от контекста и контекст определяет права пользователя, а не наоборот.

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


                                                        Вот об этом я и говорю. Если пользователь это VO по отношению к комментарию и находится с комментариями в разных контекстах, то почему страница не может быть так же VO по отношению к комментарию и быть с ними в разных контекстах?

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

                                                          0
                                                          Нет, "также" остаётся. Ключевые слова "также является автором страниц и комментариев", а не про права.

                                                          Пунктуация страдает. Я иначе воспринял ваше предложение. Прошу прощения.
                                                          Вообще, с формулировкой можно много играться. Например:


                                                          User — человек, персонифицированный пользователь системы, прошедший аутентификацию, имеет набор прав на различные действия в системе, автор страниц, комментатор.

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


                                                          • User — человек;
                                                          • User — персонифицированный пользователь системы;
                                                          • User — пользователь прошедший аутентификацию;
                                                          • User — пользователь имеющий набор прав на различные действия в системе;
                                                          • User — автор страниц;
                                                          • User — комментатор.

                                                          Вспоминаем Фаулера


                                                          As you try to model a larger domain, it gets progressively harder to build a single unified model. Different groups of people will use subtly different vocabularies in different parts of a large organization. The precision of modeling rapidly runs into this, often leading to a lot of confusion. Typically this confusion focuses on the central concepts of the domain. Early in my career I worked with a electricity utility — here the word "meter" meant subtly different things to different parts of the organization: was it the connection between the grid and a location, the grid and a customer, the physical meter itself (which could be replaced if faulty). These subtle polysemes could be smoothed over in conversation but not in the precise world of computers. Time and time again I see this confusion recur with polysemes like "Customer" and "Product".

                                                          По поводу:


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

                                                          Учитывая то, о чем мы тут говорим, вопрос надо ставить так: Почему страница в контексте комментариев должна быть VO, представляющим сущность из другого контекста, а не сущностью в этом контексте?

                                                            0

                                                            Конечно же роли пользователя, а не свойства пользователя )))

                                                              0
                                                              Допустим у нас есть страница вывода поста. В таком случае нужно обращаться как минимум к 3-м контекстам — посты, комментарии, пользователи. Как это дело объединять?
                                                                0

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

                                                                  0
                                                                  Что значит «поднимать»?

                                                                  Вы разделяете всё на контексты, и соотвественно из этих контекстов потом нужно забирать данные. Страница вывода поста — обращение как минимум к «посты», «комментарии» и «пользователи», блок «Самые комментируемые посты» — обращение как минимум к контекстам «посты» и «комментарии».

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

                                                                  Вот вопрос скорей стоял где та грань, когда из-за «много своей бизнес логики» вместо выделения в отдельный агрегат, нужно выделять в отдельный контекст, несмотря на то, что он как самостоятельная единица бессмысленен.
                                                                    +1
                                                                    Страница вывода поста — обращение как минимум к «посты», «комментарии» и «пользователи», блок «Самые комментируемые посты» — обращение как минимум к контекстам «посты» и «комментарии».

                                                                    Повторюсь. Это Read поток. Для оптимизации нагрузки рекомендуется не использовать домен на этом уровне (см. CQRS). Для оптимизации нагрузки все эти данные, как правило, кладутся в NoSQL хранилища или вытягиваются простыми запросами к БД и не строят многоуровневые агрегаты, а используются DTO или вообще массивы.


                                                                    Домен нужен для того, чтоб сформировать бизнес логику обработки Write потока, а доменные события тригерят обновление кеша и снепшотов для Read потока.


                                                                    Это мнение Эванса и Вернона и я с ними солидарен. Я не хочу сказать, что все должны делать так, но там где есть выделенный домен, как правило задумываются о разделении read и write потоков.


                                                                    Вот вопрос скорей стоял где та гран

                                                                    Грань каждый определяет сам. Есть некоторые рекомендации описанные в книгах по DDD, но четких правил нет.

                                                                      0
                                                                      Домен нужен для того, чтоб сформировать бизнес логику обработки Write потока, а доменные события тригерят обновление кеша и снепшотов для Read потока.

                                                                      Такой подход работает лишь в сильно специализированных не CRUD приложениях. Не менее важная логика есть и в потоке чтения.
                                                                        0
                                                                        Такой подход работает лишь в сильно специализированных не CRUD приложениях.

                                                                        Так в этом и смысл. Незачем городить DDD в CRUD приложениях.


                                                                        Под "Не менее важная логика" вы подразумеваете бизнес ограничения?
                                                                        Вы можете привести какой нибудь пример, чтоб мы говорили более предметно?

                                                                          0
                                                                          Хабр возьмите со счётчиком просмотров постов. Ну и требование, что пользователю при открытие поста счётчик должен показываться с учётом текущего его открытия. Вроде чистый запрос GET /company/otus/blog/353500/, но состояние домена мутирует. Причём не безусловно, а со сложной логикой типа повторный просмотр одним пользователем счётчик не увеличивает, просмотр автором поста не увеличивает, в корпоративных блогах просмотр сотрудниками не увеличивает, просмотр администрацией хабра не увеличивает, визиты поисковых систем игнорируются и т. п. Ну и возможность на будущее заложить начинать какой-то бизнес-процесс, если просмотр оказался энным по счёту просмотром поста, другой процесс, если оказался эмным просмотром данным пользователем и третий, если элным всех постов пользователей Ну типа беджик добавить автору или пользователю.

                                                                          Вроде поток чтения на 99%, но есть нюансы. Можно разделить read и write хранилища, но сходить придётся в оба, причём сначала во write, дождаться окончания записи во write хранилище и её отражения на read и только потом начать собственно запрос.
                                                                            0

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


                                                                            Обновлять щетчики просмотров можно 2 способами. Можно на fronend через js или картинки по аналогии с другими метриками. Можно на backend кидать event "страница просмотрена".
                                                                            Первый вариант предпочтительней так как в этом случае вы можете полностью или частично закешировать страницу и не дергать бекенд лишний раз. А щетчики на странице в этом случае обновлять через API.


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


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


                                                                            Подведя итог, для работы щетчиков поднимать домен не нужно.

                                                                              0
                                                                              Начнем с простого. Вы в read потоке выполняете запись. Это уже не правильно.

                                                                              CQRS у "меня" (в придуманной в процессе написания коммента архитектуре) на уровне Domain Layer, на уровне сервисов домена, а на Application/Use Cases Layer обычный вызов методов, которые могут мутировать домен, а могут не мутировать, могут возвращать результат, а могут не возвращать. В любой комбинации.


                                                                              Далее, с чего вы решили, что вам обязательно дожидаться обновления хранилища прежде чем отдавать контент пользователю?

                                                                              Бизнес-требование. Кроме того, вместо типичного ожидаемого контента отдаться может нечто совершенно другое, например, вместо страницы поста страница "поздравляем, вы выиграли приз, введите адрес куда его доставить, и ИНН для уплаты налогов на выигрыш."


                                                                              Обновлять щетчики просмотров можно 2 способами. Можно на fronend через js или картинки по аналогии с другими метриками.

                                                                              Просмотр в данной задаче не просто сбор метрики для маркетинга или эксплуатации, а доменное событие. Более того, совсем не факт, что на клиентской стороне обычный браузер.


                                                                              Но даже в случае второго способа с event, вы просто его бросаете и read поток на этом заканчивается.

                                                                              1) Предположительно событие бросается между проверкой наличия поста и запросом его контента.


                                                                              2) Read поток должен вернуть актуальное состояние домена, с учётом текущего посещения. Можно, конечно, эмулировать его, проработав всю логику учета текущего посещения домена после получения текущего состояния без учёта, и добавив 1 к результатам запроса, но всё равно нужно записать это в базу в пределах одной транзакции с получением результата запроса.

                                                                                0
                                                                                но всё равно нужно записать это в базу в пределах одной транзакции с получением результата запроса.

                                                                                Ещё раз. Почему в пределах именно одной транзакции? Почему нельзя обработать запрос в фоне?


                                                                                Даже если вы не используете Commad и Query, а используете методы, то почему метод в духе getPage() должен ещё и писать что-то? Кидать событие, да возможно, но не писать.

                                                                                  0
                                                                                  Под «Не менее важная логика» вы подразумеваете бизнес ограничения?
                                                                                  Вы можете привести какой нибудь пример, чтоб мы говорили более предметно?

                                                                                  Банально — поисковый запрос: отсеивание запрещённого для страны пользователя контента, анализ поискового запроса, внедрение своего контента на основе запроса и т.д.

                                                                                  А насчёт событий — о каких бизнес требованиях может идти речь, если Вы «просто бросаете событие»? Где гарантия их выполнения?

                                                                                  Можно на fronend через js или картинки по аналогии с другими метриками.

                                                                                  А как на фронте понять что нужно обновлять счётчик?..

                                                                                  У Вас уже логика прямо на фронте получилась. А на беке получается паутина из обработчиков событий. При этом это один use case.
                                                                                    0

                                                                                    Я вам говорю о базовых принципах не однократно описанные множеством известных специалистов. Вы можете пренебрегать этими принципами. Это ваше право.


                                                                                    А как на фронте понять что нужно обновлять счётчик?..

                                                                                    Я разве говорил, что нужно понимать это на фронте? Нет. Я говорил, что вы можете отправлять на сервер сообщения о просмотре, по аналогии с GA и YM, а на сервере уже применять просмотры в соответствии с вашей бизнес логикой.


                                                                                    У Вас уже логика прямо на фронте получилась.

                                                                                    Нет. Хотя она там может быть. Вспоминаем SPA.


                                                                                    А на беке получается паутина из обработчиков событий. При этом это один use case.

                                                                                    Паутины нет. События только так где они действительно нужны.
                                                                                    Вы говорити, что Use case один, а так ли это?


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

                                                                                    Вы собираетесь запихивать все это в один Use case, в один метод, в один GET запрос и пусть весь мир подождёт)))

                                                                                      0
                                                                                      Это разные аспекты одного Use case. В целях оптимизации времени выполнения основного запроса можно пойти на компромисс и ради скорости отказаться от транзакционной целостности, но это, как минимум, бизнес-решение, а не просто на уровне кода что-то намутить. Product owner должен решать допустимо ли в данном юзкейсе перейти к, например, целостности в конечном сч'те, а то и, о боже, допустить потерю событий просмотра в одном или даже всех аспектах.
                                                                                        0

                                                                                        Об том и спич

                                                                                          0
                                                                                          Это изменение бизнес-требований по причине неспособности разработчиков реализовать начальные с разумным бюджетом.
                                                                                    0

                                                                                    В пределах одной транзакции чтобы гарантировать точность и целостность. Чтобы не получить незасчитанных просмотров и не засчитывать просмотры дважды.


                                                                                    Событие — это тоже "писать", это изменение состояния. Сами события могут храниться в соседней таблице, хотя могут и во отдельном хранилище типа RabbitMQ или Kafka, да даже в памяти.

                                                                                      0
                                                                                      Событие — это тоже "писать", это изменение состояния.

                                                                                      Нет. Событие это сообщение возникающие в процессе выполнения программы. Само по себе событие ничего не меняет.

                                                                                        0
                                                                                        Сам факт появления события означает, что где-то что-то записалось, пускай в оперативку, пускай одна строка или даже булево значение, но записалось, причём для наружного потребления, как результат (обычно неявный, помещаемый в какую-то очередь, пускай в виде массива или списка, в дополнение к явному или неявному return) вызова какого-то метода. Грубо, не просто `new Event()` (хотя и тут могут быть варианты), а `$this->events[] = new Event()` — какое-то доступное извне локальной области видимости метода просмотра значение меняется.
                                                                      0
                                                                      Полный домен, DTO или массивы использовать — дело десятое. Всё равно нужно обращение к трём контекстам. Да, где-то можно «смухлевать», зная что всё хранится в одной базе и сделать четвертый контекст, который будет грубо нарушать границы первых трёх трехэтажным join запросом или дать такое право одному из трёх имеющихся.
                                                                    0
                                                                    Мы ещё не определились есть ли контекст комментария вообще, или он входит в контекст страницы. Есть ли необходимость вообще выделять комментарий в отдельную сущность, не входящую в агрегат страницы, не говоря о том, чтобы выносить эту отдельную сущность в отдельный контекст.
                                                                      0

                                                                      Разве? Вроде и по Ubiquitous Language прошлись. Напоминаю:


                                                                      • User — в контексте страниц пользователь является автором страниц;
                                                                      • User — в контексте комментариев к страницам пользователь является комментатором.

                                                                      Можем ещё модераторов добавить.


                                                                      • User — модератор страниц;
                                                                      • User — модератор комментариев;
                                                                      • Модератор страниц может подтвердить или отклонить публикацию страницы;
                                                                      • Модератор комментариев может удалить или заблокировать комментарий.

                                                                      Можем ещё пробежаться по Use cases страниц и комментариев.


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

                                                                        0

                                                                        Вы ввели понятие "комментатор" сильно позже.


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

                                                                          0
                                                                          Вы ввели понятие "комментатор" сильно позже.

                                                                          Я с самого начала говорил о комментаторе, но называл его автором комментариев. Я уже давно поправился.


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

                                                                          Ну во первых, сущность комментариев не такая уж и "мало-мальски значащая". Мы сейчас общаемся на Хабре в комментариях и я, обсуждая комментарии, ассоциирую их с комментариями Хабра, а комментарии на Хабре не такие уж и мало значащие.
                                                                          Во вторых, да. Контексты должны быть маленькими. Чем меньше контексты, тем они оптимальней. Большие контексты сродни отсутствию контекстов вообще. Если мне память не изменяет, то именно Вернон говорил об этом в своей книге. Рекомендую почитать если вы ещё не читали.

                                                                            0

                                                                            Чем меньше контексты, тем их больше, тем больше связей между ними (а каждая связь это не просто ссылка, а модуль трансляции понятий между ними), тем сложнее архитектура и выше её хрупкость. А в пределе у нас контекстом становится одна сущность и непонятно чем эти понятия отличаются друг друг от друга. Эванс явно говорит "контексты — это не модули", модули ЯП или платформы разработки могут использоваться как для разграничения контекстов, так и для организации элементов модели одного контекста, причём первое, как по мне, вынужденное использование имеющихся технических средств, когда несколько контекстов нужно заставить работать в рамках одного программного процесса, а платформа средств разбиянения выше модулей (сборки модулей, например) не предлагает. В PHP так и модулей нормальных нет и приходится довольствоваться соглашениями типа "третий элемент неймспейса — контекст, четвёртый — модуль". Приравнивая контекст к модулю мы лишаемся дополнительного уровня организации.


                                                                            Он же говорит о необходимости поиска баланса при выборе границ контекста, ведь преимущества и недостатки есть и у больших, и у малых контекстов и нельзя просто сказать "чем меньше контекст, тем лучше". В частности, при небольших контекстах резко растёт количество модулей интеграции между контекстами, например, количество преобразований сущностей в VO и обратно. А если преобразование заключается в полном копировании значений полей 1:1, то как-то непонятен вообще его смысл, но его наличие усложняет понимание работы системы.

                                                                              0

                                                                              Совершенно верно. Большие контексты плохо и совсем маленькие контексты тоже плохо. Нужно искать золотую середину. Моя золотая середина это два отдельных контекста комментарии и страницы. Ваша золотая середина это комментарии и страницы в одном контексте. Я уже устал это повторять. Давайте завязывать.

                                                                                0
                                                                                Я просто пытаюсь понять, какие плюсы в разделении контекстов страниц и комментариев. Особенно после введения в единый язык понятия «комментатор», который исключает неоднозначность в термине «автор». Я могу представить вспомогательные задачи, в рамках которых нужно выносить комментарии в отдельную сущность, модуль, а не держать их листьями агрегата «страница с комментариями», но вот зачем вынонсить их в отдельный контекст не понимаю, если не предполагается в обозримом будущем комментировать не только страницы. Как минимум это заставит создавать отдельный модуль интгерации контекстов и страниц, не позволит выбирать комментарии со страницами одним SQL-запросом или хранить их в рамках одного документа в базах типа MongoDB. Ради чего?
                                                                                  0
                                                                                  Как минимум это заставит создавать отдельный модуль интгерации контекстов и страниц, не позволит выбирать комментарии со страницами одним SQL-запросом или хранить их в рамках одного документа в базах типа MongoDB.

                                                                                  • Зачем модули интеграции если их не нужно интегрировать?
                                                                                  • Один SQL запрос так и так нельзя использовать для извлечения одной страницы и всех её комментариев.
                                                                                  • Документ в монге это конечно аргумент, но если использовать монгу как read хранилище, то можно. Структуру read хранилища определяет уровень представления, а не домен.

                                                                                  Я просто пытаюсь понять, какие плюсы в разделении контекстов страниц и комментариев.

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


                                                                                  Надеюсь не нужно объяснять чем плохи больше контексты?

                                                                                    0
                                                                                    Если говорим о чём-то типа Хабра, то интеграция нужна. Комментарии нужны для основного (предположительно по статистике) юзкейса — просмотра страницы с десктопного браузера. Комментарии (их количество) нужны для показа списка страниц. Страница нужна для показа списка комментариев пользователя. Это только навскидку, без учёта всяких «обсуждаемое». Интеграция может быть примитивной типа представления сущности в виде VO или ассоциативного массива 1:1 по основным свойствам, но её надо будет делать.

                                                                                    > Надеюсь не нужно объяснять чем плохи больше контексты?

                                                                                    Надеюсь не надо объяснять, чем плохи маленькие контексты? :)
                                                                                      0
                                                                                      Если говорим о чём-то типа Хабра, то интеграция нужна.

                                                                                      И опять вы выносить домен в уровень представления. Сначало говорили, что аннотация в домене это протечка инфраструктуры, а теперь сами же вытаскиваете домен в представление.


                                                                                      А если серьезно, то посмотрите хотя бы на эту страницу. Только ли статья на ней и только ли комментарии? На странице есть как минимум сущности из контекста аунтификации, что значит, что сосуществование разных контекстов в одном запросе на уровне представления это норма. И к API это тоже относится.

                                                                                        0
                                                                                        Причём тут уровень представления? Страница и комментарии сильно связаны друг с другом на уровне домена. Например, при перемещении страницы в черновки комментарии тоже не должны быть доступны как минимум обычному пользователю, потому что комментарии вне контекста страницы смысла не имеют.

                                                                                        Вы говорите о родительских контекстах для нескольких самостоятельных (что уже звучит близко к нонсенсу), но какую проблему решает не просто разделение прдемтной области на контексты, но ещё и организацию их в иерархию, а не просто «плоский список»? Как взаимодействуют друг с другом родительский и дочерние контексты, а так же дочерние друг с другом? На общих основаниях через слои изоляции контекстов? Если да, то в чём выражается «родственность»? Если у них есть особые правила друг для друга, нарушающие границы контекстов, то можно ли говорить о контекстах вообще, не считая их общей внешней границы?

                                                                                          0

                                                                                          Это как посмотреть на вопрос.


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


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


                                                                                          Вот вам и преимущество разделения.

                                                                                            0
                                                                                            Как взаимодействуют друг с другом родительский и дочерние контексты, а так же дочерние друг с другом?

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


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


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


                                                                                            А вот выделение субконтекстов из основного контекста для их изоляции, как в примере с страницами и комментариями, дело довольно спорное, как вы уже подметили.

                                                        0

                                                        я буду обновлять страницу перед отправкой комментария

                                                      0

                                                      Я немного сплоховал. Должно быть CommentatorId, а не CommentAuthorId.

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

                                                    Самое читаемое