Acessando repositórios do domínio

14

Digamos que tenhamos um sistema de registro de tarefas, quando uma tarefa é registrada, o usuário especifica uma categoria e a tarefa assume o status 'Excelente'. Suponha, neste caso, que Categoria e Status tenham que ser implementados como entidades. Normalmente eu faria isso:

Camada de aplicação:

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

Entidade:

public class Task
{
    //...

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

Faço-o assim porque sempre me dizem que as entidades não devem acessar os repositórios, mas faria muito mais sentido para mim se eu fizesse isso:

Entidade:

public class Task
{
    //...

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

O repositório de status é injetado com segurança de qualquer maneira, portanto, não há dependência real, e isso me parece mais o fato de que é o domínio que está fazendo a decisão que uma tarefa tem como padrão pendente. A versão anterior parece que é o leigo do aplicativo que toma essa decisão. Qualquer um dos motivos pelos quais os contratos de repositório estão frequentemente no domínio, se isso não deveria ser uma possibilidade?

Aqui está um exemplo mais extremo, aqui o domínio decide a urgência:

Entidade:

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

Não há como você desejar passar em todas as versões possíveis do Urgency e não deseja calcular essa lógica de negócios na camada de aplicativos; portanto, certamente essa seria a maneira mais apropriada.

Portanto, esse é um motivo válido para acessar repositórios do domínio?

EDIT: também pode ser o caso de métodos não 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
fonte

Respostas:

8

Você está misturando

entidades não devem acessar os repositórios

(o que é uma boa sugestão)

e

a camada de domínio não deve acessar os repositórios

(o que pode ser uma má sugestão, desde que seus repositórios façam parte da camada de domínio, não da camada de aplicativo). Na verdade, seus exemplos não mostram casos em que uma entidade acessa um repositório, pois você está usando métodos estáticos que não pertencem a nenhuma entidade.

Se você não deseja colocar essa lógica de criação em um método estático da classe de entidade, pode introduzir classes de fábrica separadas (como parte da camada de domínio!) E colocar a lógica de criação lá.

EDIT: no seu Updateexemplo: considerando que _urgencyRepositorye statusRepository são membros da classe Task, definidos como algum tipo de interface, agora você precisa injetá-los em qualquer Taskentidade antes de poder usá-lo Updateagora (por exemplo, no construtor Task). Ou você os define como membros estáticos, mas cuidado, isso pode facilmente causar problemas de multiencadeamento ou apenas problemas quando você precisar de repositórios diferentes para diferentes entidades de tarefa ao mesmo tempo.

Esse design torna um pouco mais difícil criar Taskentidades isoladamente, mais difícil escrever testes de unidade para Taskentidades, mais difíceis de escrever testes automáticos, dependendo das entidades da tarefa, e você produz um pouco mais de sobrecarga de memória, já que toda entidade da tarefa agora precisa mantenha essas duas referências aos repositórios. Obviamente, isso pode ser tolerável no seu caso. Por outro lado, a criação de uma classe de utilitário separada TaskUpdaterque mantém as referências aos repositórios corretos pode ser frequentemente ou pelo menos às vezes uma solução melhor.

A parte importante é: TaskUpdaterainda fará parte da camada de domínio! Só porque você coloca esse código de atualização ou criação em uma classe separada, não significa que você precisa mudar para outra camada.

Doc Brown
fonte
Eu editei para mostrar que isso se aplica tanto a métodos não estáticos quanto a métodos estáticos. Eu realmente nunca pensei que o método da fábrica não fazia parte de uma entidade.
Paul T Davies
@PaulTDavies: veja minha edição
Doc Brown
Concordo com o que você está dizendo aqui, mas eu adicionaria uma peça concisa que traça o ponto que Status = _statusRepository.GetById(Constants.Status.OutstandingId)é uma regra de negócios , uma que você pode ler como "A empresa determina que o status inicial de todas as tarefas será excelente" e é por isso que essa linha de código não pertence a um repositório, cujas únicas preocupações são o gerenciamento de dados por meio de operações CRUD.
Jimmy Hoffa
@ JimmyHoffa: hm, ninguém aqui estava sugerindo colocar esse tipo de linha em uma das classes de repositório, nem o OP nem eu - então qual é o seu ponto?
Doc Brown
Eu gosto bastante da idéia do TaskUpdater como um serviço doméstico. De alguma forma, parece um pouco falso apenas para manter os princípios do DDD, mas significa que posso evitar injetar o repositório toda vez que uso o Task.
Paul T Davies
6

Não sei se o seu exemplo de status é um código real ou aqui apenas para fins de demonstração, mas me parece estranho que você implemente o Status como uma entidade (para não mencionar uma raiz agregada) quando seu ID é uma constante definida no código - Constants.Status.OutstandingId. Isso não anula o objetivo dos status "dinâmicos" que você pode adicionar quantos quiser no banco de dados?

Eu acrescentaria que, no seu caso, a construção de um Task(incluindo o trabalho de obter o status correto do StatusRepository, se necessário) pode merecer um TaskFactorypouco, em vez de permanecer por Tasksi só, já que é um conjunto não trivial de objetos.

Mas :

É-me dito constantemente que as entidades não devem acessar os repositórios

Essa afirmação é imprecisa e simplista, na melhor das hipóteses, enganosa e perigosa, na pior das hipóteses.

É comumente aceito nas arquiteturas controladas por domínio que uma entidade não deve saber como se armazenar - esse é o princípio da ignorância da persistência. Portanto, nenhuma chamada para seu repositório se adiciona ao repositório. Deve saber como (e quando) armazenar outras entidades ? Novamente, essa responsabilidade parece pertencer a outro objeto - talvez um objeto que esteja ciente do contexto de execução e do progresso geral do caso de uso atual, como um serviço da camada de Aplicativo.

Uma entidade poderia usar um repositório para recuperar outra entidade ? 90% do tempo não deveria, uma vez que as entidades necessárias geralmente estão no escopo de seu agregado ou podem ser obtidas pela passagem de outros objetos. Mas há momentos em que não são. Se você adota uma estrutura hierárquica, por exemplo, as entidades geralmente precisam acessar todos os seus ancestrais, um neto em particular etc. como parte de seu comportamento intrínseco. Eles não têm uma referência direta a esses parentes remotos. Seria inconveniente passar esses parentes para eles como parâmetros da operação. Então, por que não usar um Repositório para obtê-los - desde que sejam raízes agregadas?

Existem alguns outros exemplos. O problema é que, às vezes, há um comportamento que você não pode colocar em um serviço de Domínio, pois ele parece se encaixar perfeitamente em uma entidade existente. E, no entanto, essa entidade precisa acessar um Repositório para hidratar uma raiz ou uma coleção de raízes que não podem ser passadas para ele.

Portanto, o acesso a um Repositório a partir de uma Entidade não é ruim por si só , ele pode assumir diferentes formas que resultam de uma variedade de decisões de design, que variam de catastróficas a aceitáveis.

guillaume31
fonte
Discordo que uma entidade deve usar um repositório para acessar uma entidade com a qual já possui um relacionamento - você deve poder percorrer o gráfico de objetos para acessar essa entidade. Usar o repositório dessa maneira é um absoluto não-não. O que estou discutindo aqui é que a entidade ainda não tem uma referência, mas precisa criar uma em alguma condição comercial.
Paul T Davies
Bem, se você leu-me bem, estamos totalmente concordo com isso ...
guillaume31
2

Esse é um dos motivos pelos quais não uso Enums ou tabelas de pesquisa pura no meu domínio. Urgência e Status são estados e existe uma lógica associada a um estado que pertence diretamente ao estado (por exemplo, em quais estados posso fazer a transição para o estado atual). Além disso, ao registrar um estado como um valor puro, você perde informações como por quanto tempo a tarefa estava em um determinado estado. Eu represento status como uma hierarquia de classes assim. (Em 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>();
  }
}

A implementação do CompletedTaskStatus seria praticamente a mesma.

Há várias coisas a serem observadas aqui:

