Design Patterns: O que são, importância, categorias, padrões populares e quando usar. (Parte 1)
O que são Design Patterns?
Vamos alinhar as coisas. Quando falamos de design patterns, ou "padrões de projeto", não estou me referindo aos "padrões" como os melhores, ou mais definitivos, de se fazer design de software.
O termo "padrão" pode ser traduzido em 3 palavras diferentes para o inglês: "standard", "default" ou "pattern". Muitos entendem os "Padrões" no sentido de "Standard", ou seja, um padrão oficial, uma definição definitiva sobre algo. Mas na verdade é que estamos falando de "Patterns": algo que se repete muito, independente se é bom ou ruim, certo ou errado. E que está longe de ser uma definição definitiva.
Nunca trate um design pattern como uma regra. É apenas uma alternativa/sugestão de resolver um problema. E acredite ou não, praticamente todos os seus problemas de design/software já foram resolvidos.
O livro mais famoso, e o precursor, sobre esse assunto foi o "Design Patterns: Elements of Reusable Object-Oriented Software", escrito por Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides, também conhecidos como "Gang of Four" (GoF). Publicado em 1994. E o principal responsável por disseminar esse termo "Design Patterns" e também por gerar essa má interpretação sobre "Patterns".
O livro "Design Patterns", como todos os outros desse assunto, é apenas um CATÁLOGO de patterns e não um manual de como fazer design. Patterns que os autores viram em diversos projetos, e simplesmente deram um nome para eles. Só isso, nada mais, nada menos.
Sua importância
Talvez tenha soado um pouco desnecessário. Mas não é. Só quis colocar esse freio no início, pois daqui pra frente você vai ver como é necessário aprender sobre eles e talvez ache que seja a solução para todos os seus problemas. E infelizmente não existe isso em desenvolvimento de software.
De uma forma mais otimista, design patterns são estratégias de desenvolvimento que transformam desafios complexos em soluções elegantes. E sim, você deve usá-los.
Dificilmente você está resolvendo algo que já não foi resolvido antes. Padrões pré-definidos para resolver problemas comuns e recorrentes são um cenário ideal para usar design patterns.
Essa comunicação padronizada vejo que na prática é o que mais ajuda. Quando o "Pattern" é conhecido por todos, a comunicação e entendimento do código fica muito mais fácil. Não importa se é estagiário, júnior, pleno, sênior, staff engineer, etc. Todos vão entender o que está acontecendo ali.
Design de Software e Design Patterns
Antes de falarmos sobre os padrões em si, vamos deixar todo mundo na mesma página definindo o que é esse tal de design.
Arquitetura de software vs Design de software é um assunto "polêmico". Não vou me aprofundar nesse artigo. Mas simplificando muito, design de software se refere a como o software é estruturado a nível de código. E arquitetura de software é sobre componentes de alto nível como linguagens, bancos de dados, servidores, e suas interações. Clara, isso foi uma super simplificação. Mas por enquanto é isso que você precisa saber. Design de software é sobre código.
Dito isso, temos alguns princípios de design de software que são de mais alto nível, coisas como: SOLID, Ports e Adapters, Clean Architecture, etc. Esses princípios são mais abrangentes, são ideias, conceitos gerais e que podem ser aplicados a qualquer projeto.
Quando falamos de design patterns, é algo mais específico. Implementação real de códigos. Coisas como: como criar um objeto, como organizar classes, como estruturar um código, etc. Existem alguns que ajudam até na definição de arquitetura, mas a maioria é sobre design de software mesmo.
Categorias de Design Patterns
Se for para você aprender algo nesse artigo, que seja o seguinte:
Todo Pattern existe para um propósito. Entenda o propósito antes da implementação.
Vou repetir, a intenção é mais importante que a implementação. Conforme você for estudando, você perceberá que muitos patterns são parecidos, e a diferença está na intenção.
A intenção é o que vai fazer você escolher um pattern ou outro. E não a implementação.
A intenção de um pattern começa na sua "categoria", ou melhor, que tipo de problema ele ataca.
A primeira categoria é a mais simples, e a mais fácil de entender. É a categoria Padrões Criacionais. Padrões que ajudam na criação de objetos de forma flexível e reutilizável.
O segundo tipo é Padrões Estruturais. Padrões que ajudam na organização de classes e objetos para formar sistemas mais modulares.
E por último temos os Padrões Comportamentais. Padrões que ajudam na definição da interação entre objetos e a distribuição de responsabilidades entre eles.
Essas categorias foram definidas no livro do "Gang of Four" (GOF) citado anteriormente. Acaba sendo a base para a maioria dos livros e artigos sobre o assunto. Tem outras categorias, mas essas são as mais conhecidas e utilizadas. Na prática, não importa tanto a categoria, mas sim o objetivo/intenção.
De novo, foque sempre na intenção.
Exemplo de Intenções diferentes
Exemplificar é a melhor forma de entender. Vou citar alguns padrões que são muito utilizados, porém com intenções completamente diferentes. Não se preocupe em decorar os nomes ou funções. O foco aqui é entender a diferença na intenção de cada um deles.
Um bem conhecido e muito polêmico é o Singleton. Uma hora ele é amado, outra hora ele é odiado. Outro momento vou falar só sobre ele. Mas por enquanto, o que você precisa saber é que ele é um padrão criacional. Ele tem a intenção de garantir que uma classe tenha apenas uma instância na sua aplicação. Na qual conseguimos trabalhar com recursos compartilhados. A ideia é: como eu consigo criar um objeto, se eu precisar criá-lo novamente, uma nova instância não será criada, e sim utilizar a mesma instância já criada anteriormente. Isso para evitar inconsistências no sistema, ou economia de recursos. Por exemplo, em uma instância de servidor de cache. Você não quer criar uma instância nova toda vez que for usar. Isso vai acabar causando inconsistência de dados. Serve para banco de dados também. Você não quer criar uma nova conexão toda vez que for fazer uma consulta. Você quer reutilizar a mesma conexão. Tem alguns que não gostam de usar o Singleton. Mas isso é para outrora. O que você precisa saber é que ele é um padrão criacional, e a intenção dele é garantir que uma classe tenha apenas uma instância na sua aplicação.
Outro padrão é o Adapter. O Adapter é um padrão estrutural. A intenção dele é adaptar. Serve para permitir que duas interfaces incompatíveis possam trabalhar juntas. É muito útil, por exemplo, quando você precisa integrar sistemas legados com sistemas novos ou quando uma biblioteca de terceiros não segue o formato que sua aplicação espera. Um exemplo bobo é o das tomadas: imagine que você tem um aparelho com plugue antigo e uma tomada nova. O adaptador permite que você conecte o aparelho antigo na tomada nova, sem precisar modificar o aparelho ou a tomada. A implementação não é relevante agora. Foque na intenção.
Outro padrão é o Strategy. O Strategy é um padrão comportamental. A intenção dele é definir uma família de algoritmos, colocar cada um deles em uma classe separada e tornar seus objetos intercambiáveis. O exemplo clássico: imagine que você tem um sistema de pagamento que pode usar diferentes métodos: cartão de crédito, boleto, pix, etc. Com o padrão Strategy, você cria uma interface de pagamento e implementa o mesmo método em classes separadas. Ou seja, 3 classes diferentes, todas com um método do mesmo nome, mas com implementações diferentes. Dessa maneira, a classe responsável pelo pagamento pode ser escolhida em tempo de execução. Isso é útil quando você tem várias maneiras de fazer a mesma coisa, mas quer manter o código limpo e organizado.
De novo, a implementação não é relevante agora. Entenda a intenção dele.
Nesses 3 exemplos, é muito fácil perceber a diferença entre eles, pois são de categorias diferentes. Mas quando lidamos com padrões da mesma categoria, a diferença é mais sutil.
Por exemplo, Adapter e Facade: Ambos envolvem "encapsular" código. Ambos fornecem uma interface para outras classes, mas o Adapter traduz interfaces incompatíveis, como o exemplo da tomada. Já o Facade simplifica uma interface complexa, tornando-a mais fácil de usar e entender, escondendo a complexidade interna.
Se você não entendeu a diferença, não se preocupe. Isso é normal. Se você estudar da maneira correta, você vai entender. E de novo, estudar corretamente é entender a intenção.
Quando usar Design Patterns?
Sendo direto, não tem resposta para essa pergunta. Mas vou levantar algumas considerações que devem ser levadas em conta na hora da escolha.
Foque no seguinte: um Pattern foi feito para resolver um problema. Se você tem um problema, e esse problema parece ser tão comum, é provável que já exista um padrão que pode resolver.
O primeiro passo não é pensar no Pattern, e sim no problema. Com base no problema, você vai verificar os possíveis padrões. Então, se o seu código está ficando cada vez mais amarrado, se você está com problemas de modularidade, extensibilidade, toda vez tem problemas com responsabilidades únicas, toda vez que você mexe em um código afeta outro, etc. Provavelmente existem padrões que podem te ajudar com isso.
E óbvio, não precisa decorar a implementação de todos os patterns. Você nem vai usar todos eles. Você precisa conhecer os patterns e seus objetivos, mas não precisa decorar. O que você precisa saber é que provavelmente existe um padrão para resolver o seu problema. E a partir disso, você vai pesquisar e estudar o padrão que pode te ajudar.
Outro ponto crucial é a frequência do problema no sistema. Se só um pedaço do seu sistema tem tal problema, talvez não valha a pena implementar um padrão. Por mais que veja que caberia um padrão ali, acredito que não vale a pena implementar um padrão só para resolver um problema específico. O custo de implementação deve ser levado em consideração. Criar interfaces, classes, etc., pode gerar um overhead desnecessário.
Se o seu código não tem reuso dentro do sistema, pense se vale a pena implementar um padrão ou se é melhor manter a simplicidade.
Se não tem muita manutenção nesse código, talvez não compense investir tempo em um padrão.
Leve todos esses pontos em consideração. E não tenha medo de não usar um padrão. Não é porque existe um padrão que você tem que usar. O padrão é uma alternativa, e não uma regra.
Evite a "Paternite"
A "Paternite" é um termo utilizado quando um desenvolvedor aplica esses padrões a todo momento, e principalmente quando não há necessidade.
A simplicidade é a arte mais elegante de todas. Use os patterns quando necessário. Esse necessário é difícil de definir, eu sei. E parece que quanto menos experiente você é, mais você tende a usar patterns.
Antes de implementar, busque conversar com alguém que entenda do padrão e verifique se é necessário para aquele caso.
Mas é quase inevitável, todo mundo passa por essa fase. Com o tempo você pega esse feeling. Você vai perceber quando é necessário e quando não é. E isso só vem com tempo de prática.
Conclusão
No final do dia, nosso objetivo é deixar o código mais legível, consequentemente mais fácil de manter e escalar. Se algum pattern te ajuda a fazer isso, use-o. Se não, não use. O difícil é tomar essa decisão.
Com o tempo, você vai perceber que o bom uso dos patterns está em saber combiná-los de forma estratégica.
Referências
Esses livros são dos tipos que não precisam ser lidos do começo ao fim. Você pode ler um capítulo ou outro e escolher o que mais se aplica para o seu momento. Tenho os dois e li alguns capítulos apenas.
-
Livro: Design Patterns: Elements of Reusable Object-Oriented Software (Padrões de Projeto Soluções Reutilizáveis de Software Orientados a Objetos).
-
Livro: Patterns of Enterprise Application Architecture. (Padrões de Arquitetura de Aplicações Corporativas).
-
Site: https://refactoring.guru/design-patterns