Репозитории как фабрики?

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

public class Aggregate {
   public int Id { get; }
   public IEnumerable<Entities> Entities { get; }
   public Entity CreateEntity(params);
}

public class Entity {
   public int Id { get; }
   public Aggregate Parent { get; }
}

Внезапно меня осенило очень важное понятие об агрегатах: агрегаты не появляются волшебным образом из ниоткуда. Не существует такого понятия, как «новый агрегат (id);». в мире ДДД.

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

public class MyAggregate {
    public int Id { get; private set; }

    protected MyAggregate() {}

    public MyAggregate(int id) {
        Id = id;
    }
}

public interface IMyAggregateRepository {

    MyAggregate Create();
    void DeleteById(int id);
    void Update(MyAggregate aggregate);
    MyAggregate GetById(int id);
    // no Add() method on this layer!
}

private class EfMyAggregateRepository : IAggregateRepository {

    public EfMyAggregateRepository(DbContext context) {
        ...
    }

    public MyAggregate Create() {
        var pto = context.Create<MyAggregate>();
        context.Set<MyAggregate>().Attach(pto);
        return pto;
    }

}

Таким образом, база данных (или, например, EF) могла бы автоматически генерировать ключ, возможно, определяя правила проверки в репозитории, которые также применяются, если объект изменяется (и обновляется) и т. д.

Или я сейчас что-то путаю? Это больше задача сервиса/завода?


person xvdiff    schedule 08.01.2015    source источник
comment
Я помещаю свои методы создания в репозиторий в основном потому, что это избавляет меня от необходимости проходить через фабрику, а также репозиторий. У меня работает, но я, конечно, не претендую на звание эксперта по DDD.   -  person Cerad    schedule 08.01.2015
comment
Я склонен согласиться с ответом @MikeSW ниже. У Уди Дахана также есть особый взгляд на это, который, возможно, стоит прочитать: udidahan.com/2009/06/29/dont-create-aggregate-roots   -  person guillaume31    schedule 09.01.2015
comment
@guillaume31 guillaume31 Я тоже это читал и нашел это интересным. Однако не будет ли это большим снижением производительности? Насколько я понимаю, коллекции должны быть загружены перед добавлением в них.   -  person plalx    schedule 09.01.2015
comment
@plalx Я никогда этого не делал (всегда использовал Repository.Add()), но, возможно, вы можете избежать этого, просто добавив новый идентификатор совокупного корня в список идентификаторов, как это рекомендуется для отношений между агрегатами. Не знаю, как с этим справятся различные ORM.   -  person guillaume31    schedule 09.01.2015
comment
@ guillaume31 guillaume31 Что ж, рекомендуемая практика для отношения агрегата к агрегату — моделировать его со стороны «многие к одному», когда это возможно. Следовательно, у вас нет коллекций.   -  person plalx    schedule 09.01.2015


Ответы (3)


Репозиторий просто абстрагирует персистентность, и хотя он восстанавливает (возможно, само хранилище делает восстановление) совокупный корень, он не создает его. Цель репозитория не в том, чтобы создавать объекты.

Цель фабрики — создавать объекты, однако фабрика используется, когда создание не является простым (например, new myobject() ), и это зависит от некоторых правил или вы не знаете, какой конкретный тип запрашивать (абстрактная фабрика) .

Насчет корня агрегатов должен откуда-то взяться, я не согласен. Они не обязательны , но предпочтительнее, если это имеет смысл с семантической точки зрения. Я делаю "new MyAggregate(id)" все время, это не проблема, нет необходимости форсировать события в соответствии с каким-то произвольным правилом только потому, что кто-то так сказал. Если у вас есть веская причина (дизайн, техническая), то сделайте это. Если нет, не усложняйте себе жизнь.

