Cuide bem do seu domínio! Parte 1

Irei abordar nesse post e nos próximos uma forma de iniciar o desenvolvimento de uma aplicação web ASP.NET MVC 5 com base na metodologia do TDD - Test Driven Development, Specification Pattern, DDD e SOLID. Iremos focar na criação das entidades e serviços do domínio aplicando os testes de unidade que irão validar as regras de negócio da aplicação. Portanto, sem CRUD e sem camada de apresentação.

Vamos ao exemplo:
Será criada uma aplicação para cadastro de cursos, alunos e matrículas. Considerar como regras de negócio os dados abaixo:

1 - Cadastro do curso
- Código do curso único.
O código é digitado pelo usuário, composto pela sigla do curso mais ano letivo.
Exemplo: 1EF-2016 - onde 1EF = 1a serie fundamental.
1EM-2016 = 1o ano ensino médio

2 - Cadastro do aluno
- Ter idade entre 6 a 18 anos.
- CPF válido e único para a entidade Alunos

3 - Cadastro da matrícula
Limites de idades para matrícula do aluno
Ensino Fundamental
1a série(6 a 7), 2a(7 a 8), 3a(8 a 9), 4a(9 a 10), 5a(10 a 11), 6a(11 a 12), 7a(12 a 13), 8a(13 a 14) e 9a(14 a 15).
Ensino Médio
1o(15 a 16), 2o(16 a 17) 3o(17 a 18)
- A matrícula deverá possuir dois estados, true(efetivada) ou false(pré-matricula).
- Matrícula única - Aluno não pode ser matriculado duas vezes no mesmo curso.

Poderiam haver muitas regras e n variáveis no que se refere a uma matrícula, por enquanto vamos nos atentar às relacionadas acima.

Lembrando que nesse projeto vamos nos concentrar apenas na construção da camada de domínio e nos testes para validação da mesma.

Bora pro código...

Passo 1 - Crie um projeto no Visual Studio do tipo "Blank Solution". Lembre-se de aplicar regras na criação do nome como por exemplo, "NomeEmpresa.Projeto". A título de estudos, defini o nome apenas como "EscolaTDD".

Passo 2 - Adicione uma pasta chamada "Domain" na solução.

Passo 3 - Em seguida crie um projeto chamado "EscolaTDD.Domain" do tipo "Class Library" abaixo da pasta Domain. Após a criação do projeto, exclua a classe Class1.cs e delete todas as referências importadas automaticamente, exceto a System.

TDD-ASP-NET-2

Passo 4 - Crie as pastas e subpastas no projeto "EscolaTDD.Domain" conforme imagem abaixo.  A estrutura é sugestiva e ao meu ver bem organizada. Estou me baseando na arquitetura proposta no artigo sobre DDD do Eduardo Pires. O conteúdo de cada pasta será explicado nos próximos passos.

TDD-ASP-NET-3

Passo 5 - Vamos criar as nossas entidades. Adicione quatro classes na pasta "Entities" que irão representar as entidades Curso, Matrícula, Aluno e Endereço. Adicionei algumas listas na pasta Enum para compor a entidade Curso.

TDD-ASP-NET-4

Aluno.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Aluno
{
    public Aluno()
    {
        AlunoId = Guid.NewGuid();
        Enderecos = new List();
    }
    public Guid AlunoId { get; set; }
    public string Nome { get; set; }
    public string CPF { get; set; }
    public DateTime DataNascimento { get; set; }
    public virtual ICollection Matriculas { get; set; }
    public virtual ICollection Enderecos { get; set; }
}

Curso.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Curso
{
    public Curso()
    {
        CursoId = Guid.NewGuid();
    }
    public Guid CursoId { get; set; }
    public string CodigoCurso { get; set; }
    public string TituloCurso { get; set; }
    public NivelEnsino NivelEnsino { get; set; }
    public Periodo Periodo { get; set; }
    public int AnoLetivo { get; set; }
    public virtual ICollection Matriculas { get; set; }
    public virtual ICollection Alunos { get; set; }
}

Endereco.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Endereco
{
    public Endereco()
    {
        EnderecoId = Guid.NewGuid();
    }
 
    public Guid EnderecoId { get; set; }
    public string Logradouro { get; set; }
    public string Numero { get; set; }
    public string Complemento { get; set; }
    public string Bairro { get; set; }
    public string CEP { get; set; }
    public string Cidade { get; set; }
    public string Estado { get; set; }
    public Guid AlunoId { get; set; }
    public virtual Aluno Aluno { get; set; }
}

