Правильный способ создания дочерних объектов с DDD

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

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

Например, мне нужно создать простую модель, представляющую названия автомобилей: фирма, модель и модификация (например, Nissan Teana 2012 - это будет фирма "Nissan", модель "Teana" и модификация "2012").

Эскиз модели, которую я хочу создать, выглядит так:

CarsCompany
{
    Name
    (child entities) Models
}

CarsModel
{
    (parent entity) Company
    Name
    (child entities) Modifications
}


CarsModification
{
    (parent entity) Model
    Name
}

Итак, теперь мне нужно создать код. Я буду использовать C# в качестве языка и NHibernate в качестве ORM. Это важно и то, что обычно не отображается в обширных образцах DDD в Интернете.

Первый подход.

Я начну с простого подхода с типичным созданием объектов фабричными методами.

public class CarsCompany
{
    public virtual string Name { get; protected set; }
    public virtual IEnumerable Models { get { return new ImmutableSet (this._models); } }


    private readonly ISet _models = new HashedSet ();


    protected CarsCompany ()
    {
    }


    public static CarsCompany Create (string name)
    {
        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        return new CarsCompany
        {
            Name = name
        };
    }


    public void AddModel (CarsModel model)
    {
        if (model == null)
            throw new ArgumentException ("Model is not specified.");

        this._models.Add (model);
    }
}


public class CarsModel
{
    public virtual CarsCompany Company { get; protected set; }
    public virtual string Name { get; protected set; }
    public virtual IEnumerable Modifications { get { return new ImmutableSet (this._modifications); } }


    private readonly ISet _modifications = new HashedSet ();


    protected CarsModel ()
    {
    }


    public static CarsModel Create (CarsCompany company, string name)
    {
        if (company == null)
            throw new ArgumentException ("Company is not specified.");

        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        return new CarsModel
        {
            Company = company,
            Name = name
        };
    }


    public void AddModification (CarsModification modification)
    {
        if (modification == null)
            throw new ArgumentException ("Modification is not specified.");

        this._modifications.Add (modification);
    }
}


public class CarsModification
{
    public virtual CarsModel Model { get; protected set; }
    public virtual string Name { get; protected set; }


    protected CarsModification ()
    {
    }


    public static CarsModification Create (CarsModel model, string name)
    {
        if (model == null)
            throw new ArgumentException ("Model is not specified.");

        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        return new CarsModification
        {
            Model = model,
            Name = name
        };
    }
}

Плохая сторона этого подхода заключается в том, что создание модели не добавляет ее в коллекцию родительских моделей:

using (var tx = session.BeginTransaction ())
{
    var company = CarsCompany.Create ("Nissan");

    var model = CarsModel.Create (company, "Tiana");
    company.AddModel (model);
    // (model.Company == company) is true
    // but (company.Models.Contains (model)) is false

    var modification = CarsModification.Create (model, "2012");
    model.AddModification (modification);
    // (modification.Model == model) is true
    // but (model.Modifications.Contains (modification)) is false

    session.Persist (company);
    tx.Commit ();
}

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

Второй подход.

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

public class CarsCompany
{
    public virtual string Name { get; protected set; }
    public virtual IEnumerable Models { get { return new ImmutableSet (this._models); } }


    private readonly ISet _models = new HashedSet ();


    protected CarsCompany ()
    {
    }


    public static CarsCompany Create (string name)
    {
        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        return new CarsCompany
        {
            Name = name
        };
    }


    public CarsModel AddModel (string name)
    {
        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        var model = new CarsModel
        {
            Company = this,
            Name = name
        };

        this._models.Add (model);

        return model;
    }
}


public class CarsModel
{
    public virtual CarsCompany Company { get; protected internal set; }
    public virtual string Name { get; protected internal set; }
    public virtual IEnumerable Modifications { get { return new ImmutableSet (this._modifications); } }


    private readonly ISet _modifications = new HashedSet ();


    protected internal CarsModel ()
    {
    }


    public CarsModification AddModification (string name)
    {
        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        var modification = new CarsModification
        {
            Model = this,
            Name = name
        };

        this._modifications.Add (modification);

        return modification;
    }
}


public class CarsModification
{
    public virtual CarsModel Model { get; protected internal set; }
    public virtual string Name { get; protected internal set; }


    protected internal CarsModification ()
    {
    }
}

...

using (var tx = session.BeginTransaction ())
{
    var company = CarsCompany.Create ("Nissan");
    var model = company.AddModel ("Tiana");
    var modification = model.AddModification ("2011");

    session.Persist (company);
    tx.Commit ();
}

На этот раз создание каждого объекта оставляет как родительский, так и дочерний объект в согласованном состоянии. Но проверка состояния дочерней сущности просочилась в родительскую сущность (методы AddModelи AddModification). Поскольку я нигде не разбираюсь в DDD, я не уверен, нормально это или нет. Это может создать больше проблем в будущем, когда свойства дочерних сущностей нельзя будет просто установить с помощью свойств, а настройка некоторого состояния на основе переданных параметров потребует более сложной работы, чем присвоение значения параметра свойству. У меня сложилось впечатление, что мы должны концентрировать логику о сущности внутри этой сущности везде, где это возможно. Для меня этот подход превращает родительский объект в некий гибрид Entity&Factory.

Третий подход.

Хорошо, мы поменяем местами обязанности по поддержанию отношений между родителями и детьми.

public class CarsCompany
{
    public virtual string Name { get; protected set; }
    public virtual IEnumerable Models { get { return new ImmutableSet (this._models); } }


    private readonly ISet _models = new HashedSet ();


    protected CarsCompany ()
    {
    }


    public static CarsCompany Create (string name)
    {
        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        return new CarsCompany
        {
            Name = name
        };
    }


    protected internal void AddModel (CarsModel model)
    {
        this._models.Add (model);
    }
}


public class CarsModel
{
    public virtual CarsCompany Company { get; protected set; }
    public virtual string Name { get; protected set; }
    public virtual IEnumerable Modifications { get { return new ImmutableSet (this._modifications); } }


    private readonly ISet _modifications = new HashedSet ();


    protected CarsModel ()
    {
    }


    public static CarsModel Create (CarsCompany company, string name)
    {
        if (company == null)
            throw new ArgumentException ("Company is not specified.");

        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        var model = new CarsModel
        {
            Company = company,
            Name = name
        };

        model.Company.AddModel (model);

        return model;
    }


    protected internal void AddModification (CarsModification modification)
    {
        this._modifications.Add (modification);
    }
}


public class CarsModification
{
    public virtual CarsModel Model { get; protected set; }
    public virtual string Name { get; protected set; }


    protected CarsModification ()
    {
    }


    public static CarsModification Create (CarsModel model, string name)
    {
        if (model == null)
            throw new ArgumentException ("Model is not specified.");

        if (string.IsNullOrEmpty (name))
            throw new ArgumentException ("Invalid name specified.");

        var modification = new CarsModification
        {
            Model = model,
            Name = name
        };

        modification.Model.AddModification (modification);

        return modification;
    }
}

...

using (var tx = session.BeginTransaction ())
{
    var company = CarsCompany.Create ("Nissan");
    var model = CarsModel.Create (company, "Tiana");
    var modification = CarsModification.Create (model, "2011");

    session.Persist (company);
    tx.Commit ();
}

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

Итак, какой самый правильный способ добавления дочерних объектов с помощью DDD?

10
задан Community 23 May 2017 в 12:10
поделиться