person MikeSW    schedule 08.01.2015
comment
Приятно видеть, что кто-то поощряет использование ключевого слова new. Некоторые фанатики SRP слишком религиозно относятся к отмене ключевого слова new. Я предпочел бы случайный new, чем взрыв класса со специально созданными фабриками повсюду. - person Adrian Thompson Phillips; 09.01.2015
comment
Я согласен со всем, что вы сказали, однако я считаю, что большинство совокупного кода создания обычно находит свое естественное место на существующих агрегатах. Например. post = discussion.post(msg), а не post = new Post(discussionId, msg). Наличие фабричного метода на совокупных корнях имеет несколько преимуществ, таких как уменьшение количества параметров, необходимых для создания, и, что более важно, лучшее выражение домена. Однако это, очевидно, может оказать негативное влияние на производительность, так как вы должны загрузить другой агрегат, но обычно это незначительно. - person plalx; 09.01.2015
comment
Я очень редко нахожу причину иметь фабричный метод в AR для создания другого AR. Обычно один AR не знает о других, это во многом зависит от предметной области, которую вы моделируете. И «загружаю» еще один AR!?? - person MikeSW; 09.01.2015
comment
@MikeSW Это довольно распространенная практика, и она обсуждается в книге «Внедрение доменно-ориентированного проектирования», которая, вероятно, является самой известной книгой после синей, если вы хотите прочитать об этом. - person plalx; 10.01.2015
comment
@plalx Я согласен, если вы имеете в виду сущности. Если вы имеете в виду агрегаты, то я согласен с MikeSW — почему агрегат должен отвечать за создание и/или загрузку другого агрегата? Банка червей! - person Chalky; 24.12.2016
comment
@Chalky Создание да, загрузка нет. Агрегаты не должны появляться из ниоткуда. Использование агрегатов в качестве фабрик для других агрегатов является очень распространенной практикой и часто позволяет более точно следовать Ubiquitous Language. Большинство книг по DDD, которые я читал, также предлагают этот подход. С точки зрения объектно-ориентированного программирования это также имеет большой смысл. - person plalx; 24.12.2016
comment
@plalx ах да, теперь я понял, спасибо. Соглашаться. Меня очень интересует вопрос этого ОП, и я хочу узнать, публикует ли кто-нибудь решение, отличное от «вам нужно создать ключи GUID», для использования EF, отложенной загрузки и ключей, сгенерированных БД. Очевидно, что в этой ситуации есть зависимость от репозитория, так как вам нужно вызвать DBSet‹T›.Create, чтобы получить прокси-объект, и что-то должно сохранить его, если требуется идентификатор. - person Chalky; 24.12.2016
comment
@Chalky Я опубликовал свой ответ на вопрос. Что касается ленивой загрузки, то в большинстве случаев ее можно рассматривать как совокупный антипаттерн. Подумайте о том, чтобы сделать ленивую часть собственным агрегатом. Если это абсолютно необходимо, и у вас нет прокси-серверов, тогда просто используйте выделенные операции выборки, такие как findCustomerToDoSomething, которые будут лениво загружать данные, необходимые для части doSomething, но, как я уже сказал, избегайте ленивой загрузки, когда это возможно. - person plalx; 25.12.2016

Совокупные и совокупные корни

В реальных приложениях явная Aggregate реализация не требуется. Aggregate – это набор связанных entities, ограниченных aggregation relation, с заданным основным entity, называемым Aggregate Root.

Вам просто нужно решить, что entities является aggregate roots, а затем спроектировать свои классы и агрегаты в приложении с aggregate root encapsulation and other rules.

С учетом этого я предлагаю следующие интерфейсы и классы

interface IAggregateRoot<TKey> 
{
    TKey Key { get; }
}

//entity which is aggregate root
class Car : IAggregateRoot<int>
{
   int Key { get; }
   Ienumerable<Door> Doors { get; }
}

//other entity, not aggregate root
class Door 
{
   string Colour { get; set; }
}

//generic interface with IAggregateRoot constraint
interface IRepository<TAggregateRoot, TKey> where TAggregateRoot : IAggregateRoot
{
   TAggregateRoot Get(TKey key)
   //other methods
}

Фабрики и хранилища

И factories, и repositories технически создают экземпляры aggregate roots и другие, ограниченные агрегатом entities. Оба строят aggregate, но имеют разную семантику.

Репозиторий

Repository восстанавливает aggregate из постоянного хранилища (с учетом модели хранения) и предполагает, что агрегат находится в консистентном состоянии, то есть агрегатные инварианты не нарушены.

Если есть инвариантное нарушение, то Repository приходится делать какие-то действия, т.к. уже созданный агрегат несовместим (?!).

Фабрика

Factory больше похож на строителя. Он строит aggregate со знанием своих инвариантов. Во время построения aggregate aggregate может находиться в несогласованном состоянии, но по мере завершения процесса построения aggregate он должен соблюдать инварианты и защищать их.

Если по каким-то причинам происходит нарушение инварианта, factory не может построить aggregate и отбрасывает его.

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

Пример

Один пример того, как может работать фабрика.

Предположим, что инвариантом класса Car является «Автомобиль может иметь только 2 или 4 двери».

public interface ICar : IAggregateRoot<int>
{
    int Key { get; }
    IEnumerable<IDoor> Doors { get; }
    SetDoors(IEnumerable<IDoor> doors);
}

