Dipendenza circolare e creazione di oggetti durante il tentativo di DDD

1

Ho un dominio in cui Organization ha People .

Entità organizzazione

public class Organization {
    private readonly List<Person> _people = new List<Person>();

    public Person CreatePerson(string name) {
        var person = new Person(organization, name);
        _people.Add(person);
        return person;
    }

    public IEnumerable<Person> People {
        get { return _people; }
    }
}

Entità persona

public class Person
{
    public Person(Organization organization, string name) {
        if (organization == null) {
            throw new ArgumentNullException("organization");
        }

        Organization = organization;
        Name = name;
    }

    public Organization { get; private set; }   
    public Name { get; private set; }
}

La regola per questa relazione è che un Person deve appartenere esattamente a un Organization .

Gli invarianti che voglio garantire sono:

  1. Una persona deve avere un'organizzazione

    • questo viene applicato tramite il costone della persona
  2. Un'organizzazione deve conoscere la sua gente

    • questo è il motivo per cui l'organizzazione ha un metodo CreatePerson
  3. Una persona deve appartenere a una sola organizzazione

    • questo è il motivo per cui la lista delle persone dell'organizzazione non è mutabile pubblicamente (ignorando il casting su List, forse ToEnumerable può applicarlo, non troppo preoccupato però)

Ciò che voglio è che se una persona viene creata, l'organizzazione ne sa la sua creazione.

Tuttavia, il problema con il modello attualmente è che sei in grado di creare una persona senza mai aggiungerla alla collezione delle organizzazioni.

Ecco un test unitario in errore per descrivere il mio problema

[Test]
public void AnOrganizationMustKnowOfItsPeople()
{
    var organization = new Organization();
    var person = new Person(organization, "Steve McQueen");

    CollectionAssert.Contains(organization.People, person);
}

Qual è il modo più idiomatico per applicare gli invarianti e la relazione circolare?

    
posta Matthew 11.06.2014 - 18:16
fonte

4 risposte

5

Punti multipli:

Durante la lettura di DDD, mi sono ricordato una cosa: se vuoi, evita le relazioni a doppia faccia. Dovresti solo definire una direzione e avere l'altra direzione accessibile attraverso la query nel repository. Nel tuo caso, potresti avere Organization come attributo di Person , ma non avere l'elenco di Persona come parte di Organization . E fallo in modo che il repository contenga il metodo GetPeopleOfOrganization per richiedere tali informazioni.

La prossima cosa da notare è che spesso è accettabile avere i dati in uno stato non valido per un breve periodo di tempo. Il punto è che il sistema non ti permette di mantenere questo stato e lo stato non valido non è visibile a nessun'altra parte del sistema. Questo è il caso delle transazioni. È possibile avere uno stato non valido durante la transazione, ma la transazione avrà esito negativo quando si tenta di eseguire il commit con lo stato non valido. E il sistema non consente a transazioni diverse di vedere cosa succede all'interno di altre transazioni.

E in relazione a sopra: l'entità non fa realmente parte del modello finché non viene aggiunta all'elenco / repository di tutte le entità. Va bene cercare di avere i dati validi prima, ma potrebbe causare problemi, che potrebbero facilmente essere risolti quando il controllo dell'integrità viene eseguito nel repository quando i dati vengono salvati.

E l'ultima cosa. Sono d'accordo che essere in grado di applicare invarianti e integrità con un codice e durante la compilazione è grandioso. Ma C # semplicemente non ha le strutture di tipo e lingua per consentirlo. Non è un peccato affidarsi al controllo ed alle eccezioni in fase di esecuzione. Finché assicurati che il codice si blocchi durante lo sviluppo, anziché la produzione, tutto dovrebbe andare bene.

    
risposta data 12.06.2014 - 19:31
fonte
5

Sembra che questo sarebbe più facile da fare se ci fosse una terza entità: appartenenza. Le tue tre regole sono applicate:

  1. Gestione persone - deve avere un'iscrizione (che richiede un'organizzazione)
  2. Organizzazione: ottiene la lista delle persone dall'appartenenza, quindi le conosce.
  3. Membership - mantiene una Persona unica e impedisce alla stessa persona di entrare in un'altra Organizzazione.

