Design Patterns: como você está construindo objetos complexos?
Os prazos estão encurtando. Precisamos ser mais ágeis. E mesmo quando só precisamos adicionar um contexto diferente para uma funcionalidade parecida, a atuação no código continua demorada e a reutilização da solução não pode ser trabalhada da melhor forma possível. E aquele código difícil de entender pra quem está chegando? Pois é. Ás vezes até erros ocorrem, ou mesmo a performance é afetada.
No fim, a experiência nos faz perceber que esses mesmos problemas aparecem com frequência, em mais sistemas, com mais desenvolvedores e a importância de se utilizar melhores práticas acaba entrando em foco. Então já que esses problemas são recorrentes, porque não padronizamos as soluções e disseminamos à comunidade?
Em 1995, um grupo de 4 pessoas conhecidos como Gang of Four (GOF): Rich Gamma, Richard Helm, Ralph Johnson e John Vlissides; fizeram isso. Eles publicaram o livro Design Patterns: Elements of Reusable Object para propor a comunidade padrões de projeto que podem contribuir no melhor uso da programação orientada a objetos. Isso aumenta a velocidade de desenvolvimento, torna a manutenção do código mais viável e modular.
Logo, o conhecimento desses padrões é importante para entender o que eles podem ajudar no sistema quando for o caso. E dentre os padrões existentes, vou abordar nesse artigo um que tem papel importante na criação de objetos complexos. Esse padrão é o Builder.
Então, vamos falar sobre o Builder!
Uma classe pode permitir várias representações de objeto. E para construí-los nesse contexto, é comum definirmos um construtor com vários parâmetros de entrada, que muitas vezes uns são inúteis em algumas representações e precisam ser preenchidos com valores default.
Ainda para solucionar o problema anterior, acabamos definindo vários construtores para adaptarmos parâmetros diferentes, o que as vezes pode confundir o desenvolvedor sobre qual usar.
Com base nisso, surge o padrão Builder como padrão de criação. Cujo objetivo é…
…separar a construção de um objeto complexo de sua representação para que o mesmo processo de construção possa criar representações diferentes.
- Gang of Four
Além da criação de representações diferentes de um mesmo objeto, implementá-lo ajuda também na questão de manter a complexidade sob controle, pois classes com muitas tarefas são mais difíceis de entender dificultando na manutenção e evolução do sistema. A lógica assim da criação do objeto fica apartada, dedicando a responsabilidade a classes específicas, mantendo alta coesão e modularidade.
Normalmente, no padrão Builder temos os seguintes tipos de classes:
- A interface ou classe abstrata builder;
- A classe de implementação da interface builder para a criação das partes do objeto;
- O director (diretor) que chama os métodos do builder para gerar o objeto final;
- E o product (produto), que representa o objeto.
Segue o diagrama UML dessas classes conforme apresentado no livro da GoF:
A seguinte estrutura apresentada pelo refactoring.guru detalha mais essas classes e mostra como se dá na construção do objeto:
Let’s Code!
O exemplo que vou abordar é um projeto console desenvolvido em C# para a construção de labirintos que varia conforme nível de dificuldade desejado. Para tanto, começamos com a classe abstrata do Builder (que poderia ser adotado também interface nesse caso):
Nela definimos todos os passos que serão implementados nas diferentes representações do Labirinto. Implementei então com base nessa classe, o Labirinto Padrão (classe LabirintoPadraoBuilder):
E o Labirinto Difícil (classe LabirintoDíficilBuilder):
Veja que essas classes podem ser facilmente modificadas para novas configurações desses labirintos. Além de ser possível adicionar um novo builder para outra representação futuramente.
Segue a classe Labirinto com os atributos definidos e um método para exibição desses valores:
Na classe Director foi definido um método de construção que passa pelas etapas de criação do builder desse objeto.
E para vermos funcionando, construí os dois labirintos no método main exibindo seus valores ao final:
Segue a saída no console:
Caso a saída seja diferente para as representações desse objeto, esse método pode ser implementado também no builder. Mas a ideia no geral é isolar a lógica de criação dos objetos das regras de negócio do sistema. E tornar a construção mais reutilizada também.
Há alguns exemplos atualmente na comunidade que não utilizam o Director, sendo chamados diretamente os métodos dos Builders para a construção desses objetos.
Esse encadeamento é possível pois cada método retorna a própria classe builder referida. Essa versão do Builder foi proposta por Joshua Bloch na segunda edição do livro Effective Java.
Bom, ficamos por aqui. Até a próxima!
Os exemplos apresentados podem ser acessados no meu GitHub pelo seguinte link:
Referências
Design Patterns: Elements of Reusable Object-Oriented Software. Por Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides.
Refactoring Guru: Builder. https://refactoring.guru/design-patterns/builder.
Engenharia de Software Moderna: 6- Padrões de Projeto. https://engsoftmoderna.info/cap6.html