Padrões de Projetos com PHP: Chain of Responsibility

O padrão de design Chain of Responsibility PHP é uma técnica fundamental no desenvolvimento de software. Ele permite que um pedido passe por uma cadeia de possíveis processadores até que algum deles o trate efetivamente. Dessa forma, este padrão é especialmente valioso para a construção de sistemas flexíveis e desacoplados, onde a lógica de processamento pode ser alterada dinamicamente e distribuída entre vários objetos. Neste artigo, exploraremos como implementar esse padrão Chain of Responsibility PHP.

Para saber mais sobre a origem e os fundamentos do Chain of Responsibility, você pode consultar o GoF Design Patterns que oferece uma introdução completa.

O que é um Padrão Comportamental?

O Chain of Responsibility PHP, por exemplo, exemplifica um padrão comportamental clássico. Esse padrão faz parte de uma categoria de padrões de design que, por sua vez, concentra a interação e a delegação de responsabilidades entre objetos e classes. Além disso, esses objetos organizam e passam as tarefas ou solicitações ao longo da cadeia, enquanto cada um deles tem a capacidade de processar ou, se necessário, encaminhar a solicitação adiante. Consequentemente, esse padrão facilita a colaboração entre objetos, promovendo assim uma comunicação eficaz e, ao mesmo tempo, evitando a criação de dependências rígidas entre eles.

Leia mais sobre padrões comportamentais no site oficial do PHP.net.

Conceito Básico do Chain of Responsibility

O Chain of Responsibility, por sua vez, possibilita que múltiplos objetos tratem uma solicitação, enquanto forma uma sequência onde cada objeto tem a oportunidade de processar o pedido. Além disso, o padrão também se baseia na ideia de que cada objeto na cadeia pode, a qualquer momento, decidir se processa a solicitação ou se a encaminha ao próximo objeto. Dessa maneira, ele promove grande modularidade e flexibilidade no manejo das operações.

Vantagens do Chain of Responsibility

  • Desacoplamento: O emissor da solicitação não precisa saber quem trata o pedido, além disso, não precisa saber como alguém o processa.
  • Flexibilidade: O desenvolvedor, por sua vez, pode facilmente adicionar novos tratadores à cadeia sem precisar modificar o código existente.
  • Distribuição de responsabilidades: O padrão, consequentemente, facilita que diferentes objetos compartilhem responsabilidades.

Exemplo Prático: Sistema de Aprovação de Compras

Por exemplo, imagine um sistema onde as solicitações de compra passam por várias verificações antes que alguém as aprove. Nesse sistema, cada verificação é representada por um handler.

  • Handler: Esta classe abstrata, primeiramente, implementa o método setNext() para encadear manipuladores e, além disso, define o método abstrato handle() para tratar as solicitações. Por isso, as classes que herdam de Handler implementam esse método.
  • ConcreteHandler: Classes como GerenteAprovacaoHandler, DiretorAprovacaoHandler e CIOAprovacaoHandler estendem a classe abstrata Handler. Dessa forma, cada uma dessas classes implementa o método handle(). Nesse contexto, cada classe pode tratar parte da solicitação ou, alternativamente, passá-la ao próximo manipulador na cadeia.

Como é o fluxo de dados entre os objetos?

  • Início da Cadeia: O cliente envia uma solicitação ao primeiro manipulador na cadeia.
  • Processamento de Solicitações:
  1. Primeiro, cada manipulador verifica se pode tratar a solicitação.
  2. Se um manipulador pode tratar a solicitação, ele o faz e pode decidir se passará a solicitação adiante.
  3. Caso contrário, ele simplesmente passa a solicitação para o próximo manipulador na cadeia.
  • Término do Processamento:
  1. Se nenhum manipulador na cadeia puder tratar a solicitação, o sistema pode descartar a solicitação ou tratá-la de uma maneira padrão, dependendo da implementação.
  2. Por outro lado, se a cadeia conseguir tratar a solicitação em qualquer ponto, o processamento termina conforme a lógica definida.