public class Car : ICar
{
    //take a look here, it's internal
    internal IList<IDoor> Doors { get; set;}

    public int Key { get; set; }
    public IEnumerable<IDoor> Doors { get { return Doors; } }

    //aggregate root always controls aggregate invariants
    public SetDoors(IEnumerable<IDoor> doors)
    {
        if (doors.Count() != 2 || doors.Count() != 4)
            throw new ApplicationException();
        Doors = doors.ToList();
    }

}

interface IDoor { }

class Door : IDoor { }

//generic interface with IAggregateRoot constraint
interface ICarFactory
{
    ICar CreateCar(int doorsCount)
}

public class CarFactory : ICarFactory
{
    public ICar CreateCar(int doorsCount) 
    {
        if (doorsCount != 2 && doorsCount != 4)
            throw new ApplicationException();

        var car = new Car();
        car.Key = 1;

        car.Doors = new List<IDoor>();
        car.Doors.Add(new Door());
        //now car is inconsistent as it holds just one door
        car.Doors.Add(new Door());
        //now car is consistent
        return car;
     }
 } 

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

Теперь предполагается, что Factory и Aggregate Root имеют тесные связи, поскольку Factory знает о внутренних полях Car.

Наконец, я думаю, что вы неправильно понимаете некоторые концепции, и рекомендую вам прочитать книгу Evans DDD.

РЕДАКТИРОВАТЬ: Возможно, я упускаю тот факт, что MyAggregate на самом деле является MyAggregateRoot. Но для меня странно смешивать эти понятия. Более того, на SO был один вопрос с явным искусственным классом Aggregate, содержащим все агрегатные внутренности только для того, чтобы показать границы агрегатов. Я предпочитаю приемлемое понятие AggregateRoot, как и большинство диаграмм, которые я видел http://ptgmedia.pearsoncmg.com/images/chap10_9780321834577/elementLinks/10fig05.jpg

