Zugriff auf Repositorys über die Domäne

14

Angenommen, wir haben ein Aufgabenprotokollierungssystem. Wenn eine Aufgabe protokolliert wird, gibt der Benutzer eine Kategorie an und die Aufgabe hat standardmäßig den Status "Ausstehend". Angenommen, in diesem Fall müssen Kategorie und Status als Entitäten implementiert werden. Normalerweise würde ich das machen:

Anwendungsschicht:

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ät:

public class Task
{
    //...

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

Ich mache das so, weil mir durchweg gesagt wird, dass Entities nicht auf die Repositories zugreifen sollen, aber es würde für mich viel sinnvoller sein, wenn ich das mache:

Entität:

public class Task
{
    //...

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

Das Status-Repository ist ohnehin von Abhängigkeiten abhängig, es besteht also keine wirkliche Abhängigkeit, und dies erscheint mir eher so, als ob es die Domäne ist, die die Entscheidung trifft, für die eine Aufgabe standardmäßig ausstehend ist. Die vorherige Version scheint der Antragsteller zu sein, der diese Entscheidung trifft. Warum sind Repository-Verträge oft in der Domain, wenn dies keine Möglichkeit sein sollte?

Hier ist ein extremeres Beispiel, hier entscheidet die Domain über die Dringlichkeit:

Entität:

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

Es gibt keine Möglichkeit, alle möglichen Versionen von Dringlichkeit zu übergeben, und es gibt keine Möglichkeit, diese Geschäftslogik in der Anwendungsebene zu berechnen. Dies ist also mit Sicherheit die am besten geeignete Methode.

Ist dies also ein triftiger Grund für den Zugriff auf Repositorys von der Domäne aus?

EDIT: Dies könnte auch bei nicht statischen Methoden der Fall sein:

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;
    }
}
Paul T Davies
quelle

Antworten:

8

Sie mischen

Entitäten sollten nicht auf die Repositorys zugreifen

(das ist ein guter Vorschlag)

und

Die Domänenschicht sollte nicht auf die Repositorys zugreifen

(Dies kann ein schlechter Vorschlag sein, solange Ihre Repositorys Teil der Domänenschicht und nicht der Anwendungsschicht sind.) Tatsächlich zeigen Ihre Beispiele keinen Fall, in dem eine Entität auf ein Repository zugreift, da Sie statische Methoden verwenden, die keiner Entität angehören.

Wenn Sie diese Erstellungslogik nicht in eine statische Methode der Entitätsklasse einfügen möchten, können Sie separate Factory-Klassen (als Teil der Domänenschicht!) Einführen und die Erstellungslogik dort einfügen.

EDIT: zu Ihrem UpdateBeispiel: Da _urgencyRepositoryund statusRepository sind Mitglieder der Klasse Task, als eine Art Schnittstelle definiert, müssen Sie sie jetzt in jede einzuspritzen TaskEinheit , bevor Sie können Updatenun (zum Beispiel im Konstruktor - Task). Oder Sie definieren sie als statische Member, aber Vorsicht, dies kann leicht zu Multithreading-Problemen führen, oder nur zu Problemen, wenn Sie verschiedene Repositorys für verschiedene Task-Entitäten gleichzeitig benötigen.

Durch diesen Entwurf ist es etwas schwieriger, TaskEntitäten isoliert zu erstellen , daher ist es schwieriger, Komponententests für TaskEntitäten zu schreiben, und es ist schwieriger, automatische Tests in Abhängigkeit von Task-Entitäten zu schreiben. Außerdem wird ein wenig mehr Arbeitsspeicher benötigt, da es jetzt für jede Task-Entität erforderlich ist halte das für zwei Verweise auf die Reposities. Natürlich kann das in Ihrem Fall erträglich sein. Andererseits kann die Erstellung einer separaten Utility-Klasse TaskUpdater, die die Verweise auf die richtigen Repositorys beibehält, häufig oder zumindest manchmal eine bessere Lösung sein.

Der wichtige Teil ist: TaskUpdaterWird immer noch Teil der Domain-Schicht sein! Nur weil Sie diesen Aktualisierungs- oder Erstellungscode in einer separaten Klasse ablegen, müssen Sie nicht auf eine andere Ebene wechseln.