Este fluxo mostra claramente como cada handler na cadeia tem a oportunidade de processar a solicitação. Se não puderem lidar com a solicitação por conta própria devido ao valor, passam a solicitação para o próximo handler na cadeia. No exemplo prático:

  • Início do Processo: O cliente envia uma solicitação (request) ao objeto GerenteAprovacaoHandler.
  • Verificação pelo Gerente:
  • O GerenteAprovacaoHandler verifica se o valor da solicitação é menor que 1000.
  • Se sim, o gerente aprova a solicitação e o processo termina.
  • Caso contrário, o GerenteAprovacaoHandler envia a solicitação ao DiretorAprovacaoHandler para mais verificações.
  • Verificação pelo Diretor:
  • O DiretorAprovacaoHandler avalia a solicitação.
  • Se o valor da solicitação é menor que 10000, o diretor aprova a solicitação e o processo termina.
  • Se o valor é igual ou maior que 10000, o DiretorAprovacaoHandler envia a solicitação ao CIOAprovacaoHandler.
  • Verificação pelo CIO:
  • O CIOAprovacaoHandler aprova a solicitação independentemente do valor. Assim, o processo termina com a aprovação do CIO.

Definição da Classe Abstrata Handler

Primeiramente, ao projetar a classe abstrata Handler no padrão Chain of Responsibility, é importante que o desenvolvedor decida como encadear os manipuladores. Existem duas formas principais de atribuir os manipuladores concretos: através de um método ou diretamente pelo construtor. Dependendo da abordagem que o desenvolvedor escolher, ele precisará ajustar a classe abstrata para dar suporte à respectiva forma de encadeamento.

Abordagem 1: Atribuição via Método

Nesta abordagem, o desenvolvedor atribui o próximo manipulador na cadeia durante a execução, utilizando o método setNext(). Essa estratégia proporciona flexibilidade, pois o desenvolvedor pode configurar ou modificar a cadeia dinamicamente.

A classe Handler para essa abordagem inclui o método setNext(), permitindo que o desenvolvedor atribua o próximo manipulador manualmente:

abstract class Handler
{
    protected $nextHandler;

    // Método para definir o próximo manipulador na cadeia
    public function setNext(Handler $handler): Handler
    {
        $this->nextHandler = $handler;
        return $handler;
    }

    // Método abstrato que as classes concretas devem implementar
    abstract public function handle(Request $request): ?string;
}

Neste caso, o desenvolvedor configura o próximo manipulador usando o método setNext(), permitindo que o encadeamento dos manipuladores ocorra conforme necessário em tempo de execução. Essa abordagem é útil quando a cadeia pode mudar ou crescer durante o ciclo de vida da aplicação, oferecendo maior flexibilidade.

Abordagem 2: Atribuição via Construtor

Por outro lado, na atribuição via construtor, o desenvolvedor define o próximo manipulador no momento da criação do objeto. Com essa abordagem, o desenvolvedor configura a cadeia de responsabilidade diretamente na instância dos manipuladores, garantindo que ela permaneça estável e imutável ao longo do tempo.

Para suportar essa abordagem, a classe abstrata Handler deve receber o próximo manipulador como um parâmetro do construtor, em vez de utilizar o método setNext(). A seguir, veja como o desenvolvedor pode ajustar a implementação da classe abstrata Handler para essa abordagem:

abstract class Handler
{
    protected $nextHandler;

    // Construtor que aceita o próximo manipulador na cadeia
    public function __construct(Handler $nextHandler = null)
    {
        $this->nextHandler = $nextHandler;
    }

    // Método abstrato que as classes concretas devem implementar
    abstract public function handle(Request $request): ?string;
}

Com essa implementação, o próximo manipulador é passado diretamente no momento da criação do objeto. Isso garante uma cadeia de responsabilidade mais rígida, onde o encadeamento não pode ser alterado facilmente após a criação.

Qual abordagem escolher?

A escolha entre essas duas abordagens depende diretamente das necessidades do sistema:

