Accès aux référentiels à partir du domaine

14

Supposons que nous ayons un système de journalisation des tâches, lorsqu'une tâche est journalisée, l'utilisateur spécifie une catégorie et la tâche par défaut a le statut 'En suspens'. Supposons dans ce cas que la catégorie et le statut doivent être implémentés en tant qu'entités. Normalement, je ferais ceci:

Couche d'application:

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

Je le fais comme ça, car on me dit constamment que les entités ne devraient pas accéder aux référentiels, mais il serait beaucoup plus logique pour moi de le faire:

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

Le référentiel de statuts est de toute façon injecté dans la dépendance, donc il n'y a pas de réelle dépendance, et cela me semble davantage que c'est le domaine qui prend la décision qu'une tâche par défaut est en suspens. La version précédente donne l'impression que c'est le calque d'application qui prend cette décision. Pourquoi les contrats de référentiel sont-ils souvent dans le domaine si cela ne devrait pas être possible?

Voici un exemple plus extrême, ici le domaine décide de l'urgence:

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

Il n'y a aucun moyen que vous souhaitiez passer dans toutes les versions possibles d'Urgence, et aucun moyen que vous ne souhaitiez calculer cette logique métier dans la couche application, alors ce serait sûrement le moyen le plus approprié?

Est-ce donc une raison valable pour accéder aux référentiels à partir du domaine?

EDIT: Cela pourrait également être le cas sur les méthodes non statiques:

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
la source

Réponses:

8

Vous mélangez

les entités ne doivent pas accéder aux référentiels

(ce qui est une bonne suggestion)

et

la couche de domaine ne doit pas accéder aux référentiels

(ce qui pourrait être une mauvaise suggestion tant que vos référentiels font partie de la couche domaine, pas de la couche application). En fait, vos exemples ne montrent aucun cas où une entité accède à un référentiel, car vous utilisez des méthodes statiques qui n'appartiennent à aucune entité.

Si vous ne voulez pas placer cette logique de création dans une méthode statique de la classe d'entité, vous pouvez introduire des classes d'usine distinctes (dans le cadre de la couche de domaine!) Et y placer la logique de création.

EDIT: à votre Updateexemple: étant donné que _urgencyRepositoryet statusRepository sont des membres de la classe Task, définis comme une sorte d'interface, vous devez maintenant les injecter dans n'importe quelle Taskentité avant de pouvoir les utiliser Updatemaintenant (par exemple dans le constructeur de tâches). Ou vous les définissez comme des membres statiques, mais attention, cela pourrait facilement causer des problèmes de multithread, ou simplement des problèmes lorsque vous avez besoin de différents référentiels pour différentes entités de tâche en même temps.

Cette conception rend un peu plus difficile la création d' Taskentités isolément, donc plus difficile d'écrire des tests unitaires pour les Taskentités, plus difficile d'écrire des tests automatiques en fonction des entités de tâche, et vous produisez un peu plus de surcharge de mémoire, car chaque entité de tâche doit maintenant tenir que deux références aux référentiels. Bien sûr, cela peut être tolérable dans votre cas. D'un autre côté, la création d'une classe d'utilitaires distincte TaskUpdaterqui conserve les références aux bons référentiels peut être souvent ou au moins parfois une meilleure solution.

La partie importante est: TaskUpdaterfera toujours partie de la couche domaine! Ce n'est pas parce que vous mettez cette mise à jour ou ce code de création dans une classe distincte que vous devez passer à une autre couche.

Doc Brown
la source
J'ai édité pour montrer que cela s'applique aux méthodes non statiques autant qu'aux méthodes statiques. Je n'ai jamais vraiment pensé que la méthode d'usine ne faisait pas partie d'une entité.
Paul T Davies
@PaulTDavies: voir mon montage
Doc Brown
Je suis d'accord avec ce que vous dites ici, mais j'ajouterais une pièce concise qui dessine le point qui Status = _statusRepository.GetById(Constants.Status.OutstandingId)est une règle commerciale , une règle que vous pourriez lire comme "L'entreprise dicte que le statut initial de toutes les tâches sera exceptionnel" et c'est pourquoi cette ligne de code n'appartient pas à un référentiel dont les seules préoccupations sont la gestion des données via les opérations CRUD.
Jimmy Hoffa
@JimmyHoffa: hm, personne ici ne suggérait de mettre ce genre de ligne dans l'une des classes de référentiel, ni l'OP ni moi - alors quel est votre point?
Doc Brown
J'aime bien l'idée du TaskUpdater en tant que service domian. Cela semble en quelque sorte un peu fudge juste pour conserver les principes DDD, mais cela signifie que je peux éviter d'injecter le référentiel chaque fois que j'utilise Task.
Paul T Davies du
6

Je ne sais pas si votre exemple de statut est du vrai code ou ici juste à des fins de démonstration, mais il me semble étrange que vous devez implémenter le statut en tant qu'entité (sans parler d'une racine agrégée) lorsque son ID est une constante définie dans le code - Constants.Status.OutstandingId. Cela ne va-t-il pas à l'encontre de l'objectif des statuts "dynamiques" que vous pouvez ajouter autant de fois que vous le souhaitez dans la base de données?