Doc Brown
quelle
Ich habe bearbeitet, um zu zeigen, dass dies sowohl für nicht statische als auch für statische Methoden gilt. Ich hätte nie gedacht, dass die Fabrikmethode nicht Teil einer Einheit ist.
Paul T Davies
@ PaulTDavies: siehe meine Bearbeitung
Doc Brown
Ich stimme dem zu, was Sie hier sagen, aber ich möchte ein kurzes Stück hinzufügen, das den Punkt Status = _statusRepository.GetById(Constants.Status.OutstandingId)einer Geschäftsregel aufzeigt , die Sie als "Das Unternehmen schreibt den Anfangsstatus aller Aufgaben als herausragend vor" bezeichnen können, und aus diesem Grund Diese Codezeile gehört nicht in ein Repository, dessen einziges Anliegen die Datenverwaltung über CRUD-Operationen ist.
Jimmy Hoffa
@JimmyHoffa: hm, niemand hier hat vorgeschlagen, diese Art von Zeile in eine der Repository-Klassen einzufügen, weder in das OP noch in mich.
Doc Brown
Mir gefällt die Idee des TaskUpdater als Domian-Dienst sehr gut. Es scheint irgendwie ein bisschen kompliziert zu sein, nur die DDD-Prinzipien beizubehalten, aber es bedeutet, dass ich vermeiden kann, das Repository jedes Mal zu injizieren, wenn ich Task benutze.
Paul T Davies
6

Ich weiß nicht, ob es sich bei Ihrem Statusbeispiel um echten Code handelt oder ob es sich hier nur um Demonstrationszwecke handelt, aber es erscheint mir merkwürdig, dass Sie Status als Entität implementieren sollten (ganz zu schweigen von einem aggregierten Stamm), wenn dessen ID eine definierte Konstante ist im Code - Constants.Status.OutstandingId. Hält das nicht den Zweck von "dynamischen" Status außer Kraft, von denen Sie so viele in die Datenbank aufnehmen können, wie Sie möchten?

Ich würde hinzufügen, dass in Ihrem Fall der Aufbau eines TaskObjekts (einschließlich der Aufgabe, den richtigen Status aus dem StatusRepository zu erhalten, falls erforderlich) möglicherweise ein Objekt verdient, TaskFactoryanstatt im eigenen Objekt zu bleiben Task, da es sich um eine nicht triviale Zusammenstellung von Objekten handelt.

Aber :

Mir wird durchweg gesagt, dass Entitäten nicht auf die Repositories zugreifen sollen

Diese Aussage ist im besten Fall ungenau und zu simpel, im schlimmsten Fall irreführend und gefährlich.

In domänengetriebenen Architekturen ist es allgemein anerkannt, dass eine Entität nicht wissen sollte, wie sie sich selbst speichert - das ist das Prinzip der Persistenz-Ignoranz. Es werden also keine Aufrufe an das Repository gesendet, um sich dem Repository hinzuzufügen. Sollte es wissen, wie (und wann) andere Entitäten zu speichern sind ? Auch hier scheint diese Verantwortung in ein anderes Objekt zu gehören - vielleicht ein Objekt, das den Ausführungskontext und den Gesamtfortschritt des aktuellen Anwendungsfalls kennt, wie ein Application-Layer-Service.

Könnte eine Entität ein Repository verwenden, um eine andere Entität abzurufen ? In 90% der Fälle sollte dies nicht erforderlich sein, da die benötigten Entitäten normalerweise im Bereich ihrer Gesamtheit liegen oder durch Durchqueren anderer Objekte verfügbar sind. Aber es gibt Zeiten, in denen dies nicht der Fall ist. Wenn Sie beispielsweise eine hierarchische Struktur verwenden, müssen Entitäten im Rahmen ihres intrinsischen Verhaltens häufig auf alle ihre Vorfahren, ein bestimmtes Enkelkind usw. zugreifen. Sie haben keinen direkten Bezug zu diesen entfernten Verwandten. Es wäre unpraktisch, diese Verwandten als Parameter der Operation an sie weiterzugeben. Warum also nicht ein Repository verwenden, um sie abzurufen - vorausgesetzt, es handelt sich um aggregierte Wurzeln?

Es gibt noch einige andere Beispiele. Die Sache ist, manchmal gibt es ein Verhalten, das Sie nicht in einen Domain-Service einfügen können, da es perfekt in eine vorhandene Entität zu passen scheint. Dennoch muss diese Entität auf ein Repository zugreifen, um eine Wurzel oder eine Sammlung von Wurzeln zu hydratisieren, die nicht an sie übergeben werden können.

Ein Repository von einer Entität so den Zugriff auf nicht schlecht an sich , kann es aus einer anderen Formen dieses Ergebnis nehmen Vielzahl von Design - Entscheidungen von katastrophalen zu akzeptablen Bereich.

