Accesso ai repository dal dominio


14

Supponiamo di avere un sistema di registrazione delle attività, quando un'attività viene registrata, l'utente specifica una categoria e l'attività passa automaticamente allo stato "Eccezionale". Supponiamo in questo caso che Categoria e Stato debbano essere implementati come entità. Normalmente farei questo:

Livello applicazione:

public class TaskService
{
    //...

    public void Add(Guid categoryId, string description)
    {
        var category = _categoryRepository.GetById(categoryId);
        var status = _statusRepository.GetById(Constants.Status.OutstandingId);
        var task = Task.Create(category, status, description);
        _taskRepository.Save(task);
    }
}

Entità:

public class Task
{
    //...

    public static void Create(Category category, Status status, string description)
    {
        return new Task
        {
            Category = category,
            Status = status,
            Description = descrtiption
        };
    }
}

Lo faccio in questo modo perché mi viene costantemente detto che le entità non dovrebbero accedere ai repository, ma avrebbe molto più senso per me se lo facessi:

Entità:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        return new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };
    }
}

Il repository di stato viene comunque iniettato dipendenza, quindi non esiste una reale dipendenza, e questo mi sembra più simile al dominio che sta prendendo la decisione che un'attività viene impostata automaticamente come eccezionale. La versione precedente sembra essere il livello dell'applicazione che prende quella decisione. Qual è il motivo per cui i contratti di deposito sono spesso nel dominio se questo non dovrebbe essere una possibilità?

Ecco un esempio più estremo, qui il dominio decide l'urgenza:

Entità:

public class Task
{
    //...

    public static void Create(Category category, string description)
    {
        var task = new Task
        {
            Category = category,
            Status = _statusRepository.GetById(Constants.Status.OutstandingId),
            Description = descrtiption
        };

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                task.Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            task.Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}

Non c'è modo in cui vorresti passare in tutte le possibili versioni di Urgency e in nessun modo vorresti calcolare questa logica di business nel livello dell'applicazione, quindi sicuramente questo sarebbe il modo più appropriato?

Quindi questo è un motivo valido per accedere ai repository dal dominio?

EDIT: questo potrebbe anche essere il caso di metodi non statici:

public class Task
{
    //...

    public void Update(Category category, string description)
    {
        Category = category,
        Status = _statusRepository.GetById(Constants.Status.OutstandingId),
        Description = descrtiption

        if(someCondition)
        {
            if(someValue > anotherValue)
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.UrgentId);
            }
            else
            {
                Urgency = _urgencyRepository.GetById
                    (Constants.Urgency.SemiUrgentId);
            }
        }
        else
        {
            Urgency = _urgencyRepository.GetById
                (Constants.Urgency.NotId);
        }

        return task;
    }
}

Risposte:


8

Stai mescolando

le entità non dovrebbero accedere ai repository

(che è un buon suggerimento)

e

il livello del dominio non dovrebbe accedere ai repository

(che potrebbe essere un suggerimento negativo purché i repository facciano parte del livello dominio, non del livello applicazione). In realtà, i tuoi esempi non mostrano casi in cui un'entità accede a un repository, poiché stai utilizzando metodi statici che non appartengono a nessuna entità.

Se non vuoi mettere quella logica di creazione in un metodo statico della classe di entità, puoi introdurre classi di fabbrica separate (come parte del livello di dominio!) E inserire lì la logica di creazione.

EDIT: al tuo Updateesempio: dato che _urgencyRepositorye statusRepository sono membri della classe Task, definita come una sorta di interfaccia, ora devi iniettarli in qualsiasi Taskentità prima di poter usareUpdate ora (ad esempio nel costruttore Task). Oppure li definisci come membri statici, ma fai attenzione, che potrebbero facilmente causare problemi di multi threading o solo problemi quando hai bisogno di repository diversi per entità Task diverse allo stesso tempo.

Questo design rende un po 'più difficile la creazione di Taskentità in isolamento, quindi è più difficile scrivere test unitari per Taskentità, più difficile scrivere test automatici in base alle entità Task e si produce un po' più di memoria overhead, poiché ogni entità Task ora deve ritengono che due riferimenti ai repository. Naturalmente, questo potrebbe essere tollerabile nel tuo caso. D'altra parte, la creazione di una classe di utilità separata TaskUpdaterche mantiene i riferimenti ai repository giusti può essere spesso o almeno qualche volta una soluzione migliore.

La parte importante è: TaskUpdatersarà ancora parte del livello del dominio! Solo perché inserisci quel codice di aggiornamento o creazione in una classe separata non significa che devi passare a un altro livello.