J'ajouterais que dans votre cas, la construction d'un Task(y compris le travail d'obtenir le bon statut de StatusRepository si besoin est) mériterait un TaskFactoryplutôt que de rester en Tasksoi, car il s'agit d'un assemblage d'objets non trivial.

Mais :

On me dit constamment que les entités ne devraient pas accéder aux référentiels

Cette déclaration est imprécise et trop simpliste au mieux, trompeuse et dangereuse au pire.

Il est assez communément admis dans les architectures de domaine qu'une entité ne devrait pas savoir comment se stocker - c'est le principe de l'ignorance de la persistance. Donc, pas d'appels à son référentiel pour s'ajouter au référentiel. Doit-elle savoir comment (et quand) stocker d'autres entités ? Encore une fois, cette responsabilité semble appartenir à un autre objet - peut-être un objet qui connaît le contexte d'exécution et la progression globale du cas d'utilisation actuel, comme un service de couche application.

Une entité pourrait-elle utiliser un référentiel pour récupérer une autre entité ? 90% du temps, cela ne devrait pas être nécessaire, car les entités dont il a besoin se trouvent généralement dans la portée de son agrégat ou peuvent être obtenues par la traversée d'autres objets. Mais il y a des moments où ils ne le sont pas. Si vous prenez une structure hiérarchique, par exemple, les entités doivent souvent accéder à tous leurs ancêtres, un petit-enfant particulier, etc. dans le cadre de leur comportement intrinsèque. Ils n'ont pas de référence directe à ces parents éloignés. Il serait gênant de leur faire passer ces proches comme paramètres de l'opération. Alors pourquoi ne pas utiliser un référentiel pour les obtenir - à condition qu'il s'agisse de racines agrégées?

Il y a quelques autres exemples. Le problème est que, parfois, il y a un comportement que vous ne pouvez pas placer dans un service de domaine car il semble s'intégrer parfaitement dans une entité existante. Et pourtant, cette entité doit accéder à un référentiel pour hydrater une racine ou une collection de racines qui ne peuvent pas lui être transmises.

Ainsi, l'accès à un référentiel à partir d'une entité n'est pas mauvais en soi , il peut prendre différentes formes qui résultent d'une variété de décisions de conception allant de catastrophique à acceptable.

guillaume31
la source
Je ne suis pas d'accord qu'une entité devrait utiliser un référentiel pour accéder à une entité avec laquelle elle a déjà une relation - vous devriez pouvoir parcourir le graphe d'objet pour accéder à cette entité. L'utilisation du référentiel de cette manière est un non non absolu. Ce dont je parle ici, c'est que l'entité n'a pas encore de référence, mais doit en créer une sous une certaine condition commerciale.
Paul T Davies
Eh bien, si vous m'avez bien lu, nous sommes totalement d'accord là-dessus ...
guillaume31
2

C'est une des raisons pour lesquelles je n'utilise pas d'énumérations ou de tables de recherche pures dans mon domaine. L'urgence et le statut sont tous deux des états et il existe une logique associée à un état qui appartient directement à l'état (par exemple, vers quels états puis-je passer en fonction de mon état actuel). De plus, en enregistrant un état en tant que valeur pure, vous perdez des informations telles que la durée de la tâche dans un état donné. Je représente les statuts comme une hiérarchie de classes comme ça. (En 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'implémentation de CompletedTaskStatus serait à peu près la même.

Il y a plusieurs choses à noter ici:

  1. Je protège les constructeurs par défaut. C'est ainsi que le framework peut l'appeler lors de l'extraction d'un objet de la persistance (EntityFramework Code-first et NHibernate utilisent des proxys dérivés de vos objets de domaine pour faire leur magie).

  2. De nombreux dépositaires de biens sont protégés pour la même raison. Si je veux changer la date de fin d'un intervalle, je dois appeler la fonction Interval.End () (cela fait partie de la conception pilotée par le domaine, fournissant des opérations significatives plutôt que des objets de domaine anémiques.

  3. Je ne le montre pas ici, mais la tâche cacherait également les détails de la façon dont elle stocke son état actuel. J'ai généralement une liste protégée d'États historiques que j'autorise le public à interroger s'il est intéressé. Sinon, j'expose l'état actuel en tant que getter qui interroge HistoricalStates.Single (state.Duration.End == null).

  4. La fonction TransitionTo est importante car elle peut contenir une logique sur les états valides pour la transition. Si vous avez juste une énumération, cette logique doit se trouver ailleurs.

J'espère que cela vous aidera à mieux comprendre l'approche DDD.

Michael Brown
la source
1
Ce serait certainement la bonne approche si les différents états ont un comportement différent comme dans votre exemple de modèle d'état, et cela résout certainement le problème discuté aussi. Cependant, je trouverais difficile de justifier une classe pour chaque état s'ils avaient juste des valeurs différentes, pas un comportement différent.
Paul T Davies
1

J'essaie de résoudre le même problème depuis un certain temps, j'ai décidé que je voulais pouvoir appeler Task.UpdateTask () comme ça, même si je préfère que ce soit spécifique au domaine, dans votre cas, je l'appellerais Task.ChangeCategory (...) pour indiquer une action et pas seulement CRUD.

de toute façon, j'ai essayé votre problème et j'ai trouvé ça ... prenez mon gâteau et mangez-le aussi. L'idée est que les actions se déroulent sur l'entité mais sans injection de toutes les dépendances. Au lieu de cela, le travail est effectué dans des méthodes statiques afin qu'ils puissent accéder à l'état de l'entité. L'usine met tout en œuvre et aura normalement tout ce dont elle a besoin pour faire le travail que l'entité doit faire. Le code client semble désormais propre et clair et votre entité ne dépend d'aucune injection de référentiel.

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
la source