Matricula.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Matricula
{
    public Matricula()
    {
        MatriculaId = Guid.NewGuid();
    }
    public Guid MatriculaId { get; set; }
    public Guid CursoId { get; set; }
    public Guid AlunoId { get; set; }
    public bool Status { get; set; }
    public virtual Curso Curso { get; set; }
    public virtual Aluno Aluno { get; set; }
}

Passo 6 - Até aqui sem novidades né !? Entidades criadas porém sem nenhum método ou validação de suas propriedades ou regras de negócios. Vamos trabalhar agora na validação do domínio que é o objetivo principal desse post. Para isso, iremos instalar um pacote via Nuget que nos ajudará a criar as especificações do seu domínio e validá-las de acordo com os requisitos funcionais do seu projeto.

Abra o Package Manage Console e instale o pacote no projeto de domínio:
Install-Package DomainValidation

Esse pacote(by Eduardo Pires) foi desenvolvido com base no Specification Pattern. Em resumo, o Specification Pattern é uma forma de encapsular uma regra de negócio em um método que retorna um valor do tipo boolean. Dessa forma podemos criar classes com responsabilidade única para cada regra de negócio que, combinada com outras classes, irão facilitar a criação e manutenção de todas as especificações do projeto, por mais complexas que elas sejam.

Passo 7 - Vamos criar uma classe para validação do CPF do Aluno. Na pasta "Validations", crie uma subpasta chamada "Documents" e crie uma classe chamada "CPFValidation". Crie também na pasta \Specifications\Aluno\ a especificação que irá utilizar a validação de CPF.
TDD-ASP-NET-7

CPFValidation.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class CPFValidation
{
    public static bool Validar(string cpf)
    {
        if (cpf.Length > 11)
            return false;
 
        while (cpf.Length != 11)
            cpf = '0' + cpf;
 
        var igual = true;
        for (var i = 1; i < 11 && igual; i++)
            if (cpf[i] != cpf[0])
                igual = false;
 
        if (igual || cpf == "12345678909")
            return false;
 
        var numeros = new int[11];
 
        for (var i = 0; i < 11; i++)
            numeros[i] = int.Parse(cpf[i].ToString());
 
        var soma = 0;
        for (var i = 0; i < 9; i++)
            soma += (10 - i) * numeros[i];
 
        var resultado = soma % 11;
 
        if (resultado == 1 || resultado == 0)
        {
            if (numeros[9] != 0)
                return false;
        }
        else if (numeros[9] != 11 - resultado)
            return false;
 
        soma = 0;
        for (var i = 0; i < 10; i++)
            soma += (11 - i) * numeros[i];
 
        resultado = soma % 11;
 
        if (resultado == 1 || resultado == 0)
        {
            if (numeros[10] != 0)
                return false;
        }
        else if (numeros[10] != 11 - resultado)
            return false;
 
        return true;
    }
}

AlunoDeveTerCPFValidoSpecification.cs

1
2
3
4
5
6
7
public class AlunoDeveTerCPFValidoSpecification : ISpecification
{
    public bool IsSatisfiedBy(Aluno aluno) 
    {
        return CPFValidation.Validar(aluno.CPF);
    }
}

Passo 8 - Vamos criar mais uma especificação para a entidade Aluno que verifica se ele é elegível para cadastro com base na idade.
AlunoDeveTerIdadeEntre6a18Specification.cs

1
2
3
4
5
6
7
8
public class AlunoDeveTerIdadeEntre6a18Specification : ISpecification
{
    public bool IsSatisfiedBy(Aluno aluno)
    {
        return (DateTime.Now.Year - aluno.DataNascimento.Year >= 6 
            && DateTime.Now.Year - aluno.DataNascimento.Year <= 18);
    }
}

Passo 9 - Crie uma subpasta "Alunos" dentro da pasta "Validations" e adicione uma classe chamada AlunoConsistenteValidation.cs. Ela será responsável por instanciar as classes de especificação e retornar a coleção de erro caso as especificações não sejam atendidas.

1
2
3
4
5
6
7
8
9
10
11
public class AlunoConsistenteValidation : Validator
{
    public AlunoConsistenteValidation()
    {
        var CPFAluno = new AlunoDeveTerCPFValidoSpecification();
        var alunoFaixaEtaria = new AlunoDeveTerIdadeEntre6a18();
 
        base.Add("CPFAluno", new Rule(CPFAluno, "Aluno informou um CPF inválido."));
        base.Add("alunoFaixaEtaria", new Rule(alunoFaixaEtaria, "Aluno não se enquadra na faixa etária da escola."));
    }
}