Ho modificato per mostrare che questo vale sia per i metodi non statici che per quelli statici. Non ho mai pensato davvero che il metodo factory non facesse parte di un'entità.
Paul T Davies,

@PaulTDavies: vedi la mia modifica
Doc Brown,

Sono d'accordo con quello che stai dicendo qui, ma aggiungerei un pezzo conciso che disegna il punto che Status = _statusRepository.GetById(Constants.Status.OutstandingId)è una regola aziendale , che potresti leggere come "L'attività determina lo stato iniziale di tutte le attività sarà eccezionale" ed è per questo quella riga di codice non appartiene a un repository, le cui uniche preoccupazioni sono la gestione dei dati tramite operazioni CRUD.
Jimmy Hoffa,

@JimmyHoffa: hm, nessuno qui stava suggerendo di mettere quel tipo di linea in una delle classi di repository, né l'OP né me - quindi qual è il tuo punto?
Doc Brown,

Mi piace molto l'idea di TaskUpdater come servizio domian. Sembra in qualche modo un po 'di confusione solo per mantenere i principi DDD, ma significa che posso evitare di iniettare il repository ogni volta che utilizzo Task.
Paul T Davies,

6

Non so se il tuo esempio di stato sia un codice reale o qui solo a scopo dimostrativo, ma mi sembra strano che tu debba implementare Status come Entità (per non parlare di una radice aggregata) quando il suo ID è una costante definita nel codice - Constants.Status.OutstandingId. Questo non vanifica lo scopo degli stati "dinamici" che puoi aggiungere quanti ne vuoi nel database?

Aggiungo che nel tuo caso, la costruzione di un Task(incluso il lavoro per ottenere lo status giusto da StatusRepository se necessario) potrebbe meritare un TaskFactorypiuttosto che rimanere in Tasksé, poiché è un assemblaggio non banale di oggetti.

Ma :

Mi viene costantemente detto che le entità non dovrebbero accedere ai repository

Questa affermazione è imprecisa e semplicistica nella migliore delle ipotesi, fuorviante e pericolosa nella peggiore delle ipotesi.

È abbastanza comunemente accettato nelle architetture guidate dal dominio che un'entità non dovrebbe sapere come immagazzinarsi - questo è il principio dell'ignoranza della persistenza. Quindi nessuna chiamata al suo repository per aggiungersi al repository. Dovrebbe sapere come (e quando) archiviare altre entità ? Ancora una volta, tale responsabilità sembra appartenere a un altro oggetto, forse un oggetto consapevole del contesto di esecuzione e dell'avanzamento complessivo del caso d'uso corrente, come un servizio a livello di applicazione.

Un'entità potrebbe usare un repository per recuperare un'altra entità ? Il 90% delle volte non dovrebbe essere necessario, poiché le entità di cui ha bisogno sono di solito nell'ambito del suo aggregato o ottenibili attraverso l'attraversamento di altri oggetti. Ma ci sono momenti in cui non lo sono. Se prendi una struttura gerarchica, ad esempio, le entità hanno spesso bisogno di accedere a tutti i loro antenati, un nipote particolare, ecc. Come parte del loro comportamento intrinseco. Non hanno un riferimento diretto a questi parenti remoti. Sarebbe scomodo passare questi parenti intorno a loro come parametri dell'operazione. Quindi perché non usare un repository per ottenerli, a condizione che siano radici aggregate?

Ci sono alcuni altri esempi. Il fatto è che a volte c'è un comportamento che non è possibile inserire in un servizio di dominio poiché sembra adattarsi perfettamente a un'entità esistente. Eppure, questa entità deve accedere a un repository per idratare una radice o una raccolta di radici che non possono essere passate ad essa.

Quindi l'accesso a un repository da un'entità non è male in sé , può assumere diverse forme che derivano da una varietà di decisioni di progettazione che vanno da catastrofiche a accettabili.


Non sono d'accordo sul fatto che un'entità dovrebbe usare un repository per accedere a un'entità con cui ha già una relazione - dovresti essere in grado di attraversare il grafico dell'oggetto per accedere a quell'entità. L'uso del repository in questo modo è un no assoluto. Ciò di cui sto discutendo qui è che l'entità non ha ancora un riferimento, ma deve crearne uno in determinate condizioni aziendali.
Paul T Davies,

Bene, se mi hai letto bene, siamo totalmente d'accordo su questo ...
guillaume31

2

Questo è uno dei motivi per cui non uso Enum o tabelle di ricerca pure nel mio dominio. L'urgenza e lo stato sono entrambi Stati e c'è una logica associata a uno stato che appartiene direttamente allo stato (ad esempio, a quali stati posso passare al mio stato attuale). Inoltre, registrando uno stato come valore puro si perdono informazioni come per quanto tempo l'attività è stata in un determinato stato. Rappresento gli stati come una gerarchia di classi in questo modo. (In C #)

