Para trabalhar com nossa camada de domínio precisamos definir nossas entidades, suas validações, criar nossos repositórios e serviços de domínio.
Para começarmos a trabalhar temos que criar um projeto com suporte a .NET Core.
Para isso através do Visual Studio vamos criar nosso projeto acessando: File -> New -> Project -> .NET Standard -> Class Library.
Com o projeto criado vamos adicionar os seguintes pacotes via nuget: Tnf.Builder, Tnf.Dto, Tnf disponível em nosso package source: https://www.myget.org/F/tnf/api/v3/index.json
As entidades são um dos conceitos fundamentais do DDD (Domain Driven Design) onde Eric Evans descreve-o como "um objeto que não é fundamentalmente definido por seus atributos, mas sim por um fio de continuidade e identidade".
No TNF, entidades são derivadas de classe Entity onde esta já possui uma chave (do tipo int ou informada através de um parâmetro genérico):
using System; using Tnf.Domain.Entities; namespace Tnf.Architecture.Domain.Registration { internal class Person : Entity { public string Name { get; internal set; } public DateTime CreationTime { get; internal set; } public Person() => CreationTime = DateTime.Now; public enum Error { Unexpected = 0, PersonIdMustHaveValue = 1, PersonNameMustHaveValue = 2 } } }
Quando a entidade é criada também é definido um enumerador, com códigos únicos que descrevem os erros de negócio da entidade. Note que o exemplo acima está marcado como internal para que ela não seja exposta para fora da camada de domínio.
Para validar qualquer tipo de regra em cima de uma entidade usamos alguns patterns: Specification, Builder e Notification.
Em um de seus artigos descrevendo o uso do Notification pattern, Martin Flowler faz uma citação sobre o uso de exception dentro de uma aplicação descrita pelos autores Dave Thomas Andy Hunt em seu livro "The Pragmatic Programmer: From Journeyman to Master" (versão traduzida):
"Acreditamos que exceções raramente devem ser usadas como parte de um fluxo normal em um programa: exceções devem ser reservadas para eventos inesperados"
No TNF pensamos da mesma forma: não devemos usar exceção para simples validações de nossas entidades de domínio.
Podemos resumir o notification pattern como uma estrutura (representado como um DTO no TNF) que coleta erros e os expõe de forma tratadas as diversos níveis da aplicação.
Temos que ter em mente que entidades de domínio são a representação de nosso objeto de negócio evitando que elas sejam criadas de forma anêmica ou em um estado inválido (sem comportamento).
Abaixo vemos um exemplo de criação de uma especificação para a entidade Person:
using System; using System.Linq.Expressions; using Tnf.Specifications; namespace Tnf.Architecture.Domain.Registration.Specifications { internal class PersonNameShouldHaveValueSpecification : Specification<Person> { public override Expression<Func<Person, bool>> ToExpression() { return (p) => !string.IsNullOrWhiteSpace(p.Name); } } }
Dentro do pacote Tnf podemos utilizar através da herança a classe Specification<> que recebe como parâmetro a entidade a ser validada.
Pensa na especificação como sendo uma regra para sua entidade.
Sendo assim a regra acima valida para que o campo "Name" de nossa classe Person não seja inválido.
O pattern de Builder é usado para construir nossa entidade de negocio aqui no TNF, não apenas isso mas também agregar todas as especificações e retornar uma estrutura informado se a entidade é valida ou não para ser utilizada.
Pense no Builder como um agregador de regras para a entidade de negocio. Podemos ver sua definição para a entidade criada Person disponível a seguir:
using Tnf.Architecture.CrossCutting; using Tnf.Architecture.Domain.Registration.Specifications; using Tnf.Builder; using Tnf.Localization; namespace Tnf.Architecture.Domain.Registration { internal class PersonBuilder : Builder<Person> { public PersonBuilder() : base() { } public PersonBuilder(Person instance) : base(instance) { } public PersonBuilder WithId(int id) { Instance.Id = id; return this; } public PersonBuilder WithName(string name) { Instance.Name = name; return this; } public override BuilderResponse<Person> Build() { var shouldHaveName = new PersonNameShouldHaveValueSpecification(); if (!shouldHaveName.IsSatisfiedBy(Instance)) { var notificationMessage = LocalizationHelper.GetString(AppConsts.LocalizationSourceName, Person.Error.PersonNameMustHaveValue); Response.AddNotification(Person.Error.PersonNameMustHaveValue, notificationMessage); } return base.Build(); } } }
Note que o builder acima cria a entidade de forma fluente, adicionando valores a entidade e através do método sobrescrito Build executando todas as regras (especificações) de nossa entidade.
A estrutura retornada pelo método de Build é do Tipo BuilderResponse<Person>.
Esse objeto BuilderResponse vem do pacote Tnf.Dto e possibilita que utilizemos assim o Notification pattern para retornar as validações da entidade como notificações de negocio evitando o uso de exceções.
Também podemos ver no exemplo acima que se a regra da especificação testada não é valida (IsSatisfiedBy) utilizamos a classe LocalizationHelper para obter a mensagem de erro através da localização do TNF, adicionando ela ao objeto de Response que contém todas as notificações do builder.
A classe PersonBuilder também está marcada como internal para que tanto a entidade, regras e builder estejam fechadas dentro de nossa camada de domínio.
Quando precisamos consultar uma fonte de dados em nosso domínio, precisamos fazer isso através de um repositório.
Temos que ter em mente que o repositório é apenas definido nesta camada e não criado.
Nossa camada de domínio tem a responsabilidade de implementar a interface (contrato) de nosso repositório deixando para que outra camada de infraestrutura realize sua implementação.
Esta camada não conheço a fonte da informação obtida/persistida (Relacional, NoSQL, Arquivo, etc).
O TNF possui uma interface chamada IRepository que fará a injeção de dependência pela convenção de quem a implementar.
Todo novo repositório deve ser herdado desta interface:
public interface IPersonRepository : IRepository { int CreatePerson(PersonDto personDto); }
Note que ao definir nosso repositório não passamos a entidade de domínio em sua declaração e sim um DTO que a representa. O uso do DTO é para que a entidade de domínio não seja exposta e assim dessa forma sua integridade seja mantida.
Quando falamos em serviço de domínio podemos falar em processos de negócio. Um serviço de domínio tem a característica de representar esses processos de forma clara e definida utilizando toda a infraestrutura necessária (Repositórios).
Para trabalhar com serviços de domínio, o TNF prove uma interface IDomainService e uma classe chamada DomainService<IRepository> que recebe por parâmetro genérico um repositório.
Tanto a interface como a classe base quando implementadas já utilizam as convenções ao realizar a injeção da dependência automaticamente.
O serviço por default recebe um repositório podendo conter um ou mais de acordo com a necessidade.
Abaixo temos a definição de um serviço:
using Tnf.Domain.Services; using Tnf.Dto; namespace Tnf.Architecture.Domain.Interfaces.Services { public interface IRegistrationService : IDomainService { DtoResponseBase<PersonDto> Register(PersonDto dto); } }
Podemos perceber a herança da interface IDomainService e o retorno do objeto do método Register DtoResponseBase.
Esse objeto de resposta irá retornar as validações da entidade caso ela não esteja apta a ser cadastrada.
O código a seguir exemplifica a implementação de nosso serviço de domínio, usando o repositório e o builder criado para a entidade Person.
using Tnf.Architecture.Domain.Interfaces.Services; using Tnf.Domain.Services; using Tnf.Dto; namespace Tnf.Architecture.Domain.Registration { public class RegistrationService : DomainService<IPersonRepository>, IRegistrationService { public RegistrationService(IPersonRepository repository) : base(repository) { } public DtoResponseBase<PersonDto> Register(PersonDto dto) { var response = new DtoResponseBase<PersonDto>(); var builder = new PersonBuilder() .WithName(dto.Name); var build = builder.Build(); if (!build.Success) { response.AddNotifications(build.Notifications); return response; } var id = Repository.CreatePerson(dto); dto.Id = id; return response; } } }
Perceba que no inicio do método Register definimos o objeto de retorno e logo após criamos o PersonBuilder para construir nossa entidade validando ela logo após sua criação pelo método Build.
O método Build nos retorna a estrutura de DTO informando se contém ou não notificações presentes na entidade através da propriedade Success.
Caso existam notificações retornadas do Build agregamos elas ao nosso retorno.
Quando a entidade é apta a ser utilizada podemos consumir nosso repositório através da propriedade Repository que corresponde a IPersonRepository recebida pela injeção de interface no construtor da classe herdada DomainService<IPersonRepository>.
Neste exemplo temos apenas um repositório, mas lembre-se que temos presente a injeção de dependência do TNF e podemos usar quantos forem necessários em nosso serviço de domínio.