Clean Code - Desenvolvimento com qualidade Fique por dentro Este artigo é útil porque fornece algumas propostas para um melhor uso da orientação a objetos, a fim de que seja possível deixar o código menos acoplado, mais inteligente e limpo. Também buscaremos apresentar aspectos dos modelos anêmico e de domínio, mostrando as principais diferenças na hora de optar por um deles. A máxima popular afirma que para bom entendedor, meia palavra basta. Seguramente também poderíamos utilizá-la na nossa profissão e afirmar: para um bom programador, meio código basta. Quando o código é bem escrito, qualquer programador pode compreendê-lo sem grandes dificuldades e entende rapidamente a real função da implementação em questão. Como diria Martin Fowler, “qualquer um pode escrever um código que o computador entenda. Bons programadores escrevem códigos que os humanos entendem”. Este é o verdadeiro desafio do desenvolvedor. Neste contexto, o objetivo deste artigo, como o próprio título sugere, é propor aspectos que tornem o código mais claro, de modo que ele possa ser compreendido com facilidade por outros profissionais. Para a criação de um código limpo nos termos de Robert C. Martin, torna-se necessário seguir algumas regras básicas. Segundo este autor, a possibilidade de um fácil entendimento e manutenção decorre do cumprimento de determinadas regras. Existe uma significativa literatura referente ao tema código limpo. Neste artigo faremos comentários sobre o que é o código limpo e como alcançá-lo, através de um breve resumo baseado no livro Clean Code, de Robert C. Martin. Neste livro, são descritas regras que definem, por exemplo, a nomenclatura adequada para classes, métodos e atributos, o correto uso dos comentários, a melhor formatação do código, o tratamento de erros e até mesmo testes unitários. Martin, mais conhecido como Uncle Bob, é um grande nome na comunidade de desenvolvimento de software, trabalhando na área desde 1970. Fundador e presidente da Object Mentor Inc, é um dos 17 membros do Manifesto Ágil e publicou diversos artigos e livros sobre o assunto. Contudo, não adianta apenas ter um código limpo e bem escrito, se o mesmo for aplicado a um modelo de domínio mal estruturado. Para resolver este problema serão abordadas as diferenças entre o modelo anêmico e o modelo de domínio. Estes modelos são tipos de padrões que o programador poderá utilizar para o desenvolvimento das funcionalidades de seu respectivo projeto. Apesar disso, atualmente o modelo anêmico é considerado um anti-pattern, ou seja, ele é um anti-padrão. Como curiosidade, a maioria dos profissionais ainda insiste em utilizá-lo. Com base nisso, neste artigo procuraremos expor algumas razões que confirmem as limitações deste modelo, e como alternativa, apresentaremos o modelo de domínio (domain model), que consiste numa visão e numa técnica para lidar tanto com os casos de domínios mais simples até os altamente complexos. Para a definição correta do domínio, teremos como base o uso da prática do DDD, técnica criada por Eric Evans, autor do livro Domain-Driven Design e um líder nos estudos sobre design de software e modelagem 1
de domínio. Assim, serão propostas algumas sugestões para que o desenvolvedor possa criar códigos claros, eficazes e de fácil compreensão para outros desenvolvedores. Como anda o seu código? Em primeiro lugar, devemos ter, de forma clara, a ideia do que se entende por código limpo (ver BOX 1). O código limpo não deve ser apenas desejável devido à sua organização, mas também pela facilidade com que outros profissionais poderão futuramente manuseá-lo. Quando um código não está bom, ele só tende a piorar com o decorrer do tempo, pois o desenvolvedor que tiver de fazer alguma manutenção, possivelmente não o fará com tanta organização porque encontrou o trabalho inicial já desestruturado. É como o exemplo do carro com um dos vidros quebrados. BOX 1. Código Limpo Segundo Kent Beck, um dos integrantes do Manifesto Ágil, Código Limpo (Clean Code), de forma resumida, é o código fácil de entender, fácil de modificar e fácil de testar. Foi realizada uma experiência nos Estados Unidos que consistia em deixar um carro trancado e com apenas uma das janelas quebradas numa esquina. Após alguns dias, o mesmo já possuía mais janelas quebradas. Ao longo de mais algumas semanas, o carro já estava todo amassado e depenado. Para comparação, outro carro foi deixado no mesmo local, só que dessa vez sem problema algum. Este automóvel permaneceu assim por semanas e ao final da experiência, nenhum dano lhe foi causado. Com este exemplo queremos dizer que as pessoas têm zelo por aquilo que está arrumado, que está sob bons cuidados. O mesmo acontece com o código. Quando o desenvolvedor escreve-o de qualquer forma, ou seja, sem nenhum padrão, os outros profissionais que posteriormente manusearem seu código irão danificá-lo ainda mais. Em outras palavras, o profissional tende a perpetuar uma desestruturação já existente. Um código limpo é aquele que segue determinados padrões e regras de implementação, desde a escolha do nome da classe, atributos e métodos, até o uso correto da orientação a objetos. Para exemplificar esses padrões e regras, apresentaremos sub tópicos que explicam de forma breve as etapas para tornar o código eficaz e limpo. Nomes que fazem sentido Os nomes que fazem sentido são os nomes pronunciáveis. A nomenclatura dada aos atributos, métodos e classes deve ser autoexplicativa, ou seja, deve esclarecer sua real função de imediato. Portanto, não economize nas palavras e evite abreviações. Veja dois exemplos na Listagem 1. Listagem 1. Evite abreviaturas. Não poupe as palavras. public class Aluno { //COM ABREVIAÇÃO private String catAluno() { //implementação } //SEM ABREVIAÇÃO – O mais indicado private String cadastrarMatriculaDoAluno() { //implementação
2
} }
Como indicado no exemplo, os nomes devem refletir exatamente o significado da função do método. Deste modo, defina-os de forma objetiva e clara, de modo que a leitura seja simplificada. Além disso, evite as chamadas notações húngaras, que ocorrem quando o tipo do objeto vem junto do nome (ex: listaDisso ou mapaDaquilo). A nomenclatura das classes deve ser elaborada a partir de substantivos e não deve conter verbos. Diferentemente dos nomes dos métodos, que devem sim possuir verbos, pois representam uma determinada ação. Classes e métodos Tanto as classes quanto os métodos devem ter o menor tamanho possível. Quanto menores eles forem, mais fácil se tornará o seu entendimento. No próprio livro Clean Code existem algumas normas voltadas aos métodos informando que estes devem ter no máximo vinte linhas e cada linha de código precisa ter no máximo cem caracteres, ao o que a classe deve possuir entre duzentas e quinhentas linhas. No entanto, se o desenvolvedor tiver uma classe com 550 linhas, ou seja, se ele exceder 50 linhas, não é preciso reescrever todo o seu código. O fato de ter ultraado um pouco não significa que o profissional tenha que alterar toda a sua implementação para restringir-se às quinhentas linhas. Para isso existe o bom senso. Também é importante que métodos e classes tenham apenas uma função. Esta norma chama-se princípio da responsabilidade única ou, no inglês, Single Responsibility Principle. Dessa forma o entendimento torna-se muito mais rápido e preciso. Uma sugestão para detectar essa responsabilidade única é identificar se determinado trecho de código está realizando uma função diferente além da proposta. O trecho de código que realiza outra tarefa deve ser movido para um novo método. Além disso, busque evitar os métodos que recebem parâmetros demais. Métodos com muitos parâmetros acabam por se tornar confusos. A agem de muitos parâmetros indica justamente que o seu método está realizando coisas demais. E se for este o caso, os métodos devem possuir uma justificativa plausível. É importante entender que quando falamos em código limpo, deve-se ter em mente a ideia de que “Menos é sempre Mais”. Outra sugestão é seguir a norma do DRY – Don’t Repeat Yourself, que no português significa “Não repita a si mesmo”. Sendo assim, preste muita atenção à repetição de código. Evite a duplicidade reaproveitando os seus métodos. Ademais, é necessário prestar atenção no grau de coesão das classes. Como informado anteriormente, menos pode significar mais. Dessa forma, nossas classes devem fazer apenas a função descrita em seu nome. Um exemplo disso seria uma suposta classe Imprime, criada com o simples objetivo de imprimir. Portanto, ela deveria executar apenas esta função, não precisando se preocupar em estabelecer conexão com o banco de dados, salvar os arquivos ou verificar se o usuário tem permissão de o. Quando uma classe possui mais responsabilidades, além da que ela se propõe, ela se mostra claramente incoerente, pois cada classe deve cumprir uma única responsabilidade. Comentários 3
Evite o máximo possível fazer comentários. Se partirmos do princípio explicado anteriormente, já podemos eliminar alguns tipos desnecessários de comentários, pois teremos seguido todas as regras para a nomenclatura de nossas classes e métodos. Sendo assim, um comentário com o propósito de explicar a função de um método se torna desnecessário. No entanto, e se o projeto for um legado no qual se está realizando apenas uma manutenção, podemos inserir novos comentários? De forma alguma. Devemos apenas verificar se os comentários que já existem ainda fazem sentido em relação ao código do método. Caso contrário, é preciso alterá-los, deixando-os coerentes com o código novamente. Quando for necessário realizar manutenções no código de um projeto antigo, nunca se esqueça de alterar também o comentário do método, caso eles existam. Ao lermos um comentário, o correto seria não precisarmos olhar para o restante do código para entendê-lo. Caso precisemos voltar ao código depois de ver o comentário, então este último precisa ser alterado. Como diria o próprio Uncle Bob: “Qualquer comentário que faça você olhar para outras partes do código para entendê-lo, não vale nem os bits que ele consome”. Porém, há situações em que os comentários são necessários. Eles podem ser úteis em casos como, por exemplo, no aviso de consequências que um trecho de código possa vir a causar. Como uma lentidão causada no banco de dados e a recomendação de que se faça uma alteração da procedure usada. Ou seja, o comentário deve mostrar a real intenção por trás de uma decisão tomada, pois ocasionalmente fica difícil mostrar apenas com o nome do método o porquê daquele código ter sido feito de uma determinada maneira. Enfim, são raros os casos em que o comentário pode vir a ser necessário de fato. O ideal é evitarmos a sua utilização. Existem também os trechos de código comentados. No entanto, um trecho de código comentado deve ser algo deixado ali apenas por um breve momento, só servindo como uma rápida referência. Se o mesmo já não tem nenhuma utilidade, apague-o sem medo, pois os controles de versão existem para isso, resgatar uma versão anterior para que possa ser novamente utilizada. Formatação A formatação do código é muito importante porque é uma das formas como nós, desenvolvedores, nos comunicamos uns com os outros. A partir dela outros desenvolvedores poderão olhar para o código e entendê-lo com mais facilidade. Ela é, portanto, componente fundamental para a comunicação. A ordem dos métodos é outro fator essencial. Métodos com conceitos relacionados devem ficar verticalmente próximos. Assim, é criado um fluxo de leitura que melhora a legibilidade do código. Outro fator importante é o cuidado com a indentação do seu código. Esta não deve possuir mais do que dois níveis, como é o caso dos ifs mostrados na Listagem 2, que chegam a ter um terceiro nível. Caso isso ocorra, deve-se extrair o terceiro nível para outro método. Listagem 2. Atenção à indentação. Observe o erro comum na estruturação dos ifs. if(situacao == 1) { if(situacao == 2) { if(situacao == 3) { } }
4
}
É preciso cuidado também com relação aos espaçamentos dados no código. O espaçamento correto entre os operadores, parâmetros e vírgulas fazem uma grande diferença no momento da leitura e da compreensão do que foi codificado. Podemos ver um simples exemplo das duas percepções na Listagem 3. Listagem 3. Observe o uso correto do espaçamento. //SEM ESPAÇAMENTO public class CalculaDesconto { public void caucula(Integer val1,Integer val2){ Integer resultado=val1+val2; System.out.println(“O resultado é:”+resultado); } } //COM ESPAÇAMENTO public class CalculaDesconto { public void caucula(Integer val1, Integer val2) { Integer resultado = val1 + val2; System.out.println(“O resultado é:” + resultado); } }
Tratamento de erros O tratamento de erros é de suma responsabilidade do desenvolvedor. As coisas podem sempre dar errado e é nosso dever garantir que o nosso código possua o correto tratamento para cada situação. Neste contexto, é importante que o desenvolvedor dê prioridade ao uso de exceções, em vez de apenas retornar códigos de erro. O uso de códigos de erro pode causar certa confusão, pois o método que invocou determinada funcionalidade acaba tendo que se preocupar em tratar esses códigos retornados e esse processo pode ser facilmente esquecido. As exceções devem indicar a localização exata de um erro. Portanto, ao lançar uma exceção, use mensagens que realmente informem o erro ocorrido, mencione o que de fato aconteceu, o que estava tentando fazer e o porquê do erro. Além disso, evite usar exceções para regras de negócio. Sempre use-as para os erros inesperados, como NumberFormatException. Para os tratamentos de erros de negócio, não devemos criar exceções conforme o caso mostrado no exemplo da Listagem 4, pois são situações específicas e difíceis de serem previstas. Nestas situações, devemos preferir o uso de ifs, pois torna o nosso código mais organizado e de melhorar manutenção, como podemos constatar na Listagem 5. Listagem 4. Cuidado com o mau uso das exceções. Despesas despesasComFestas = new Despesas(); try { despesasComFestas = despesasDao.getFestas(); resultado += despesasComFestas.getSoma(); } catch (DespesasComFestasNotFoundException e) { resultado += despesasComFestas.getSomaParcial(); }
5
Listagem 5. O correto uso de ifs para o tratamento de erros de negócio. Despesas despesasComFestas = new Despesas(); despesasComFestas = despesasDao.getFestas(); if(despesasComFestas.getSoma() != 0) { resultado += despesasComFestas.getSoma(); } else { resultado += despesasComFestas.getSomaParcial(); }
Outro fato importante que também deve ser observado quanto ao tratamento de erros é o de nunca retornamos null nos nossos métodos, pois fatalmente poderemos causar uma exceção de NullPointerException. Tal exceção ocorre devido a um simples esquecimento, que seria o de tratar o trecho de código com o if (qualquerObjeto != null). Mas o tratamento com o uso do if causa uma enorme duplicação de código, devido à grande quantidade de lugares nos quais teríamos que repetir esse mesmo procedimento. Em vez disso, podemos optar por usar um pattern conhecido como Special Case Objects, que é um padrão responsável por criar objetos especiais justamente para atender a essa necessidade. Esse pattern consiste na criação de um objeto do mesmo tipo da interface, com métodos que retornem qualquer valor padrão, como vazio para um atributo do tipo String e zero para um atributo do tipo Integer. Um exemplo pode ser visto no cenário da Listagem 6. Nesta listagem uma classe denominada NullEmpregado, do mesmo tipo da interface Empregado, é criada com métodos que retornarão valores padrões ao invés de retornar null. Listagem 6. Usando o pattern Special Case Object. public interface Empregado { String nome(); String sobrenome(); } //Nosso objeto especial public class NullEmpregado implements Empregado { private static final String VALOR_PADRAO = “”; @Override public String getNome() { return VALOR_PADRAO; } @Override public String getSobrenome() { return VALOR_PADRAO; } } public class Diretor implements Empregado { @Override public String getNome() { return “Pedro;
6
} @Override public String getSobrenome() { return “Otávio”; } } public class Funcionario implements Empregado { @Override public String getNome() { return “joao; } @Override public String getSobrenome() { return “Augusto”; } } //Nossa classe de testes public final class Escritorio { Empregado empregado = new NullEmpregado(); public Escritorio() {} public Escritorio(Empregado empregado) { this.empregado = empregado; } public String imprimirNomeCompletoDoFuncionario() { return empregado.nome() + " " + empregado.sobrenome(); } public static void main(String[] args) { //Aqui o novo objeto não foi ado, o que nos retornaria NullPointerException, //mas com o uso do special case object isso não acontece. Escritorio escritorio = new Escritorio(); System.out.println(escritorio.imprimirNomeCompletoDoFuncionario()); escritorio = new Escritorio(new Funcionario()); escritorio = new Escritorio(new Diretor()); System.out.println(escritorio.imprimirNomeCompletoDoFuncionario()); } }
Neste exemplo da classe Escritorio podemos ver que ela também foi instanciada tendo um construtor vazio. Isso certamente acarretaria em NullPointerException na execução do método imprimirNomeCompletoDoFuncionario(), pois o mesmo necessita de informações oriundas de um Empregado. Contudo, essa exceção não ocorre porque Empregado já foi inicializado antes, com a classe NullEmpregado, garantindo assim o retorno de seus métodos nome() e sobrenome() com o valor vazio, ao invés de null. Testes unitários (TDD) “Um Desenvolvedor que não faz testes é como um cirurgião que não lava as mãos” (Robert C. Martin). Testes unitários são os responsáveis pela garantia do seu trabalho e da lógica que foi implementada no código. 7
O TDD (Test Driven Development) é uma das práticas mais conhecidas para a escrita de testes unitários. Com ele, escrevemos nosso código guiado pelos testes, ou seja, começamos pelos testes da funcionalidade antes da sua codificação de fato. Assim, nosso código já é validado antes mesmo de sua escrita. Existem três leis sobre o TDD, definidas por Uncle Bob, que servem como guia durante o processo de desenvolvimento. São elas: 1. Não se pode começar a escrever o código de produção até que se tenha criado um teste que apresente erro; ou seja, devemos sempre criar o teste primeiro; 2. Não se pode escrever mais testes do que sejam necessários para testar a condição do erro. É preciso preocupar-se em criar o mínimo de testes, desenvolvendo apenas o suficiente para testar a condição de erro; 3. Não se pode escrever mais código de produção do que o suficiente para compilar o teste unitário que apresentava erro. Robert C. Martin, no livro Clean Code, também recomenda o uso de cinco regras para realização dos testes unitários. A junção da primeira letra de cada um desses conselhos forma o acrônimo FIRST, que significa: · Fast – Os testes devem ser rápidos. Quando os testes são lentos, certamente o desenvolvedor não vai querer executá-los com frequência. Deste modo, deixamos de encontrar os problemas logo no início da codificação, o que permitiria solucioná-los de forma mais rápida e fácil; · Independent – Os testes não devem depender uns dos outros, ou seja, o resultado de um teste não deve ser condição para a execução do próximo teste. Eles precisam ser executados de forma independente e em qualquer ordem. Quando um teste depende do outro, o primeiro que falhar causará uma reação em cadeia, fazendo com que todos os outros testes abaixo na hierarquia produzam erro, dificultando diagnósticos e escondendo outros defeitos; · Repeatable – Os testes precisam estar aptos a serem executados repetidas vezes sem nenhuma intervenção. Eles não podem depender de nenhum serviço ou recurso externo que poderá estar indisponível em determinado momento. A execução dos testes deve sempre apresentar o mesmo resultado, não importando o ambiente em que sua aplicação esteja rodando, seja no ambiente de desenvolvimento ou no ambiente de produção; · Self-validating – Os testes devem ter uma saída, uma resposta booleana, não importando se o retorno do mesmo indicou sucesso ou erro. Não devemos depender de agentes externos para validar os nossos testes, como por exemplo, um arquivo de log. A resposta precisa ser imediata, ou verdadeiro ou falso; · Timely – O teste deve ser criado antes do código da função a ser testada. Quando optamos por criar várias funções sem os devidos testes, fica muito mais difícil lembrar depois os testes que devem ser implementados. Além disso, o desenvolvedor poderá vir a optar por criar um super teste, abrangendo todas as regras dessas funções adas, e isso certamente irá acarretar em uma lentidão na hora de sua execução. Outro problema é a grande dificuldade na hora de realizar a manutenção nesses testes, pois cada vez que determinada função inserida nele for alterada, precisaremos encontrar nesse super teste o ponto exato de verificação da função em questão.
8
Por fim, é importante ter em mente o fato de que, ao alterarmos um código, devemos nos lembrar de também alterar o seu teste. Modelo Anêmico x Modelo de Domínio Uma modelagem orientada a objetos visa ser uma representação fiel do mundo real. Essa representação se dá através do uso de classes que devem ter seus estados e comportamentos bem definidos. Numa abordagem técnica, dizemos que as classes (objetos) devem possuir propriedades, definindo o seu estado e métodos, que implementam seu comportamento. Partindo da ideia do que é a orientação a objetos, apresentaremos motivos para a não utilização da modelagem anêmica, visto que a mesma fere, por exemplo, o uso do encapsulamento no momento em que as suas classes de domínio são criadas apenas como um aglomerado de métodos Getters e Setters sem uso real, servindo apenas como métodos assessores para nossos atributos. Essas classes não possuem estado ou comportamento, indo justamente na contramão do que prega a orientação a objetos. Para a correta modelagem de nosso domínio, devemos considerar o uso do DDD (Domain-Driven Design), que prega a ideia de que classes de domínio devem possuir suas regras de negócio dentro delas mesmas, adquirindo assim estado e comportamento. Com isso, encapsulamos verdadeiramente a nossa implementação, podendo alterá-la sem prejudicar outras classes que a utilizem. Os problemas com o uso do Modelo Anêmico Quando pensamos em orientação a objetos, nos vem rapidamente a ideia de classes que representem o comportamento e estado de objetos do mundo real, como por exemplo, uma classe do tipo Carro, que possui o método andar(). Outro conceito fundamental da orientação a objetos é o do encapsulamento. Ele indica que não devemos expor os detalhes de implementação de nossos objetos. Isto é, as demais classes não precisam saber como é o procedimento de andar, mas apenas utilizar tal comportamento. Quando encapsulamos determinada implementação, podemos facilmente alterá-la e com isso não interferir em outras classes, pois não existirá outro código que dependa diretamente desses detalhes. Então, diante do que se sabe sobre uma boa modelagem de objetos, é fato que toda nossa implementação deveria partir desses princípios, mas infelizmente não é assim que acontece na prática. Quando optamos pelo uso do modelo anêmico, deixamos de lado as boas práticas da orientação a objetos porque os objetos do domínio nele criados não possuem qualquer comportamento. Assim, a nossa classe Carro deixa de ser a responsável pelo comportamento de andar, ando essa função para uma classe intermediária. Por esse motivo o modelo anêmico é hoje considerado um anti-pattern. Em um modelo desse tipo, getters e setters gerados a partir dos atributos declarados como privados dentro das classes de domínio não am apenas de métodos públicos chamados de assessores, onde sua única função é fornecer o a esses mesmos atributos privados. Na internet encontramos facilmente vasto material para iniciantes no desenvolvimento Java que disseminam o equivocado ensinamento de indicar que precisamos criar métodos assessores para trabalhar com esses atributos, como podemos ver no exemplo da Listagem 7. Listagem 7. Criando os métodos assessores, getters e setters no modelo anêmico. 9
public class Salario { private double liquido; private double bruto; public double getLiquido() { return liquido; } public void setLiquido(double liquido) { this.liquido = liquido; } public double getBruto() { return bruto; } public void setBruto(double bruto) { this.bruto = bruto; } }
A classe Salario possui métodos que ferem a ideia do encapsulamento, como por exemplo, o setLiquido(), que nos permite o o direto ao atributo liquido, alterando o seu valor a qualquer momento na implementação. Nesse caso não é interessante que o nosso atributo seja alterado de forma direta, já que para se obter o salário líquido, alguns cálculos de desconto são necessários, como é o caso do INSS e o IRPF. É importante não criar um getter ou um setter sem necessidade. Ao implementar qualquer método em uma classe, a sua função precisa ser clara e objetiva. Os getters e setters, quando criados, por diversas vezes não são invocados, e grande parte daqueles que estão sendo utilizados poderia ser substituída por métodos de negócio do próprio domínio. Essa prática de criá-los para todos os atributos declarados como privados nas classes de domínio vem do início da AWT, quando era recomendado criar getters e setters para serem invocados no preenchimento dos campos visuais da interface gráfica com o usuário. Essa prática deu origem ao termo JavaBean, que é uma classe com atributos privados que são ados por métodos públicos. Quando se permite esse tipo de prática, trechos como salario.setLiquido(salario.getBruto() – (salario.getBruto() * 0,15)) serão encontrados ao longo de toda a aplicação. E se por acaso o cálculo mudar, teremos que procurá-lo e alterá-lo por todo o nosso código. Uma alternativa para contornar esse problema seria criar uma classe que ficasse responsável pela lógica do cálculo, como o exemplo da classe ContraCheque, que pode ser vista na Listagem 8. Listagem 8. Classe intermediária responsável pelo negócio referente ao cálculo do Salário. public class ContraCheque { public double calcularSalario(Salario salario, double desconto) { if(salario == null) { throw new InvalidArgumentException(“salario não informado!“); } if(desconto == null) { throw new InvalidArgumentException(“desconto não informado!“); } return salario.getBruto() – (salario.getBruto() * desconto); } }
10
No entanto, essa solução ainda mostra outros problemas, pois a classe ContraCheque apresenta-se de forma procedural, isto é, possui validações executadas em sequência dentro do mesmo método que realizará o cálculo, ao invés de estarem em métodos específicos. Lembre-se, devemos sempre separar as responsabilidades. E o que temos neste caso são procedimentos diferentes sendo realizados juntos (validações e regra de negócio). Além disso, a classe também possui uma forte ligação com a classe Salario, conhecendo demais a sua implementação. Isto se dá a partir do momento em que a classe Salario é disponibilizada como parâmetro, dando assim o a todos os seus métodos públicos. Quando uma classe de domínio é invocada dessa forma, a quebra do encapsulamento pode ser claramente notada, pois comportamentos e estados da classe am a ser ados de forma direta. O ideal nesse caso é deixar de pedir em excesso o valor dos atributos, como faz o método getBruto(), que serve apenas para trazer o valor do atributo bruto e a partir desse valor, realizar o cálculo do salário líquido. Portanto, é importante definir claramente o que o objeto deve fazer, para que ele internamente seja responsável pelas suas próprias regras de negócio (como por exemplo, pagar o salário líquido). Este é justamente o princípio do “Tell, don’t ask” (“Diga, não peça”). Como este princípio não foi aplicado à classe Salario, esta simplesmente parece não ter qualquer responsabilidade no sistema. Sem estado ou comportamento, ela se mostra como um fantoche, com métodos sets e gets que servem apenas para fornecer e recuperar as informações da classe. Para dar alguma ação ao domínio Salario, foi necessária outra classe, a ContraCheque. Devido a isso, qualquer alteração no domínio também irá acarretar modificações na classe ContraCheque, porque ela conhece muitos detalhes da implementação de Salario. Na modelagem anêmica é dito que não devemos colocar qualquer lógica de negócio nos objetos de domínio. Contudo, o ideal é exatamente o contrário. Por que não deixar que cada objeto de domínio fique responsável pelas suas próprias regras de negócio, unindo assim a lógica de negócio aos dados? Dessa forma eliminaríamos os métodos que apenas am e modificam diretamente os atributos. O fato é que lógica de negócio e dados podem ser unidos de uma maneira simples, fazendo com que a classe de domínio e a ter seus próprios métodos de negócio, como mostrado na Listagem 9. Assim, também podemos remover os métodos desnecessários que apenas am e alteram o estado da classe de forma direta. Listagem 9. Atribuindo responsabilidade ao domínio Salario na visão do modelo de domínio. public class Salario { private double bruto; private double liquido; private double desconto; public Salario(double bruto, double desconto) { this.bruto = bruto; this.desconto = desconto; } private void validarAtributos() { if(this.bruto == null) { throw new InvalidArgumentException(“valor } if(this.desconto == null) {
11
bruto
não
informado!“);
throw
new
InvalidArgumentException(“desconto
não
informado!“);
} } private void calcularSalario() { validarParametros(); this.liquido = this.bruto – (this.bruto * desconto); } public double getLiquido() { calcularSalario(); return this.liquido; } }
Como pode ser observado, o método getLiquido() foi mantido, pois este faz parte do domínio e retorna o valor do salário líquido já calculado. Quanto ao método calcularSalario(), ele ficou responsável apenas por realizar a regra de negócio. A validação dos atributos foi removida para outro método, o validarAtributos(). Desse modo, temos as responsabilidades definidas de forma correta e qualquer alteração feita no cálculo do salário líquido, como por exemplo, a inclusão de um novo desconto, não causará quaisquer alterações numa outra classe que se utilize de getLiquido(). No momento em que a classe de domínio a a ter seus próprios métodos de negócio, ela deixa de ser uma mera estrutura de dados com Getters e Setters. Quando falamos em definição das responsabilidades, precisamos ressaltar que a mesma deve ser pertinente ao domínio. Também é importante enfatizar que o comportamento dentro dos objetos de domínio não deve contrariar a ideia da separação em camadas do projeto, abordagem tradicionalmente pregada pelo MVC (Model-View-Controller). Sendo assim, domínio e lógica devem ficar s à camada de modelo. O uso do DDD (Domain-Driven Design) Todo sistema nasce com o objetivo de resolver questões que envolvam determinado problema do mundo real. Para esse problema que será resolvido com a implementação do nosso sistema, damos o nome de Domínio. Para uma correta modelagem do domínio devemos considerar o uso do DDD (Domain-Driven Design). Este, conforme a definição do site DDDComunity.org, não se trata de uma tecnologia ou uma metodologia, mas de uma maneira de definir prioridades, que objetiva acelerar o desenvolvimento dos projetos de software que lidam com domínios complexos. De acordo com o DDD, é preciso que os desenvolvedores tenham um entendimento do domínio, não tão profundo quanto o dos analistas de negócio (que são os especialistas do domínio), mas uma compreensão suficiente para que eles possam desenvolver guiados pelo domínio. E para que isso aconteça, é muito importante criar uma forma de comunicação entre todos os que irão lidar com o domínio. Essa “conversa” deve ocorrer entre os membros da equipe, se tornando o ponto chave para que uma linguagem comum se estabeleça entre desenvolvedores e especialistas. Dessa forma, todos os membros da equipe irão criar, juntos, os termos ou vocábulos a serem utilizados. O conjunto de termos criados neste procedimento é conhecido como Linguagem Ubíqua. Definida uma terminologia comum e um entendimento das necessidades do domínio, os desenvolvedores já possuem um modelo a ser seguido para a implementação do código, que deve ocorrer respeitando a 12
sequência de desenvolvimento acertada nas conversas com os especialistas do domínio, item por item. Caso haja qualquer mudança no modelo, ela deverá ser refletida no código. Essa interação entre modelo e código nos mostra que com o uso do DDD ocorre uma mudança de ênfase. À medida que o modelo mantido pelos analistas sofre alterações, elas também precisam ser refletidas no código mantido pelos desenvolvedores, tanto através da criação e remoção de funcionalidades, quanto através da refatoração. Para os desenvolvedores e analistas de negócio interessados numa abordagem mais profunda sobre DDD, recomendamos a leitura do livro Domain-Driven Design, de Eric Evans. Neste artigo, o nosso objetivo ao analisar as técnicas de código limpo de Martin Fowler, foi oferecer auxílio ao programador para que ele desenvolva melhor suas funcionalidades no cotidiano, com o máximo de clareza e eficiência. Dessa forma, quando este código for manuseado por outro profissional, ele não terá dificuldades para continuar um trabalho que não foi iniciado por ele. Aliado às técnicas de código limpo, é muito importante atentar para a escolha de como implementar o seu modelo de negócio. Conforme constatado, a escolha pelo modelo anêmico fere as boas práticas do uso da orientação a objetos. Portanto, opte sempre pelo modelo de domínio. Por fim, recomendamos para a correta implementação do modelo de domínio, a prática do DDD, que tem como ponto principal estabelecer uma forma de comunicação entre desenvolvedores e analistas de negócio, para que ambos possam caminhar juntos no ciclo de desenvolvimento. Links DDD Community – Discussões sobre o assunto, diversos casos de estudo e exemplos do uso do DDD. http://dddcommunity.org Special Case Pattern, por Martin Fowler. http://martinfowler.com/eaaCatalog/specialCase.html Anemic Domain Model, por Martin Fowler. http://www.martinfowler.com/bliki/AnemicDomainModel.html Clean Code, escrito por Robert C. Martin. Domain-Driven Design, escrito por Eric Evans. Introdução à Arquitetura e Design de Software, escrito por Paulo Silveira, Guilherme Silveira, Sérgio Lopes, Guilherme Moreira, Nico Steppat e Fábio Kung.
13