guillaume31
quelle
Ich bin nicht einverstanden, dass eine Entität ein Repository verwenden sollte, um auf eine Entität zuzugreifen, zu der sie bereits eine Beziehung hat. Sie sollten in der Lage sein, den Objektgraphen zu durchlaufen, um auf diese Entität zuzugreifen. Die Verwendung des Repositorys auf diese Weise ist ein absolutes Nein Nein. Ich spreche hier von Entitäten, auf die die Entität noch keinen Verweis hat, die jedoch unter bestimmten Geschäftsbedingungen erstellt werden müssen.
Paul T Davies
Wenn Sie mich gut gelesen haben, stimmen wir dem voll und ganz zu ...
guillaume31
2

Dies ist einer der Gründe, warum ich in meiner Domain keine Enums oder reinen Nachschlagetabellen verwende. Dringlichkeit und Status sind beide Zustände, und es gibt eine Logik, die mit einem Zustand verknüpft ist, der direkt zu dem Zustand gehört (z. B. welche Zustände kann ich angesichts meines aktuellen Zustands wechseln). Wenn Sie einen Zustand als reinen Wert aufzeichnen, verlieren Sie auch Informationen darüber, wie lange sich die Aufgabe in einem bestimmten Zustand befand. Ich repräsentiere Status als Klassenhierarchie. (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>();
  }
}

Die Implementierung von CompletedTaskStatus wäre ziemlich gleich.

Hier sind einige Dinge zu beachten:

  1. Ich mache die Standardkonstruktoren geschützt. Auf diese Weise kann das Framework es aufrufen, wenn ein Objekt aus der Persistenz gezogen wird (sowohl EntityFramework Code-first als auch NHibernate verwenden Proxys, die von Ihren Domänenobjekten abgeleitet sind, um ihre Magie auszuführen).

  2. Viele der Immobilienmakler sind aus dem gleichen Grund geschützt. Wenn ich das Enddatum eines Intervalls ändern möchte, muss ich die Funktion Interval.End () aufrufen (dies ist Teil von Domain Driven Design und bietet sinnvolle Vorgänge anstelle von anämischen Domänenobjekten.

  3. Ich zeige es hier nicht an, aber der Task würde ebenfalls die Details darüber verbergen, wie er seinen aktuellen Status speichert. Normalerweise habe ich eine geschützte Liste von HistoricalStates, die die Öffentlichkeit abfragen kann, wenn sie interessiert sind. Andernfalls mache ich den aktuellen Status als Getter verfügbar, der HistoricalStates.Single (state.Duration.End == null) abfragt.

  4. Die TransitionTo-Funktion ist von Bedeutung, da sie Logik darüber enthalten kann, welche Zustände für den Übergang gültig sind. Wenn Sie nur eine Aufzählung haben, muss diese Logik woanders liegen.

Hoffentlich hilft dies Ihnen dabei, den DDD-Ansatz ein wenig besser zu verstehen.

Michael Brown
quelle
1
Dies wäre mit Sicherheit der richtige Ansatz, wenn die verschiedenen Zustände sich anders verhalten als in Ihrem Zustandsmusterbeispiel, und es löst mit Sicherheit auch das diskutierte Problem. Ich würde es jedoch schwierig finden, eine Klasse für jeden Zustand zu rechtfertigen, wenn sie nur unterschiedliche Werte und kein unterschiedliches Verhalten hätten.
Paul T Davies
1

Ich habe versucht, das gleiche Problem für einige Zeit zu lösen, entschied ich, dass ich Task.UpdateTask () so aufrufen kann, obwohl ich es lieber domänenspezifisch wäre, in Ihrem Fall würde ich es Task.ChangeCategory nennen (...) um auf eine Aktion hinzuweisen und nicht nur auf CRUD.

Wie auch immer, ich habe dein Problem ausprobiert und mir das ausgedacht ... nimm meinen Kuchen und iss ihn auch. Die Idee ist, dass Aktionen auf der Entität stattfinden, ohne jedoch alle Abhängigkeiten einzuschleusen. Stattdessen wird mit statischen Methoden gearbeitet, damit sie auf den Status der Entität zugreifen können. Die Fabrik fasst alles zusammen und hat normalerweise alles, was sie braucht, um die Arbeit zu erledigen, die das Unternehmen tun muss. Der Client-Code sieht jetzt sauber und klar aus, und Ihre Entität ist von keiner Repository-Injection abhängig.

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

    }
}
Mike
quelle