public class Interval
{
  public Interval(DateTime start, DateTime? end)
  {
    Start=start;
    End=end;
  }

  //To be called by internal framework
  protected Interval()
  {
  }

  public void End(DateTime? when=null)
  {
    if(when==null)
      when=DateTime.Now;
    End=when;
  }

  public DateTime Start{get;protected set;}

  public DateTime? End{get; protected set;}
}

public class TaskStatus
{
  protected TaskStatus()
  {
  }
  public Long Id {get;protected set;}

  public string Name {get; protected set;}

  public string Description {get; protected set;}

  public Interval Duration {get; protected set;}

  public virtual TNewStatus TransitionTo<TNewStatus>()
    where TNewStatus:TaskStatus
  {
    throw new NotImplementedException();
  }
}

public class OutStandingTaskStatus:TaskStatus
{
  protected OutStandingTaskStatus()
  {
  }

  public OutStandingTaskStatus(bool initialize)
  {
    Name="Oustanding";
    Description="For tasks that need to be addressed";
    Duration=new Interval(DateTime.Now,null);
  }

  public override TNewStatus TransitionTo<TNewStatus>()
  {
    if(typeof(TNewStatus)==typeof(CompletedTaskStatus))
    {
      var transitionDate=DateTime.Now();
      Duration.End(transitionDate);
      return new CompletedTaskStatus(true);
    }
    return base.TransitionTo<TNewStatus>();
  }
}

L'implementazione di CompletedTaskStatus sarebbe praticamente la stessa.

Ci sono diverse cose da notare qui:

  1. Rendo i costruttori predefiniti protetti. Questo è così che il framework può chiamarlo quando estrae un oggetto dalla persistenza (sia EntityFramework Code-first che NHibernate usano proxy derivati ​​dagli oggetti del tuo dominio per fare la loro magia).

  2. Molti setter di proprietà sono protetti per lo stesso motivo. Se voglio cambiare la data di fine di un intervallo, devo chiamare la funzione Interval.End () (fa parte di Domain Driven Design, fornendo operazioni significative anziché Anemic Domain Objects.

  3. Non lo mostro qui, ma l'attività nasconderebbe anche i dettagli di come memorizza il suo stato attuale. Di solito ho un elenco protetto di HistoricalStates che consento al pubblico di interrogare se sono interessati. Altrimenti espongo lo stato corrente come getter che interroga HistoricalStates.Single (state.Duration.End == null).

  4. La funzione TransitionTo è significativa perché può contenere la logica su quali stati sono validi per la transizione. Se hai solo un enum, quella logica deve trovarsi altrove.

Speriamo che questo ti aiuti a capire un po 'meglio l'approccio DDD.


1
Questo sarebbe sicuramente l'approccio corretto se i diversi stati hanno comportamenti diversi come nell'esempio del tuo modello di stato, e risolve sicuramente anche il problema discusso. Tuttavia, troverei difficile giustificare una classe per ogni stato se avessero solo valori diversi, non comportamenti diversi.
Paul T Davies,

1

Ho provato a risolvere lo stesso problema per qualche tempo, ho deciso di voler chiamare Task.UpdateTask () in quel modo, anche se preferirei che fosse specifico del dominio, nel tuo caso forse lo chiamerei Task.ChangeCategory (...) per indicare un'azione e non solo CRUD.

comunque, ho provato il tuo problema e ho pensato a questo ... prendi la mia torta e mangiala anche io. L'idea è che le azioni si svolgono sull'entità ma senza iniezione di tutte le dipendenze. Invece il lavoro viene eseguito con metodi statici in modo che possano accedere allo stato dell'entità. La fabbrica mette tutto insieme e normalmente avrà tutto il necessario per fare il lavoro che l'entità deve fare. Il codice client ora appare pulito e chiaro e la tua entità non dipende da alcuna iniezione di repository.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace UnitTestProject2
{
    public class ClientCode
    {
        public void Main()
        {
            TaskFactory factory = new TaskFactory();
            Task task = factory.Create();
            task.UpdateTask(new Category(), "some value");
        }

    }
    public class Category
    {
    }

    public class Task
    {
        public Action<Category, String> UpdateTask { get; set; }

        public static void UpdateTaskAction(Task task, Category category, string description)
        {
            // do the logic here, static can access private if needed
        }
    }

    public class TaskFactory
    {      
        public Task Create()
        {
            Task task = new Task();
            task.UpdateTask = (category, description) =>
                {
                    Task.UpdateTaskAction(task, category, description);
                };

            return task;
        }

    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.