Acceso a repositorios desde el dominio

14

Supongamos que tenemos un sistema de registro de tareas, cuando se registra una tarea, el usuario especifica una categoría y la tarea por defecto es un estado de "Excepcional". Suponga en este caso que la Categoría y el Estado deben implementarse como entidades. Normalmente haría esto:

Capa de aplicación:

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

Entidad:

public class Task
{
    //...

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

Lo hago así porque constantemente me dicen que las entidades no deberían acceder a los repositorios, pero tendría mucho más sentido para mí si hiciera esto:

Entidad:

public class Task
{
    //...

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

El repositorio de estado es la dependencia inyectada de todos modos, por lo que no hay una dependencia real, y esto me parece más, ya que es el dominio el que está tomando la decisión de que una tarea predeterminada sea sobresaliente. La versión anterior se siente como si la capa de aplicación tomara esa decisión. ¿Por qué los contratos de repositorio a menudo están en el dominio si esto no debería ser una posibilidad?

Aquí hay un ejemplo más extremo, aquí el dominio decide la urgencia:

Entidad:

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

No hay forma de que desee pasar en todas las versiones posibles de Urgencia, y no hay forma de que desee calcular esta lógica de negocios en la capa de aplicación, por lo que seguramente sería la forma más adecuada.

Entonces, ¿es esta una razón válida para acceder a los repositorios desde el dominio?

EDITAR: Este también podría ser el caso en los métodos no estáticos:

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
fuente

Respuestas:

8

Te estás entremezclando

las entidades no deben acceder a los repositorios

(que es una buena sugerencia)

y

la capa de dominio no debe acceder a los repositorios

(lo que puede ser una mala sugerencia siempre que sus repositorios sean parte de la capa de dominio, no de la capa de aplicación). En realidad, sus ejemplos no muestran ningún caso en el que una entidad acceda a un repositorio, ya que está utilizando métodos estáticos que no pertenecen a ninguna entidad.

Si no desea poner esa lógica de creación en un método estático de la clase de entidad, puede introducir clases de fábrica separadas (como parte de la capa de dominio) y colocar la lógica de creación allí.

EDITAR: a su Updateejemplo: dado que _urgencyRepositoryy statusRepository son miembros de la clase Task, definidos como algún tipo de interfaz, ahora debe inyectarlos en cualquier Taskentidad antes de poder usarlos Updateahora (por ejemplo, en el constructor de tareas). O bien, puede definirlos como miembros estáticos, pero tenga cuidado, ya que podrían causar fácilmente problemas de subprocesos múltiples, o simplemente problemas cuando necesita diferentes repositorios para diferentes entidades de tareas al mismo tiempo.

Este diseño hace que sea un poco más difícil crear Taskentidades de forma aislada, por lo tanto, es más difícil escribir pruebas unitarias para Taskentidades, más difícil escribir pruebas automáticas dependiendo de las entidades de Tarea, y produce un poco más de sobrecarga de memoria, ya que cada entidad de Tarea ahora necesita sostienen que dos referencias a las reposiciones. Por supuesto, eso puede ser tolerable en su caso. Por otro lado, crear una clase de utilidad separada TaskUpdaterque mantenga las referencias a los repositorios correctos puede ser a menudo o al menos a veces una mejor solución.

La parte importante es: ¡ TaskUpdaterseguirá siendo parte de la capa de dominio! El hecho de que ponga ese código de actualización o creación en una clase separada no significa que deba cambiar a otra capa.

Doc Brown
fuente
He editado para mostrar que esto se aplica tanto a los métodos no estáticos como a los estáticos. Nunca pensé realmente que el método de fábrica no formara parte de una entidad.
Paul T Davies
@PaulTDavies: ver mi edición
Doc Brown
Estoy de acuerdo con lo que está diciendo aquí, pero agregaría una pieza concisa que señala el punto que Status = _statusRepository.GetById(Constants.Status.OutstandingId)es una regla comercial , una que podría leer como "La empresa dicta que el estado inicial de todas las tareas será excepcional" y es por eso esa línea de código no pertenece dentro de un repositorio, cuyas únicas preocupaciones son la gestión de datos a través de operaciones CRUD.
Jimmy Hoffa
@JimmyHoffa: hm, nadie aquí estaba sugiriendo poner ese tipo de línea en una de las clases de repositorio, ni el OP ni yo, entonces, ¿cuál es tu punto?
Doc Brown
Me gusta bastante la idea de TaskUpdater como un servicio domian. De alguna manera parece un poco falso para retener los principios DDD, pero significa que puedo evitar inyectar el repositorio cada vez que uso Task.
Paul T Davies
6

No sé si su ejemplo de estado es código real o aquí solo por el bien de la demostración, pero me parece extraño que deba implementar Status como una Entidad (sin mencionar una Raíz Agregada) cuando su ID es una constante definida en código - Constants.Status.OutstandingId. ¿Eso no vence el propósito de los estados "dinámicos" que puede agregar tantos como desee en la base de datos?

Agregaría que en su caso, la construcción de un Task(incluido el trabajo de obtener el estado correcto de StatusRepository si es necesario) podría merecer un TaskFactorylugar en lugar de quedarse en Tasksí mismo, ya que es un conjunto no trivial de objetos.

Pero :

Me dicen constantemente que las entidades no deben acceder a los repositorios

Esta declaración es imprecisa y demasiado simplista en el mejor de los casos, engañosa y peligrosa en el peor.

En las arquitecturas basadas en dominios se acepta con bastante frecuencia que una entidad no debería saber cómo almacenarse a sí misma ; ese es el principio de ignorancia de persistencia. Entonces no hay llamadas a su repositorio para agregarse al repositorio. ¿Debería saber cómo (y cuándo) almacenar otras entidades ? Una vez más, esa responsabilidad parece pertenecer a otro objeto, tal vez un objeto que conoce el contexto de ejecución y el progreso general del caso de uso actual, como un servicio de capa de aplicación.

¿Podría una entidad usar un repositorio para recuperar otra entidad ? El 90% de las veces no debería tener que hacerlo, ya que las entidades que necesita generalmente están dentro del alcance de su agregado o pueden obtenerse atravesando otros objetos. Pero hay momentos en que no lo son. Si toma una estructura jerárquica, por ejemplo, las entidades a menudo necesitan acceder a todos sus antepasados, un nieto en particular, etc. como parte de su comportamiento intrínseco. No tienen una referencia directa a estos parientes remotos. Sería inconveniente pasarles a estos parientes como parámetros de la operación. Entonces, ¿por qué no usar un repositorio para obtenerlos, siempre que sean raíces agregadas?

Hay algunos otros ejemplos. La cuestión es que a veces hay un comportamiento que no puede ubicar en un servicio de Dominio, ya que parece encajar perfectamente en una entidad existente. Y, sin embargo, esta entidad necesita acceder a un Repositorio para hidratar una raíz o una colección de raíces que no se le pueden pasar.

Por lo tanto, acceder a un Repositorio desde una Entidad no es malo en sí mismo , puede tomar diferentes formas que resultan de una variedad de decisiones de diseño que van desde catastróficas hasta aceptables.

guillaume31
fuente
No estoy de acuerdo con que una entidad deba usar un repositorio para acceder a una entidad con la que ya tiene una relación: debe poder recorrer el gráfico de objetos para acceder a esa entidad. Usar el repositorio de esta manera es un absoluto no no. De lo que estoy hablando aquí es de entidades a las que la entidad todavía no tiene una referencia, pero necesita crear una bajo alguna condición comercial.
Paul T Davies
Bueno, si me has leído bien, estamos totalmente de acuerdo en eso ...
guillaume31
2

Esta es una razón por la que no uso Enums o tablas de búsqueda pura dentro de mi dominio. La urgencia y el estado son ambos estados y hay una lógica asociada con un estado que pertenece directamente al estado (por ejemplo, ¿a qué estados puedo hacer la transición dado mi estado actual?). Además, al registrar un estado como un valor puro, pierde información como cuánto tiempo estuvo la tarea en un estado determinado. Represento los estados como una jerarquía de clases así. (Cía#)

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

La implementación de CompletedTaskStatus sería prácticamente la misma.

Hay varias cosas a tener en cuenta aquí:

  1. Hago que los constructores predeterminados estén protegidos. Esto es para que el marco pueda llamarlo cuando saca un objeto de la persistencia (tanto EntityFramework Code-first como NHibernate usan proxies que se derivan de los objetos de su dominio para hacer su magia).

  2. Muchos de los establecedores de propiedades están protegidos por la misma razón. Si quiero cambiar la fecha de finalización de un intervalo, tengo que llamar a la función Interval.End () (esto es parte del diseño dirigido por dominio, que proporciona operaciones significativas en lugar de objetos de dominio anémico).

  3. No lo muestro aquí, pero la Tarea también ocultaría los detalles de cómo almacena su estado actual. Por lo general, tengo una lista protegida de HistoricalStates que le permito al público consultar si están interesados. De lo contrario, expongo el estado actual como un captador que consulta HistoricalStates.Single (state.Duration.End == null).

  4. La función TransitionTo es importante porque puede contener lógica sobre qué estados son válidos para la transición. Si solo tienes una enumeración, esa lógica tiene que estar en otra parte.

Con suerte, esto lo ayudará a comprender un poco mejor el enfoque DDD.

Michael Brown
fuente
1
Sin duda, este sería el enfoque correcto si los diferentes estados tienen un comportamiento diferente al de su ejemplo de patrón de estado, y ciertamente también resuelve el problema discutido. Sin embargo, me resultaría difícil justificar una clase para cada estado si solo tuvieran valores diferentes, no comportamientos diferentes.
Paul T Davies
1

He estado tratando de resolver el mismo problema durante algún tiempo, decidí que quería poder llamar a Task.UpdateTask () así, aunque preferiría que fuera específico del dominio, en su caso tal vez lo llamaría Task.ChangeCategory (...) para indicar una acción y no solo CRUD.

de todos modos, probé tu problema y se me ocurrió esto ... toma mi pastel y cómelo también. La idea es que las acciones se realicen en la entidad pero sin la inyección de todas las dependencias. En cambio, el trabajo se realiza en métodos estáticos para que puedan acceder al estado de la entidad. La fábrica pone todo junto y normalmente tendrá todo lo que necesita para hacer el trabajo que la entidad necesita hacer. El código del cliente ahora se ve limpio y claro y su entidad no depende de ninguna inyección de repositorio.

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

    }
}
Miguel
fuente