person Valentin P.    schedule 08.01.2015
comment
Я боролся, если я должен понизить ваш ответ. Прочитав это дважды, я решил сделать это, потому что вы создаете такую ​​мешанину из понятий. Я знаю эту штуку, а тебе удалось меня запутать. Ваше определение агрегатов .. особенное, а ваш пример просто усложнен без видимой причины. Нет причин, по которым кто-то будет использовать фабрику, когда достаточно «новой машины (2)». Кроме того, IMO Car должен получать все данные ввода/создания через конструктор. - person MikeSW; 08.01.2015
comment
Если есть нарушение инварианта, то репозиторий должен выполнить некоторые действия, так как уже созданный агрегат несовместим (?!). Нет, репозиторий не знает о правилах согласованности. При восстановлении объект должен генерировать исключение, если он находится в недопустимом состоянии (этого не должно происходить). Репо ничего не делает. - person MikeSW; 08.01.2015
comment
Кроме того, шаблон IDoor, Door и ICar Car в большинстве случаев является анти-шаблоном. Не используйте интерфейсы, если вы не можете придумать более одного конкретного класса. - person plalx; 09.01.2015
comment
@plalx Это? Я знаю, что это бессмысленно, однако я думаю, что это может быть полезно, если вы, например. украсьте модель предметной области интерфейсом и используйте ее в качестве эталона для моделей приложений и инфраструктуры (dtos, ptos). Я никогда этого не делал, но вы можете иметь IEnumerable в модели домена, но вернуть ICollection в dto для Automapper и т. д. Таким образом, интерфейсы никогда не меняются, но вы можете поменять реализацию с технической точки зрения. . Кроме того, если вам когда-нибудь понадобится провести рефакторинг вашего домена, вы получите мгновенный ответ от компилятора о том, что необходимо для адаптации изменений. - person xvdiff; 09.01.2015
comment
@xvdiff В то время как IEnumerable или IUserRepository очевидно могут иметь несколько реализаций, IDoor или ICar могут и не быть. Автоматическое создание IWhatever является сверхинженерным, если нет реальной причины. Я обычно абстрагирую сервисы, репозитории или другие «утилиты», где меня не особо волнует реализация в этот момент. Однако при моделировании объектов предметной области или простых моделей представлений нет причин (или очень мало для объектов предметной области) абстрагироваться от них. - person MikeSW; 09.01.2015
comment
@MikeSW, конечно, многие концепции перепутаны, поскольку я не могу полностью переписать здесь книгу Эванса. С «новой машиной (2)» вы совершенно не правы. По крайней мере, вы не можете подделать его в модульных тестах, а также он нарушает SRP. Иногда совокупный корень конструирует его внутри, но иногда нет. Я добавил комментарий, объясняющий этот пример. Опять же, я не могу создать здесь сложный домен с агрегатами, которые нельзя построить одним вызовом. - person Valentin P.; 10.01.2015
comment
@MikeSW, судя по тому, что репозиторий не может проверить ограничения, это просто ваше мнение, которое звучит слишком убедительно. Если я найду в книге Эванса цитату, содержащую противоположное мнение, вы согласны удалить оба своих комментария, поскольку они неконструктивны? - person Valentin P.; 10.01.2015
comment
По крайней мере, сейчас я только что нашел Фабрики, восстанавливающие объекты с этой возможностью, которые в большинстве случаев реализованы как встроенные части Хранилища. - person Valentin P.; 10.01.2015
comment
@ВалентинП. Я знаю, что ни в чем не ошибаюсь, но оставлю все как есть, так как не думаю, что будет какой-то положительный результат от продолжительного обсуждения. - person MikeSW; 11.01.2015
comment
@MikeSW, пфф. Я знаю, что я совсем не ошибаюсь, я знаю это... Идеальное рассуждение! Прочитав ваш блог, я стала о вас лучшего мнения. - person Valentin P.; 11.01.2015
comment
@MikeSW Это то, на что ссылается ОП из книги Эванса. "A FACTORY reconstituting an object will handle violation of an invariant differently. During creation of a new object, a FACTORY should simply balk when an invariant isn't met, but a more flexible response may be necessary in reconstitution. If an object already exists somewhere in the system (such as in the database), this fact cannot be ignored. Yet we also can't ignore the rule violation. There has to be some strategy for repairing such inconsistencies, which can make reconstitution more challenging than the creation of new objects." - person plalx; 12.01.2015
comment
@plalx, ​​интересно, что MikeSW написал (в своем ответе) только общие факты, написанные везде. В моем ответе тоже было написано. Но с дополнительным нестандартным примером, приведенным в виде вопроса xvdiff о том, как работают фабрики, я получил 0 голосов и 5 голосов за MikeSW. Таким образом, это не место для хороших дизайнерских мыслей. Но я не против :) - person Valentin P.; 12.01.2015
comment
@plalx, ​​IDoor и ICar тоже подходят, потому что 1) использование интерфейсов делает код тестируемым, 2) использование интерфейсов обеспечивает дополнительный уровень инкапсуляции, что критично для текущего примера, поскольку Factory должен получить доступ к внутренней структуре агрегата, но другие имеют нет. Если вы используете «новые» вместо Factory и конкретные классы вместо интерфейсов, вы не знаете, что такое модульное тестирование. Я даже не говорю о других хороших (оправданных!) методах проектирования. И, опять же, это просто расширенный пример того, как работает фабрика CAN, это не принцип. - person Valentin P.; 12.01.2015
comment
Из-за инженерии... Я использую Resharper, а вы? - person Valentin P.; 12.01.2015

«Внезапно меня осенило очень важное понятие об агрегатах: агрегаты не появляются волшебным образом из ниоткуда».

Вы должны понимать, что обычно предпочитают не new создавать агрегаты напрямую, а более точно следовать Ubiquitous Language (UL).

Например, бизнес-эксперты могут сказать, что "посетители сайта смогут зарегистрироваться, чтобы стать клиентами", поэтому вместо new Customer(visitorId, ...) у вас может быть что-то вроде Customer customer = visitor.register(...).

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

«Нет такой вещи, как «новый агрегат (id);» в мире DDD».

На самом деле это совсем не так. Когда нет законной концепции домена, которая может создать агрегат (и даже если он есть), new создание агрегата напрямую — это нормально.

Введение ненужной технической фабрики только ради того, чтобы избежать new, только усложнит конструкцию.

«Учитывая, что идентификация агрегата может быть суррогатом, сгенерированным базой данных, не будет ли правдоподобным, что репозиторий отвечает за создание агрегата?»

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

Если вам нужен идентификатор, сгенерированный базой данных, вы можете поместить операцию, которая возвращает такой идентификатор в репозиторий. Например, у вас может быть метод public int nextIdentity() в репозитории, который возвращает следующее значение последовательности базы данных.

Следуя Принципу разделения интерфейсов (ISP), вы можете решить создать выделенный интерфейс для задачи, например CustomerIdentityGenerator.

person plalx    schedule 25.12.2016
comment
@xvdiff Ты забыл свой вопрос? - person plalx; 07.04.2017