Padrões de projeto, criados por Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides e publicado no livro “Design Patterns: Elements of Reusable Object-Oriented Software”, são soluções típicas para problemas comuns em projetos de software.
Provavelmente, durante o desenvolvimento de suas aplicações você já se deparou com partes do código que viviam se repetindo, não no código em si, mas nos seu projetos em geral, por exemplo, criar uma classe separada para fazer as queries do banco de dados, ou talvez, uma classe responsável por “fabricar” um objeto.
Justamente para isso temos os Design Patterns, você não precisa pensar como reinventar a roda, apenas compreender o problema e ver se já existe alguma solução para ele. Dentro dos Design Patterns, temos algumas divisões a serem consideradas:
- Padrões Criacionais: Tratam da criação de objetos, ajudando a criar instâncias de classes de maneira adequada. Eles abstraem o processo de instanciamento, facilitando a criação de objetos de forma flexível e escalável.
- Padrões Estruturais: Lidam com a composição de classes e objetos, facilitando o design de estruturas mais complexas. Eles ajudam a garantir que a estrutura do sistema seja eficiente e flexível.
- Padrões Comportamentais: Focam na interação entre objetos e a atribuição de responsabilidades, facilitando a comunicação entre objetos e o fluxo de controle no sistema.
Hoje, focaremos em apenas um padrão comportamental, o Chain of Responsability
Chain of Responsability – A corrente de responsabilidades
Também conhecido como CoR, o Chain of Reponsability é um padrão comportamental que permite você passar “pedidos” em uma corrente de “gerenciadores” (ou handlers). Cada handler possui uma ação, processar se o “pedido” passa ou não a diante.
Problema
Imagine um cenário onde uma empresa de software oferece suporte técnico aos seus clientes. O suporte é estruturado em vários níveis:
- Suporte Nível 1: Trata questões básicas e comuns, como problemas de login ou perguntas frequentes.
- Suporte Nível 2: Lida com problemas técnicos mais complexos que o Nível 1 não pode resolver.
- Suporte Nível 3: Trata de questões críticas e especializadas que necessitam de intervenção de desenvolvedores ou especialistas.
Os clientes enviam seus pedidos de suporte através de um sistema de tickets, e esses tickets precisam ser processados e direcionados ao nível de suporte apropriado.
Vamos imaginar uma implementação onde cada nível de suporte verifica individualmente se pode ou não tratar o problema e, se não puder, envia o ticket manualmente para o próximo nível. O código pode ser algo como:
- Suporte Nível 1 recebe o ticket e verifica se pode resolver.
- Se sim, resolve e responde ao cliente.
- Se não, envia o ticket manualmente para o Suporte Nível 2.
- Suporte Nível 2 recebe o ticket e faz o mesmo processo.
- Suporte Nível 3 faz o mesmo se o ticket chegar até ele.
Provavelmente você já imaginou a solução para esse problema, simplesmente vários “if/else” e pronto, problema resolvido. Mas, vamos falar mais sobre isso.
Problemas dessa Abordagem
Obviamente, lotar o seu código de “if/else” não é uma boa prática, imagina que você tenha que adicionar mais 10 níveis de suporte, vai adicionar mais 10 “if/else” ? Ou quem sabe, o suporte nível 2 tem uma nova regra de negócio que vai enviar um e-mail para o suporte de nível 3. Além de tudo isso, imagine testar de forma unitário tudo isso, difícil né?
Aqui temos mais alguns problemas desse resolução:
- Alta Complexidade e Acoplamento: Cada nível de suporte precisa saber da existência e do método de envio para o próximo nível. Isso cria uma forte dependência entre os níveis, dificultando a manutenção e a adição de novos níveis de suporte no futuro.
- Duplicação de Código: Cada nível de suporte tem lógica repetitiva para verificar se pode resolver o ticket e, em caso negativo, enviá-lo ao próximo nível.
- Dificuldade de Manutenção: Alterar a forma como os tickets são processados em um nível exige mudanças em todos os níveis subsequentes. Adicionar ou remover um nível também é complicado.
Implementação Correta com Chain of Responsibility
Agora uma boa prática para resolver esses problemas, podemos usar o padrão Chain of Responsibility. Aqui, cada nível de suporte (manipulador) apenas precisa saber como processar o ticket ou passá-lo adiante, sem saber detalhes sobre os outros níveis.
Passos para Implementação Correta
- Criação de uma Interface de Manipulador:
- Define um método para processar o ticket e uma referência para o próximo manipulador na cadeia.
- Implementação dos Manipuladores Concretos:
- Cada nível de suporte implementa a lógica para processar o ticket.
- Se um nível não puder resolver o problema, ele simplesmente passa o ticket para o próximo manipulador.
- Configuração da Cadeia:
- Os manipuladores são encadeados na ordem correta (Nível 1 -> Nível 2 -> Nível 3).
Benefícios
Agora, temos classes separadas para cada problema, facilitando o incremento de lógicas mais complexas e não precisando lotar nosso código de “if/else”, e claro, facilitando na hora de escrever testes, já que podemos testar cada classe de forma separada.
Aqui temos mais algumas vantagens:
- Baixo Acoplamento: Os manipuladores (níveis de suporte) estão desacoplados entre si. Cada um apenas conhece seu sucessor imediato, facilitando a manutenção e a extensão do sistema.
- Reutilização e Clareza: A lógica para processar tickets está isolada em cada manipulador, evitando duplicação de código e tornando o sistema mais claro e modular.
- Flexibilidade: É fácil adicionar ou remover níveis de suporte. Basta alterar a configuração da cadeia sem modificar a lógica interna de cada manipulador.
Implementação com PHP
Agora vamos para a parte boa, como implementar esse Pattern utilizando uma linguagem de programação. Nesse exemplo, estarei utilizando o PHP, mas, fique à vontade para utilizar uma linguagem da sua preferência, basta ela ter suporte a orientação a objetos que você já vai conseguir implementar.
Primeiro, como boa prática, vamos criar a interface Handler que define o método handle que será implementado pelas classes concretas:
interface Handler {
public function handle($request);
}
Em seguida, vamos criar algumas classes concretas que implementam a interface Handler. Cada classe será responsável por aprovar despesas até um certo limite.
class Supervisor implements Handler
{
private $nextHandler;
public function setNext(Handler $handler)
{
$this->nextHandler = $handler;
}
public function handle($request)
{
if ($request <= 1000) {
echo "Despesa de $request aprovada pelo Supervisor.\n";
} else if ($this->nextHandler !== null) {
$this->nextHandler->handle($request);
} else {
echo "A despesa de $request excede a alçada dos Supervisores.\n";
}
}
}
class Manager implements Handler
{
private $nextHandler;
public function setNext(Handler $handler)
{
$this->nextHandler = $handler;
}
public function handle($request)
{
if ($request <= 5000) {
echo "Despesa de $request aprovada pelo Gerente.\n";
} else if ($this->nextHandler !== null) {
$this->nextHandler->handle($request);
} else {
echo "A despesa de $request excede a alçada dos Gerentes.\n";
}
}
}
class Director implements Handler
{
public function handle($request)
{
echo "Despesa de $request aprovada pelo Diretor.\n";
}
}
Agora, vamos criar um exemplo de uso do padrão Chain of Responsibility:
$supervisor = new Supervisor();
$manager = new Manager();
$director = new Director();
$supervisor->setNext($manager);
$manager->setNext($director);
$supervisor->handle(800); // Despesa de 800 aprovada pelo Supervisor.
$supervisor->handle(3500); // Despesa de 3500 aprovada pelo Gerente.
$supervisor->handle(10000); // A despesa de 10000 excede a alçada dos Gerentes.
Neste exemplo, o Supervisor pode aprovar despesas de até 1000, o Gerente pode aprovar despesas de até 5000, e o Diretor pode aprovar despesas acima de 5000. Se uma despesa exceder o limite do Diretor, ela não será aprovada. O padrão Chain of Responsibility permite que cada handler decida se pode lidar com a solicitação ou passá-la adiante.