Accesso ai repository dal dominio

12

Supponiamo di avere un sistema di registrazione delle attività, quando viene registrata un'attività, l'utente specifica una categoria e l'attività è impostata su "Eccezionale". Supponiamo in questa istanza che Categoria e Stato debbano essere implementati come entità. Normalmente lo farei:

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 così perché mi viene costantemente detto che le entità non dovrebbero accedere ai repository, ma sarebbe molto più sensato per me se facessi questo:

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 status è comunque iniettato in dipendenza, quindi non c'è una reale dipendenza, e questo mi sembra più che il dominio che sta facendo la decisione che un task sia in sospeso. La versione precedente sembra essere il lay-out dell'applicazione che prende questa decisione. Qual è il motivo per cui i contratti di repository 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 che tu voglia passare in tutte le possibili versioni di Urgency e in nessun modo vorresti calcolare questa logica di business nel livello applicazione, quindi sicuramente questo sarebbe il modo più appropriato?

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

EDIT: potrebbe essere il caso anche per i 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;
    }
}
    
posta Paul T Davies 24.09.2012 - 17:20
fonte

4 risposte

6

Stai mescolando

entities should not access the repositories

(che è un buon suggerimento)

e

the domain layer should not access the repositories

(che potrebbe essere un cattivo suggerimento finché i repository fanno parte del livello del dominio, non del livello dell'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 factory separate (come parte del livello di dominio!) e inserire la logica di creazione lì.

EDIT: al tuo esempio Update : dato che _urgencyRepository e statusRepository sono membri della classe Task , definita come una sorta di interfaccia, ora devi inserirli in qualsiasi entità Task prima ora puoi usare Update (ad esempio nel costruttore di Task). Oppure li definisci come membri statici, ma fai attenzione, che potrebbe facilmente causare problemi di threading multiplo, o solo problemi quando hai bisogno di repository diversi per entità Task diverse allo stesso tempo.

Questo design rende un po 'più difficile creare Task di entità isolate, quindi è più difficile scrivere test unitari per Task entità, più difficile scrivere test automatici a seconda delle entità Task e si produce un po' più di memoria sovraccarico, dal momento che ogni entità Task ora deve contenere due riferimenti ai repository. Certo, potrebbe essere tollerabile nel tuo caso. D'altra parte, creando una classe di utilità separata TaskUpdater che mantiene i riferimenti ai repository giusti può essere spesso o almeno a volte una soluzione migliore.

La parte importante è: TaskUpdater farà ancora parte del livello dominio! Solo perché hai inserito l'aggiornamento o il codice di creazione in una classe separata non significa che devi passare a un altro livello.

    
risposta data 24.09.2012 - 18:01
fonte
5

Non so se il tuo esempio di stato è codice reale o qui solo per motivi di dimostrazione, ma mi sembra strano che tu debba implementare lo stato come entità (per non parlare di una radice aggregata) quando il suo ID è una costante definita nel codice: Constants.Status.OutstandingId . Non è questo che sconfigge lo scopo degli stati "dinamici" puoi aggiungerne quanti ne vuoi nel database?

Aggiungo che nel tuo caso, la costruzione di un Task (compreso il lavoro di ottenere lo stato giusto da StatusRepository se necessario) potrebbe meritare un TaskFactory invece di rimanere nella Task stessa, poiché è un assemblaggio non banale di oggetti.

Ma:

I am consistently told that entities should not access the repositories

Questa affermazione è imprecisa e alquanto esagerata, nel migliore dei casi fuorviante e pericolosa.

È comunemente accettato nelle architetture basate su domini che un'entità non dovrebbe sapere come memorizzare se stesso - questo è il principio dell'ignoranza della persistenza. Quindi nessuna chiamata al suo repository per aggiungersi al repository. Dovrebbe sapere come (e quando) per memorizzare altre entità ? Ancora una volta, questa responsabilità sembra appartenere a un altro oggetto - forse un oggetto che è a conoscenza del contesto di esecuzione e del progresso generale del caso d'uso corrente, come un servizio di livello applicazione.

L'entità potrebbe utilizzare un repository per recuperare un'altra entità ? Il 90% delle volte non dovrebbe farlo, dal momento che le entità di cui ha bisogno sono solitamente nell'ambito del suo aggregato o ottenibili per attraversamento di altri oggetti. Ma ci sono momenti in cui non lo sono. Se si prende una struttura gerarchica, ad esempio, le entità spesso hanno bisogno di accedere a tutti i loro antenati, a un particolare nipote, ecc. Come parte del loro comportamento intrinseco. Non hanno un riferimento diretto a questi lontani parenti. Sarebbe scomodo passare questi parenti intorno a loro come parametri dell'operazione. Quindi, perché non utilizzare un repository per ottenerli, purché si tratti di radici aggregate?

Ci sono alcuni altri esempi. Il fatto è che a volte esiste un comportamento che non è possibile inserire in un servizio di dominio poiché sembra adattarsi perfettamente a un'entità esistente. Eppure, questa entità ha bisogno di accedere a un repository per idratare una root o una collezione di root che non possono essere passati ad essa.

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

    
risposta data 24.09.2012 - 22:12
fonte
2

Questo è uno dei motivi per cui non utilizzo Enum o tabelle di ricerca pure all'interno del mio dominio. Urgenza e stato sono entrambi gli stati e vi è una logica associata a uno stato che appartiene direttamente allo stato (ad esempio, quali stati posso passare al dato stato attuale). Inoltre, registrando uno stato come un valore puro si perdono informazioni come la durata dell'attività in un determinato stato. Io rappresento gli status come una gerarchia di classi come questa. (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 molte 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 i proxy derivati dagli oggetti del tuo dominio per fare la loro magia).

  2. Molti dei 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 () (questa è parte del Domain Driven Design, che fornisce operazioni significative piuttosto che oggetti di dominio anemici.

  3. Non lo mostro qui ma l'operazione nasconderà anche i dettagli di come memorizza il suo stato corrente. Di solito ho un elenco protetto di HistoricalState che autorizzo il pubblico a interrogare se è interessato. 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 appena un enume, quella logica deve mentire altrove.

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

    
risposta data 25.09.2012 - 21:31
fonte
1

Ho cercato di risolvere lo stesso problema per qualche volta, ho deciso di voler chiamare Task.UpdateTask () in questo 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 trovato questo ... avere la mia torta e mangiarlo anch'io. L'idea è che le azioni abbiano luogo sull'entità ma senza l'iniezione di tutte le dipendenze. Invece il lavoro è fatto in metodi statici in modo che possano accedere allo stato dell'entità. La fabbrica mette tutto insieme e normalmente avrà tutto ciò di cui ha bisogno per fare il lavoro che l'entità deve fare. Il codice cliente 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;
        }

    }
}
    
risposta data 21.07.2013 - 21:30
fonte