Padrões de Desenho: Tudo o Que Precisas de Saber

Como engenheiros de software, uma das nossas principais responsabilidades é mapear o mundo real através de um software que aborde os seus problemas.

Visto que a maioria das aplicações permanecem funcionais após a sua criação, a flexibilidade é uma necessidade. Se assim não for, novos requisitos de vez em quando serão um grande desafio e uma tarefa árdua. 

Os padrões de desenho, também conhecidos por design patterns, vieram para te ajudar a responder a essas necessidades. 

O que é um Padrão de Desenho

Cada problema de design, num contexto de aplicação específico, tende a ter o seu padrão de identidade. Tal defeito num sistema pode comprometer uma grande solução, transformando-o numa solução não flexível, cara e difícil de manter. Geralmente, os novos requisitos irão comprometer outras funcionalidades. 

Um padrão de desenho é caracterizado por evidenciar e trabalhar em torno de tais falhas de design. Descreve também como as entidades dentro de um sistema devem interagir e ser expostas. Podemos encará-lo como um plano, que pode ser personalizado para resolver um problema de design recorrente no teu código.

Os padrões podem por vezes ser mal compreendidos como um pedaço de código que podes facilmente copiar e colar. Porém, estes devem ser entendidos como conceitos teóricos para resolver um determinado problema. 

Os padrões de desenho podem ser divididos em quatro categorias distintas:

  • Um nome que descreve os seus propósitos
  • Um problema à espera de ser resolvido
  • Uma solução que descreve como os componentes devem relacionar-se ou ser compostos
  • Uma coleção de consequências que um padrão de desenho pode trazer

É sempre uma questão de contrapartidas na escolha de um padrão de desenho em detrimento do outro. 

Tipos de Padrões de Desenho

Os padrões de desenho podem ser de três categorias diferentes:

  • Criação 
  • Estrutural
  • Comportamental 

Estas categorias diferem em termos de complexidade, nível de detalhe, e escalabilidade quando aplicadas a todo o sistema.

Estes padrões são tecnologicamente agnósticos, e podes aplicá-los facilmente a qualquer linguagem ou framework de programação.

Criação

Tal tipo de padrão de desenho fornece vários mecanismos de criação, aumentando a flexibilidade do código, e a sua reutilização. Sumariza o processo de instanciação, ao separar a forma como os objetos devem ser criados, compostos, e representados do código que neles se baseia. 

  • Factory Method – Apoia-se numa interface para criar entidades de uma classe pai. No entanto, permite alterar o tipo de entidades que uma subclasse pode criar.
  • Abstract Factory – Permite a produção de famílias de objetos relacionados sem especificar qualquer tipo específico.
  • Builder – Permite a criação passo a passo de tipos de objetos e representações.
  • Prototype – É possível copiar objetos existentes sem depender diretamente das suas classes.
  • Singleton –  Assegura que uma determinada classe tem apenas uma instância e um ponto de acesso.

Estrutural

Descreve como as entidades devem ser organizadas em estruturas maiores, mantendo-as flexíveis e eficientes.

  • Adapter – Permite a colaboração de entidades com interfaces incompatíveis. 
  • Bridge – Permite segregar uma entidade, ou um conjunto de entidades relacionadas, em diferentes hierarquias, abstrações, e implementação. Esta segregação permite trabalhar de forma independente. 
  • Composite – Descreve um conjunto de entidades que devem ser tratadas da mesma forma. Compõe entidades em estruturas de árvore, onde se pode solicitar a cada nó que execute uma determinada tarefa.
  • Decorator – Permite introduzir novos comportamentos, ao colocar a entidade dentro de um wrapper. 
  • Facade – Fornece uma interface e simplifica a utilização de uma dada biblioteca ou de um conjunto de frameworks complexas.
  • Flyweight – Este padrão de desenho tem a ver com a partilha de recursos, possibilitando a colocação de mais objetos na quantidade disponível de RAM. Tal objetivo é alcançado através da partilha de diferentes estados de objetos no sistema em vez de o manter dentro de cada objeto.
  • Proxy – Pode ser introduzido um comportamento adicional para ser executado antes ou depois de o pedido passar pela entidade original.

Comportamental

