Właściwy sposób tworzenia encji potomnych za pomocą DDD

Jestem całkiem nowy w świecie DDD i po przeczytaniu kilku książek na ten temat (wśród nich Evans DDD) nie udało mi się znaleźć odpowiedzi na moje pytanie w Internecie: jaki jest właściwy sposób tworzenia jednostek podrzędnych z DDD? Widzisz, wiele informacji w Internecie działa na prostym poziomie. Ale diabły w szczegółach i dla uproszczenia są zawsze pomijane w dziesiątkach próbek DDD.

Pochodzę zmoja własna odpowiedź na podobne pytanie tutaj na stackoverflow. Nie jestem całkowicie usatysfakcjonowany moją własną wizją tego problemu, więc pomyślałem, że muszę rozwinąć tę sprawę.

Na przykład muszę stworzyć prosty model reprezentujący nazewnictwo samochodów: firmę, model i modyfikację (na przykład Nissan Teana 2012 - będzie to firma „Nissan”, model „Teana” i modyfikacja „2012”).

Szkic modelu, który chcę utworzyć, wygląda tak:

CarsCompany
{
    Name
    (child entities) Models
}

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


CarsModification
{
    (parent entity) Model
    Name
}

Więc teraz muszę utworzyć kod. Będę używać języka C # jako języka i NHibernate jako ORM. Jest to ważne, a co zwykle nie jest pokazywane w ogromnych próbkach DDD w Internecie.

Pierwsze podejście.

Zacznę od prostego podejścia do tworzenia typowych obiektów metodami fabrycznymi.

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


    private readonly ISet<CarsModel> _models = new HashedSet<CarsModel> ();


    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<CarsModification> Modifications { get { return new ImmutableSet<CarsModification> (this._modifications); } }


    private readonly ISet<CarsModification> _modifications = new HashedSet<CarsModification> ();


    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
        };
    }
}

Wadą tego podejścia jest to, że tworzenie modelu nie dodaje go do kolekcji modeli macierzystych:

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 ();
}

Po zatwierdzeniu transakcji i opróżnieniu sesji ORM poprawnie zapisze wszystkie dane w bazie danych, a następnym razem, gdy załadujemy tę firmę, kolekcja modeli będzie poprawnie przechowywać nasz model. To samo dotyczy modyfikacji. Więc to podejście pozostawia naszą jednostkę nadrzędną w niespójnym stanie, dopóki nie zostanie przeładowana z bazy danych. Nie idź.

Drugie podejście.

Tym razem użyjemy opcji specyficznej dla języka, aby rozwiązać problem ustawiania chronionych właściwości innych klas - mianowicie użyjemy modyfikatora „protected internal” zarówno na setterach, jak i konstruktorze.

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


    private readonly ISet<CarsModel> _models = new HashedSet<CarsModel> ();


    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<CarsModification> Modifications { get { return new ImmutableSet<CarsModification> (this._modifications); } }


    private readonly ISet<CarsModification> _modifications = new HashedSet<CarsModification> ();


    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 ();
}

Tym razem tworzenie każdej jednostki pozostawia zarówno jednostkę nadrzędną, jak i podrzędną w stanie spójnym. Ale walidacja stanu jednostki podrzędnej wyciekła do jednostki nadrzędnej (AddModel iAddModification metody). Ponieważ nigdzie nie jestem ekspertem w DDD, nie jestem pewien, czy jest w porządku, czy nie. Może to spowodować więcej problemów w przyszłości, gdy właściwości elementów potomnych nie będą mogły być po prostu ustawione za pomocą właściwości, a ustawienie jakiegoś stanu w oparciu o przekazane parametry wymagałoby bardziej złożonej pracy, która przypisuje wartość parametru właściwości. Miałem wrażenie, że powinniśmy koncentrować logikę na istocie wewnątrz tej istoty, gdziekolwiek jest to możliwe. Dla mnie takie podejście zamienia obiekt macierzysty w rodzaj hybrydy Entity & Factory.

Trzecie podejście.

Ok, zmienimy obowiązki utrzymywania relacji rodzic-dziecko.

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


    private readonly ISet<CarsModel> _models = new HashedSet<CarsModel> ();


    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<CarsModification> Modifications { get { return new ImmutableSet<CarsModification> (this._modifications); } }


    private readonly ISet<CarsModification> _modifications = new HashedSet<CarsModification> ();


    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 ();
}

Podejście to uzyskało logikę walidacji / tworzenia w odpowiednich jednostkach i nie wiem, czy jest dobra czy zła, ale poprzez proste utworzenie obiektu metodą fabryczną niejawnie dodajemy go do kolekcji potomnej obiektu nadrzędnego. Po zatwierdzeniu transakcji i opróżnieniu sesji do bazy danych będą 3 wstawki, nawet jeśli nigdy nie napisałem w moim kodzie polecenia „dodaj”. Nie wiem, może to tylko ja i moje ogromne doświadczenie poza światem DDD, ale na razie jest to trochę nienaturalne.

Więc jaki jest najbardziej poprawny sposób dodawania encji potomnych za pomocą DDD?

questionAnswers(4)

yourAnswerToTheQuestion