Passo 10 - Altere a entidade Aluno e crie um método, IsValid(), que retorna um boolean e uma propriedade do tipo ValidationResult, que faz referência a uma classe do pacote DomainValidation. Ambos permitirão que a entidade possa autovalidar-se.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Aluno
{
    public Aluno()
    {
        AlunoId = Guid.NewGuid();
        Enderecos = new List();
    }
    public Guid AlunoId { get; set; }
    public string Nome { get; set; }
    public string CPF { get; set; }
    public DateTime DataNascimento { get; set; }
    public virtual ICollection Matriculas { get; set; }
    public virtual ICollection Enderecos { get; set; }
    public ValidationResult ValidationResult { get; set; } // add property
 
    public bool IsValid() // add method
    {
        ValidationResult = new AlunoConsistenteValidation().Validate(this);
        return ValidationResult.IsValid;
    }
}

Passo 11 - O próximo passo é criar o projeto de testes para testar a camada de domínio. Dentro da pasta "Domain", crie um projeto do tipo "Unit Test Project".
TDD-ASP-NET-5
Adicione a referência do projeto de domínio ao projeto de testes.
TDD-ASP-NET-6
Iremos utilizar o próprio framework de testes do Visual Studio - Unit Testing.

Instale no projeto de testes o pacote DomainValidation - Install-Package DomainValidation

Passo 12 -Crie uma pasta dentro do projeto de testes chamada "Entities" e adicione a classe AlunoTests.cs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
[TestClass]
public class AlunoTests
{
    public Aluno Aluno { get; set; }
 
    [TestMethod]
    public void Aluno_Valido_True()
    {
        Aluno = new Aluno()
        {
            CPF = "65560639834", //informando CPF válido
            DataNascimento = new DateTime(2005, 01, 01) // informando data nascimento válida para cadastro
        };
 
        Assert.IsTrue(Aluno.IsValid());
    }
 
    [TestMethod]
    public void Aluno_Valido_False()
    {
        Aluno = new Aluno()
        {
            CPF = "65560639800", //informando CPF inválido
            DataNascimento = new DateTime(1995, 01, 01) // informando data nascimento inválida 
        };
 
        Assert.IsFalse(Aluno.IsValid());
        Assert.IsTrue(Aluno.ValidationResult.Erros.Any(e => e.Message == "Aluno informou um CPF inválido."));
        Assert.IsTrue(Aluno.ValidationResult.Erros.Any(e => e.Message == "Aluno não se enquadra na faixa etária da escola."));
    }
}

Criamos dois métodos na classe AlunoTests. O método Aluno_Valido_True irá instanciar a entidade Aluno atribuindo valores válidos nas propriedades do objeto de forma que o método retorne true. Já o método Aluno_Valido_False é forçado a retornar false quando atribuímos propriedades inválidas.
A classe Assert certifica-se de que o retorno do método é true ou false ao receber propriedades válidas ou inválidas, garantindo a consistência da entidade Aluno.

Perceba que em nenhum momento saímos da camada de domínio e as regras de negócio serão testadas e validadas independentemente. Tenha em mente que as regras do negócio estarão definidas no seu domínio e elas precisam estar bem definidas e testadas para garantir a qualidade da aplicação que você se propôs a desenvolver.

Na parte 2 desse tutorial, iremos implementar as outras regras de negócio no domínio, testar as novas especificações e também iniciar as validações na camada de apresentação.

Até lá!
Thanks for reading!

4 thoughts on “Cuide bem do seu domínio! Parte 1

  1. Olá Orlando.
    A resposta para a sua pergunta está na camada de Application Service, que é uma camada intermediária entre o domínio e camada de apresentação, seja um ASP.Net MVC ou WebAPI. Ela pode acessar os repositórios como também acessar serviços do domínio.
    A camada de Application Service irá receber por injeção de dependência uma instância da classe de servico do domínio pra fazer as validações antes de executar commit.
    Abs

  2. Olá! Belo artigo!
    Mas tenho uma dúvida. Sabendo que a camada de Domínio não conhece outras camadas, e que seus serviços interagem por interfaces com as classes de repositórios, como implementar validações que usem os serviços de Domínio e que usem repositórios para buscar dados? Isso num cenário que use injeção por dependência, que tem por projeto root um aspnetmvc?

  3. Pingback: Cuide bem do seu domínio! Parte 2 | Alexandre Miranda

Leave a Reply

Your email address will not be published. Required fields are marked *

This blog is kept spam free by WP-SpamFree.