Come parte del nuovo processo di adesione, puoi passare la Persona all'Organizzazione per vedere se soddisfa tutti i criteri e viceversa. Puoi imporre il consenso reciproco all'iscrizione o meno.

Mi rendo conto che nel mondo reale l'appartenenza è un concetto astratto, ma esiste in un modo. In genere è un accordo reciproco che lega insieme queste due entità. Un'organizzazione può rendere una persona un membro senza l'approvazione, la cooperazione, ecc. Della persona? Lo spam è una forma di appartenenza non reciproca all'elenco di un'organizzazione. In questo caso non è pratico applicare regole in entrambe le direzioni: sei un socio, paghi le quote e ho pagato i miei debiti, quindi fornisci i servizi forniti con la mia iscrizione.

    
risposta data 11.06.2014 - 20:42
fonte
2

Rendi "interno" il costruttore Person oppure rendi l'elenco _people accessibile da una proprietà interna, quindi il costruttore Person può aggiungere this a _people subito dopo la creazione.

Naturalmente, questa soluzione non è perfetta in quanto consente comunque ad altre classi appartenenti allo stesso assembly di dominio di utilizzare erroneamente i propri oggetti, ma almeno vieterà qualsiasi utilizzo errato dall'esterno (che è in genere sufficiente per la maggior parte del mondo reale casi).

Se stai pensando ad un modello più limitato, dove solo la classe Person può accedere ad un certo metodo di Organization , o viceversa: i progettisti C # non hanno implementato una cosa del genere, AFAIK intenzionalmente. Vedi, per esempio, questo post SO e la risposta più alta .

    
risposta data 11.06.2014 - 18:37
fonte
1

Puoi garantire che l'associazione bidirezionale tra gli oggetti Persona e Organizzazione non possa essere interrotta per codice al di fuori delle classi stesse utilizzando gli eventi:

public class PersonCreatedEventArgs : EventArgs
{
    public readonly Person Person;
    public readonly Organization Organization;

    public PersonCreatedEventArgs(Person person, Organization organization)
    {
        this.Organization = organization;
        this.Person = person;
    }
}

public class Person
{
    public static event EventHandler<PersonCreatedEventArgs> PersonAdded;
    public static event EventHandler<PersonCreatedEventArgs> PersonRemoved;

    private Organization organization;
    private string name;

    public string Name { get { return name; } private set { name = value; }}

    public Organization Organization { 
        get { 
            return organization;
        }
        set {
            if (organization == value)
                return;
            if (organization != null && PersonRemoved != null)
                PersonRemoved(this, new PersonCreatedEventArgs(this, organization));
            organization = value;
            if (organization != null && PersonAdded != null)
                PersonAdded(this, new PersonCreatedEventArgs(this, organization));
        }
    }

    public Person(Organization organization, string name)
    {
        if (organization == null)
        {
            throw new ArgumentNullException("organization");
        }

        Name = name;
        Organization = organization;
    }
}

public class Organization
{
    static Organization()
    {
        Person.PersonAdded += Person_PersonAdded;
        Person.PersonRemoved += Person_PersonRemoved;
    }

    static void Person_PersonRemoved(object sender, PersonCreatedEventArgs e)
    {
        e.Organization._people.Remove(e.Person);
    }

    static void Person_PersonAdded(object sender, PersonCreatedEventArgs e)
    {
        e.Organization._people.Add(e.Person);
    }

    private readonly List<Person> _people = new List<Person>();

    public IEnumerable<Person> People
    {
        get { return _people.AsReadOnly(); }
    }
}

In questo schema, l'impostazione dell'organizzazione di una persona attiva gli eventi che notificano l'organizzazione dell'associazione.

Poiché gli eventi "PersonRemoved" e "PersonAdded" possono essere solo attivato dalla classe Person stessa , una persona può essere costruita solo con una determinata organizzazione e la lista "Persone" viene restituita in sola lettura, a tutti i codici al di fuori delle due classi viene impedito di danneggiare l'associazione bidirezionale . In pratica ti consigliamo di aggiungere alcuni eventi negli eventi per assicurarti che i dati rimangano coerenti (nel caso qualcuno faccia qualcosa di sgradevole con la riflessione, o in caso di errori di serializzazione, o ecc.)

    
risposta data 18.06.2014 - 10:45
fonte