Where is the aggregate root?

Adelf

Administrator
Команда форума
Хотел тут набросать пример простого домена и наткнулся на дилемму.
Обычная фриланс биржа. Термины брал из upwork. Кастомером постится Job и туда апплаятся фрилансеры. каждый со своей hourRate.
Поскольку хочется этим примером показать и Information hiding, не стал делать Freelancer::getHourRate(), а создание обьекта Proposal(Freelancer, HourRate, CoverLetter) возложил на фрилансера.
И тут вопрос. Для меня нормальный вариант, это когда Service(Application) Layer достает нужный агрегат рут, еще немного данных и делает один вызов метода агрегат рута(может я и не прав). попросить фрилансера создать обьект Proposal а потом добавить его в Job - это доменная логика в недоменном слое.

Остаются два варианта:

PHP:
final class Job
{
    public function newProposal(Freelancer $freelancer, string $coverLetterMessage)
    {
        $this->proposals[] = $freelancer->makeProposal($coverLetterMessage);
    }
}
ИЛИ

PHP:
final class Freelancer
{
    public function apply(Job $job, string $coverLetterMessage)
    {
        $job->newProposal(new Proposal($this, $this->hourRate, $coverLetterMessage));
    }
}
В первом проблема такая, что Job знает все, что надо фрилансеру для создания Proposal. coverLetter например, ненужное для него знание.
Во-втором все ок, но в дальнейших операциях агрегат рутом всегда будет Job. И это напрягает.

Полный пример двух вариантов - https://gist.github.com/adelf/a3c8a88879c1a200122ca1dd5030ba9d
 

Adelf

Administrator
Команда форума
Вспомнилась тут модель акторов.
Актор Freelancer создает Proposal и кидает сообщение. Актор Job ловит это сообщение и делает что нужно.
 

WMix

герр M:)ller
Партнер клуба
По мне обе модели странные, (ближе вторая) я вижу это так: есть работа, есть предложения, которые создаются разработчиками. у разработчика есть несколько предложений на разные работы. Отсюда предложение имеет ссылку на разработчика и на работу. Агрегатом может быть как работа так и разработчик или предложениe, в зависимости от контекста.

Работодатель создает джоб, работник делает предложение для конкретной работы, работодатель принимает/отклоняет предложения.

PHP:
class Job{
    private $employer;
}

class Proposal{
    private $job;
    private $freelancer;
}

class Employer{
    public function makeJob( ...$blabla ){
        return new Job($this, $blabla);
    }
    public function acceptProposal(Proposal $proposal){...}
    public function declineProposal(Proposal $proposal){...}
}

class Freelancer{
    public function makeProposal(Job $job, string $coverLetterMessage){
        return new Proposal($this, $job, $coverLetterMessage);
    }
}
 

Вурдалак

Продвинутый новичок
Не стоит передавать ссылки на aggregate root'ы.
Это и технически будет мешать (AR должен быть консистентен в любой момент времени; есть другой AR становится его частью, то придётся делать что-то типа SELECT ... FOR UPDATE для двух сущностей; следовательно, одна база, отсюда куча ограничений).
Также это будет мешать моделям: сущности «Freelancer» и «Job» вполне могут быть в разных BC, в разных микросервисах. Допускаю, что может быть полезно иметь read model Freelancer или value object в BC с Job, но сама сущность (особенно для проекта уровня upwork) — вряд ли. Нужно передавать просто идентификаторы.

Proposal можно сделать AR/VO, it depends (или тем, и другим, если в разных BC).

Попробуй пользоваться командами/событиями.

Поскольку хочется этим примером показать и Information hiding, не стал делать Freelancer::getHourRate(), а создание обьекта Proposal(Freelancer, HourRate, CoverLetter) возложил на фрилансера.
А на upwork разве нельзя указывать различный hour rate для разных предложений работы? Я не помню уже, но мне кажется, что hour rate желательно подставлять как дефолтное значение в UI, где можно предложить услуги, где это всегда можно поправить для конкретной работы. Тут как раз пример того, что Freelancer и Job у тебя как-то жёстко связаны.

Я понимаю, что может быть передавать ссылки на объекты — это красиво и выглядит, так, как было бы в идеальном мире ООП с машинами, у которых бесконечные ресурсы. Но реальность такова, то AR не должен содержать прямые ссылки на внешние объекты, только id. А в event sourcing такой подход в принципе не будет работать.

попросить фрилансера создать обьект Proposal а потом добавить его в Job - это доменная логика в недоменном слое.
Написать new ClassName — это даже если и бизнес-логика, то сверхпримитивная. Не нужно мучить себя такой ерундой. Как я уже сказал, фрилансер может быть вообще в другом BC, а в этот просто прилетают команды на добавление Proposal'ов.
 
Последнее редактирование:

Adelf

Administrator
Команда форума
Ага. Примерно как я и сказал.
Актор Freelancer создает Proposal и кидает сообщение. Актор Job ловит это сообщение и делает что нужно.
Да, простое предположение что фрилансер к этому BC имеет крайне малое отношение сразу все раскидывает по полочкам. Спасиб.
 
Сверху