Quando Usar a Atribuição Via Método

Se o sistema exige flexibilidade, onde a cadeia de manipuladores pode mudar ou crescer dinamicamente, a atribuição via método se torna a mais indicada. Além disso, essa abordagem permite que o desenvolvedor facilmente adicione ou remova manipuladores com base nas condições de tempo de execução. Por exemplo, ela é ideal em cenários que demandam ajustes frequentes na cadeia de processamento.

Quando Usar a Atribuição Via Construtor

Em contrapartida, se o sistema prioriza estabilidade e consistência, e a cadeia permanece inalterada após sua definição, a atribuição via construtor é mais apropriada. Nesse contexto, o desenvolvedor utiliza essa abordagem em sistemas onde a cadeia de responsabilidade já é conhecida desde o início e deve se manter imutável durante toda a execução.

    Exemplo de Implementações Concretas

    Aqui estão dois exemplos de classes concretas para ilustrar como cada abordagem seria implementada:

    Atribuição via Método:

    class GerenteAprovacaoHandler extends Handler
    {
        public function handle(Request $request): ?string
        {
            if ($request->amount < 1000) {
                return "Gerente: Aprovou\n";
            } elseif ($this->nextHandler) {
                return $this->nextHandler->handle($request);
            }
    
            return "Gerente: A solicitação requer aprovação adicional.";
        }
    }
    

    Aqui, o próximo manipulador é configurado posteriormente, usando o método setNext():

    $gerente = new GerenteAprovacaoHandler();
    $diretor = new DiretorAprovacaoHandler();
    $gerente->setNext($diretor);
    

    Atribuição via Construtor:

    class GerenteAprovacaoHandler extends Handler
    {
        public function __construct(Handler $nextHandler = null)
        {
            $this->nextHandler = $nextHandler;
        }
    
        public function handle(Request $request): ?string
        {
            if ($request->amount < 1000) {
                return "Gerente: Aprovou\n";
            } elseif ($this->nextHandler) {
                return $this->nextHandler->handle($request);
            }
    
            return "Gerente: A solicitação requer aprovação adicional.";
        }
    }
    

    Neste caso, o próximo manipulador é passado diretamente no construtor:

    $diretor = new DiretorAprovacaoHandler();
    $gerente = new GerenteAprovacaoHandler($diretor);
    

    Então, o desenvolvedor precisa ajustar a classe abstrata Handler de acordo com a forma de atribuição escolhida. Se, por exemplo, o desenvolvedor optar pela atribuição via método, ele deve então implementar o método setNext() para encadear os manipuladores durante a execução. Por outro lado, caso o desenvolvedor prefira a atribuição via construtor, ele irá definir a cadeia no momento da criação dos objetos, o que garante uma estrutura fixa e imutável.

    Portanto, ambas as abordagens oferecem suas vantagens. A escolha, no entanto, dependerá da flexibilidade ou rigidez que o sistema efetivamente exige.

    Conclusão

    Neste artigo, demonstrei como implementar o padrão Chain of Responsibility em PHP através de um sistema simples de processamento de solicitações de compra. Portanto, este padrão é extremamente útil para distribuir responsabilidades entre objetos em uma aplicação, facilitando a manutenção e a extensão do código.

    A escolha entre configurar o próximo handler através de um método ou um construtor depende largamente de como você espera gerenciar a cadeia de handlers:

    • Em primeiro lugar, use o método quando espera que a cadeia possa precisar de ajustes frequentes ou reconfiguração dinâmica.
    • Por outro lado, use o construtor para uma configuração mais estática e segura, onde a cadeia não precisa ser alterada após a inicialização.

    Em resumo, cada abordagem tem suas próprias vantagens e pode ser a melhor escolha dependendo das necessidades específicas da sua aplicação e da arquitetura de software. Compreender essas diferenças é fundamental para o desenvolvimento de aplicações robustas e flexíveis, contribuindo significativamente para a qualidade do design de software.

    Deixe um comentário

    O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

    Rolar para cima