Tais padrão de desenho identificam padrões de interação comuns e aumentam a flexibilidade na realização desta comunicação. Ao fazê-lo, permitem que essas entidades falem facilmente umas com as outras, mantendo o loose coupling entre componentes.  

  • Chain of Responsibility –  Um dado conjunto de pedidos é atribuído a “handlers” específicos. Ou um determinado handler processa o pedido, ou simplesmente passa o pedido para o seguinte. 
  • Memento – Permite guardar o estado atual ou anterior de uma determinada entidade.
  • Template Method – Define um esqueleto de algoritmo a ser executado por outros. Tais passos dentro deste esqueleto devem ser anulados e definidos pelas suas subclasses.
  • Command –  Transforma um determinado pedido numa entidade que contenha toda a informação sobre tal pedido. Ao fazê-lo, permite mover os pedidos como argumentos de método através de camadas ou outros métodos. 
  • Observer –  Define uma subscrição pronta a ser subscrita por entidades interessadas que queiram ser notificadas sempre que um determinado evento ocorra.
  • Visitor – Utilizado sempre que temos de lidar com um  grupo de entidades do mesmo tipo. Permite mover a lógica operacional de uma dada entidade para outra.
  • Iterator – Fornece uma forma default de iterar através de um conjunto de entidades.
  • State  – Pode-se alterar o comportamento da entidade atual sempre que o seu estado interno for alterado.
  • Mediator – Poderá reduzir-se o número de dependências entre entidades. As entidades são forçadas a interagir através de uma entidade mediadora.
  • Strategy – É possível alterar o comportamento da entidade atual durante o run time. Com base num dado contexto, é realizada uma determinada estratégia.

Da Teoria à Prática

Depois de ter introduzido os conceitos teóricos, deixa-me agora dar-te um exemplo real da aplicação de padrão de desenho, ao falar-te de um problema que tive de enfrentar no trabalho. 

O Problema

Certa altura tinha um ticket onde o objetivo era o refactoring do nosso serviço de extração de ficheiros de dados. Visualmente refletia-se em ter de carregar um ficheiro, tal como um PDF ou um Excel. Depois do carregamento, precisávamos de extrair todos os dados do ficheiro e apresentá-los através de uma bonita interface ao utilizador. Todos sabíamos que um novo tipo de ficheiro viria na nossa direção num futuro sprint, reforçando a necessidade de um tal refactoring.

img-2

Diagrama mostrando a estrutura antes de aplicar qualquer solução.

Observámos ao longo do desenvolvimento que muitos algoritmos semelhantes estavam a ser criados, tornando a tarefa de novas adições mais complicada e menos flexível. Uma alteração à especificação, tal como uma alteração de uma restrição, iria necessitar de ser refeita igualmente em dois componentes diferentes. Em termos de legibilidade, se tivesse sete tipos diferentes de ficheiros a carregar, a probabilidade de faltar alguns passos cruciais poderia ser elevada. 

Felizmente, os padrão de desenho descrevem vários problemas que outros engenheiros enfrentaram durante as suas carreiras. Grande parte da argumentação é feita, deixando-te a tarefa de reconhecer a questão e aplicar a solução adequada.

A Solução

Enquanto investigava vários padrão de desenho, o Template Method Pattern parecia ser a solução para o tipo de desafio que estávamos a enfrentar. 

O mesmo sugere a existência de um esqueleto partilhado através de outras subclasses. Dentro deste esqueleto, podemos definir várias etapas, cujo comportamento pode variar com base no contexto. Nesses casos, uma subclasse deve reescrever a etapa e introduzir as suas especificidades sem alterar a estrutura global do algoritmo.

Perfeito, era mesmo disto que precisávamos!

Seguir esta abordagem permite-nos:

  1. Adicionar facilmente restrições que são automaticamente aplicadas a todos os tipos suportado;
  2. Seguir o princípio DRY (Don’t Repeat Yourself);
  3. Adicionar um novo tipo de ficheiro à nossa funcionalidade que retomaria a criação de uma nova classe e anularia alguns métodos. Rápido, flexível, e muito simples.

Tecnicamente, no final ficava assim:

img-4

A estrutura final.

Como podes ver, há um método modelo que utilizámos como ponto de partida. Além deste método modelo, que não deve ser alterado por subclasses, temos vários passos que as subclasses podem substituir em caso de necessidade.

Para implementar isto, seguimos o seguinte projeto:

  • Analisar o algoritmo duplicado para etapas comuns e únicas;
  • Criar uma classe abstrata onde o método modelo inclui todas as etapas comuns. Para os únicos, definir os seus métodos abstratos correspondentes;
  • Criar uma subclasse para cada variação do algoritmo; 
  • Anular os métodos abstratos com a lógica específica.

Padrão de Desenho: Considerações Finais

Existem muitas soluções para múltiplos problemas que se podem ter, e cada solução tem os seus prós e os seus contras. Depende sempre de ti e da tua equipa decidir a melhor abordagem para cada problema que encontras no caminho.

Identificar o problema e adaptá-lo a um determinado padrão é uma das coisas mais desafiantes que um engenheiro enfrentará durante a sua carreira. É tudo uma questão de prática e experiência. 

Conheço pessoas com mais de dez anos de experiência em engenharia que ainda lutam para identificar alguns deles. Por isso não te preocupes, porque o tempo e a experiência em código vão ajudar-te com isso.