  1. Eu protejo os construtores padrão. É assim que a estrutura pode chamá-lo ao extrair um objeto da persistência (o EntityFramework Code-first e o NHibernate usam proxies derivados dos objetos do seu domínio para fazer a mágica deles).

  2. Muitos dos configuradores de propriedades são protegidos pelo mesmo motivo. Se eu quiser alterar a data final de um Intervalo, preciso chamar a função Interval.End () (isso faz parte do Design Orientado a Domínio, fornecendo operações significativas em vez de Objetos de Domínio Anêmicos).

  3. Eu não o mostro aqui, mas a Tarefa também oculta os detalhes de como armazena seu status atual. Normalmente, tenho uma lista protegida de HistoricalStates que eu permito ao público consultar se estiver interessado. Caso contrário, eu exponho o estado atual como um getter que consulta HistoricalStates.Single (state.Duration.End == null).

  4. A função TransitionTo é significativa porque pode conter lógica sobre quais estados são válidos para transição. Se você apenas tem um enum, essa lógica deve estar em outro lugar.

Felizmente, isso ajuda a entender um pouco melhor a abordagem DDD.

Michael Brown
fonte
1
Essa seria certamente a abordagem correta se os diferentes estados tiverem um comportamento diferente, como no exemplo de padrão de estado, e certamente resolverá o problema discutido também. No entanto, seria difícil justificar uma classe para cada estado se eles tivessem valores diferentes, não comportamentos diferentes.
Paul T Davies
1

Eu tenho tentado resolver o mesmo problema há algum tempo, decidi que gostaria de poder chamar Task.UpdateTask () assim, embora eu prefira que seja específico do domínio, no seu caso, talvez eu o chame de Task.ChangeCategory (...) para indicar uma ação e não apenas CRUD.

Enfim, eu tentei o seu problema e vim com isso ... pegue meu bolo e coma também. A idéia é que ações ocorram na entidade, mas sem injeção de todas as dependências. Em vez disso, o trabalho é feito em métodos estáticos para que eles possam acessar o estado da entidade. A fábrica reúne tudo e normalmente terá tudo o que precisa para fazer o trabalho que a entidade precisa fazer. O código do cliente agora parece limpo e claro e sua entidade não depende de nenhuma injeção de repositório.

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
fonte