Árvore de páginas

Você está vendo a versão antiga da página. Ver a versão atual.

Comparar com o atual Ver Histórico da Página

« Anterior Versão 21 Próxima »

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.

Criando o projeto

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

Definindo uma Entidade

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".

Entidades são derivadas de classe Entity no TNF onde esta já possui uma chave (do tipo int ou informada através de um parâmetro genérico):

Quando a entidade é criada também é definido um enumerador, com códigos únicos que descrevem os erros de negócio da entidade.

Person.cs
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
        }
    }
}

Note que o exemplo acima está marcado como internal para que ela não seja exposta para fora da camada de domínio.

Validação de Entidades 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 (representada por um DTO) que coleta erros e os expõe de forma tratadas aos 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).

Criando uma especificação

Abaixo vemos um exemplo de criação de uma especificação para a entidade Person:

PersonNameShouldHaveValueSpecification.cs
using System;
using System.Linq.Expressions;
using Tnf.Specifications;

namespace Tnf.Architecture.Domain.Registration.Specifications
{
    internal class PersonNameShouldHaveValueSpecification : Specification<Person>
    {
        public override string LocalizationSource => AppConsts.LocalizationSourceName;
        public override Enum LocalizationKey => Professional.Error.ProfessionalNameMustHaveValue;
 
        public override Expression<Func<Person, bool>> ToExpression()
        {
            return (p) => !string.IsNullOrWhiteSpace(p.Name);
        }
    }
}

Dentro do pacote Tnf.App.Specifications podemos utilizar através da herança a classe Specification<Entity> que recebe como parâmetro a entidade a ser validada.

Pense na especificação como sendo uma regra para sua entidade. A regra acima valida para que o campo "Name" de nossa classe Person não seja inválido.

Criando um Builder

O pattern de Builder é usado para construir nossa entidade de negocio, 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 negócio.

Podemos ver sua definição para a entidade criada Person disponível a seguir:

PersonBuilder.cs
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 Person Build()
        {
            base.Validate();
            return base.Build();
        }
 
        protected override void Specifications()
        {
            AddSpecification(new PersonNameShouldHaveValueSpecification());
        }
    }
}

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.

Podemos ver no exemplo acima que chamamos o método Validate da classe pai para executar todas as Specifications adicionadas no método sobrescrito Specifications() e se a regra das especificações adicionadas não for valida, ele adicionará uma Notification no objeto único do request com a mensagem especificada pelos campos LocalizationSource e LocalizationKey utilizando a classe LocalizationHelper para obter a mensagem de erro através da localização.

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.

Definindo um Repositório

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çe a fonte da informação obtida/persistida (Relacional, NoSQL, Arquivo, etc).

A interface IRepository fará a injeção de dependência pela convenção de quem a implementar.

Todo novo repositório deve ser herdado desta interface:

IPersonRepository.cs
public interface IPersonRepository : IRepository
{
	int CreatePerson(PersonDto personDto);
}

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.

Criando um Serviço de Domínio

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, temos a interface IDomainService e uma classe chamada AppDomainService<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:

IRegistrationService.cs
using Tnf.Domain.Services;
using Tnf.Dto;

namespace Tnf.Architecture.Domain.Interfaces.Services
{
    public interface IRegistrationService : IDomainService
    {
        PersonDto Register(PersonDto dto);
    }
}

Podemos perceber a herança da interface IDomainService e o retorno do objeto do método Register PersonDto.

Esse objeto de resposta irá retornar o objeto enviado com o id que foi salvo no banco.

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.

RegistrationService.cs
using Tnf.Architecture.Domain.Interfaces.Services;
using Tnf.App.Domain.Services;
using Tnf.Dto;

namespace Tnf.Architecture.Domain.Registration
{
    public class RegistrationService : AppDomainService<IPersonRepository>, IRegistrationService
    {
        public RegistrationService(IPersonRepository repository) : base(repository) 
		{ 
		}
        public PersonDto Register(PersonDto dto)
        {
            var builder = new PersonBuilder()
                   .WithName(dto.Name);
 
            var person = builder.Build();
            if (!Notification.HasNotification())
            {
                var id = Repository.CreatePerson(person);
                dto.Id = id;
            }
            return dto;
        }
    }
}

No inicio do método Register definimos o objeto de retorno e logo após criamos o PersonBuilder para construir nossa entidade através da chamada do método Build.

O método Build nos retorna a estrutura da Entidade e se foi levantado notificações  durante o build podemos verificar se existe notificações com o campo exposto pela classe Tnf.App.Domain.Services.AppDomainService Notification.

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 AppDomainService<IPersonRepository>.

Neste exemplo temos apenas um repositório, mas lembre-se que temos presente a injeção de dependência do framework e podemos usar quantos forem necessários em nosso serviço de domínio.

  • Sem rótulos