Desenvolvimento

28 fev, 2019

Arquitetura e desenvolvimento de software – Parte 14: Chain of Responsibility

Publicidade

E aí, pessoal! Tudo bem?

Neste artigo vamos dar início ao estudo dos padrões comportamentais, continuando a série sobre design patterns e começando pelo padrão Chain of Responsibility.

Padrões comportamentais

Antes de falarmos detalhadamente sobre o Chain of Responsibility, vamos entender o que é o grupo de padrões comportamentais.

Padrões comportamentais tratam de algoritmos que focam nas relações entre os objetos, fazendo com que essas entidades se comuniquem mais facilmente e flexivelmente.

Chain of Responsibility

Chain of Responsibility tem como principal função evitar a dependência entre um objeto receptor e um objeto solicitante. Consiste em uma série de objetos receptores e de objetos de solicitação, onde cada objeto de solicitação possui uma lógica interna que separa quais são tipos de objetos receptores que podem ser manipulados. O restante é passado para o próximo objeto de solicitação da cadeia.

Devido a isso é um padrão que utiliza a ideia de baixo acoplamento por permitir que outros objetos da cadeia tenham a oportunidade de tratar uma solicitação.

Explicando melhor

Vamos pensar no seguinte exemplo:

  • Temos uma empresa onde cada cargo de gestão pode solicitar a compra de materiais de escritório e outras coisas
  • Cada cargo tem um limite de gastos, e caso a compra ultrapasse o limite de gastos do cargo, a responsabilidade/aprovação da compra deve ser passada para o próximo nível
  • Temos os seguintes cargos: Supervisor, Gerente, Diretor e Presidente.

Para implementar isso, imagine que temos que verificar cada cargo, seu limite, se a compra passa esse limite para então instanciar outro cargo acima. Já imagina a quantidade de IF’s que podemos ter, tornando nosso código mais complexo de manter.

Utilizando o Chain of Responsibility, solucionamos o problema acima da seguinte forma:

  • Criaremos uma classe base, representando um gestor de qualquer cargo, com as propriedades de limite, cargo e se aplicável, uma referência para o superior, que também é um gestor
  • Nesta classe base criaremos um método para processar a compra. Esse método verifica se o valor da compra ultrapassa o limite do gestor. Caso ultrapasse, chama o processamento da instância do superior, ou se não ultrapassar o limite, processa a compra
  • Para cada cargo específico criamos uma classe específica onde, no construtor, informamos o limite e nome daquele cargo, assim como seu superior

Notem que pela abordagem acima, centralizaremos a regra de processamento na classe gestor. Ou seja, independente de quantos cargos a mais possamos criar, a regra está ali, em um único local e de fácil manutenção.

Dado que cada cargo tem uma referência a seu superior e possui a lógica da classe base gestor, entendemos que cada um vai ser responsável por chamar seu superior caso o valor de compra ultrapasse seu limite.

Ou seja, a primeira chamada de aprovação da compra será encadeada (chain), de acordo com a necessidade e responsabilidade (responsibility) de cada cargo.

Vamos ver isso melhor codificando nosso exemplo:

using System;

namespace ChainOfResponsabilitySample
{
    public class RequisicaoCompra
    {
        public RequisicaoCompra(decimal valor, string descricao)
        {
            Valor = valor;
            Descricao = descricao;
        }

        public decimal Valor { get; private set; }
        public string Descricao { get; private set; }
    }

    abstract class Gestor
    {
        protected Gestor(decimal limite, string cargo, Gestor sucessor = null)
        {
            Limite = limite;
            Sucessor = sucessor;
            Cargo = cargo;
        }

        public decimal Limite { get; private set; }
        public Gestor Sucessor { get; set; }
        public string Cargo { get; private set; }

        public void ProcessarCompra(RequisicaoCompra compra)
        {
            if (compra.Valor <= Limite)
            {
                Console.WriteLine($"{Cargo} aprovou a compra no valor de {compra.Valor.ToString("C")}");
            }
            else if (Sucessor != null)
            {
                Sucessor.ProcessarCompra(compra);
            }
            else
            {
                Console.WriteLine($"A compra no valor de {compra.Valor.ToString("C")} ultrapassa o limite de {Limite.ToString("C")} e o {Cargo} não possui superior.");
            }
        }
    }

    class Gerente : Gestor
    {
        public Gerente(Gestor sucessor = null) : base(500, "Gerente", sucessor)
        {
        }
    }

    class Diretor : Gestor
    {
        public Diretor(Gestor sucessor = null) : base(2000, "Diretor", sucessor)
        {
        }
    }

    class VicePresidente : Gestor
    {
        public VicePresidente(Gestor sucessor = null) : base(5000, "Vice Presidente", sucessor)
        {
        }
    }


    class Program
    {
        static void Main(string[] args)
        {
            var vicePresidente = new VicePresidente();
            var diretor = new Diretor(vicePresidente);
            var gerente = new Gerente(diretor);

            try
            {
                var entrada = "";

                while (!"q".Equals(entrada, StringComparison.InvariantCultureIgnoreCase))
                {
                    Console.WriteLine("Informe um valor de compra para iniciar aprovação:");
                    entrada = Console.ReadLine();

                    RequisicaoCompra compra = new RequisicaoCompra(decimal.Parse(entrada), "Computador");

                    gerente.ProcessarCompra(compra);
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                Console.ReadKey();
            }
        }
    }
}

Notem que cada cargo ou classe criados não tem nenhuma funcionalidade implementada – está toda na classe base. E se fosse necessário, ainda poderíamos sobrescrever o processamento da classe base.

Nosso código possui apenas um IF (com exceção do While utilizado no console, mas ali é parte da “View” de nosso exemplo) que valida a regra de limite.

Vamos à ficha resumo do pattern onde iremos detalhar os pontos que faltam, incluindo o diagrama UML, e ver um exemplo extra – desta vez, em Java.

Ficha resumo

  • Nome: Chain Of Responsibility
  • Objetivo/intenção: evitar o acoplamento do remetente de uma solicitação ao seu receptor ao dar a mais de um objeto a oportunidade de tratar a solicitação. Encadear os objetos receptores, passando a solicitação ao longo da cadeia até que um objeto a trate
  • Motivação: quando temos uma determinada regra/processo que tem uma dependência entre objetos em hierarquia ou sequência, utilizamos o pattern de modo a encadear o processamento entre vários objetos, onde cada um recebe a solicitação, trata, e se necessário envia a um novo objeto para continuar o processamento, fornecendo uma maneira de tomar decisões com um fraco acoplamento
  • Aplicabilidade: utilizamos o pattern quando vemos que mais de um objeto pode tratar uma solicitação e o objeto que a tratará não é conhecido inicialmente. O objeto que trata a solicitação deve ser escolhido automaticamente – deve-se emitir uma solicitação para um dentre vários objetos, sem especificar o receptor ou o conjunto de objetos que pode tratar uma solicitação deveria ser especificado dinamicamente. São situações mais comuns que incentivam a aplicação do pattern
  • Estrutura: No desenho abaixo temos um exemplo UML de aplicação do pattern, já descrito no início do artigo com a responsabilidade de cada parte do diagrama
Diagrama UML, exemplificando nosso handler e as devidas implementações dele, com chamada a seu sucessor.
  • Consequências: vimos pelo exemplo que o padrão Chain of Responsibility fornece uma maneira de tomar decisões com um fraco acoplamento. Perceba que a estrutura de cadeia não possui qualquer informação sobre as classes que compõem a cadeia. Da mesma forma, uma classe da cadeia não tem nenhuma noção sobre o formato da estrutura ou sobre elementos nela inseridos. Mas um ponto de atenção é que precisamos garantir que as chamadas sejam realmente respondidas. No exemplo foi feita uma verificação para saber se o próximo elemento é nulo, para evitar uma acesso ilegal. Mas esta é uma solução para este problema específico. Cada problema exige o seu próprio cuidado;
  • Implementações: abaixo temos um outro exemplo de código em Java para o pattern:
package com.gbbigardi.design.chainOfResponsability

import java.util.Scanner;

public class SaqueDinheiro {
    
    private int valor;
    
    public SaqueDinheiro(int valor) {
        this.valor = valor;
    }
    
    public int getValor() {
        return this.valor;
    }
}

public interface DispenserCaixaeletronico {
    void setProximoElo(DispenserCaixaeletronico proximoElo);
    void sacar(SaqueDinheiro saque);
}

public class DispenserNota100 implements DispenserCaixaeletronico {

    private DispenserCaixaeletronico proximoDispenser;
    
    @Override
    public void setProximoElo(DispenserCaixaeletronico proximoElo) {
        this.proximoDispenser = proximoElo;
    }
    
    @Override
    public void sacar(SaqueDinheiro saque) {
        if (saque.getValor() >= 100) {
            int notas = saque.GetValor() / 100;
            int restante = saque.getValor() % 100;
            System.out.println("Liberando " + notas + " de 100 reais");
            if (restante != 0)
                this.proximoDspenser.sacar(new SaqueDinheiro(restante);
        } else {
            this.proximoDispenser.sacar(saque)
        }
    }
}

public class DispenserNota50 implements DispenserCaixaeletronico {

    private DispenserCaixaeletronico proximoDispenser;
    
    @Override
    public void setProximoElo(DispenserCaixaeletronico proximoElo) {
        this.proximoDispenser = proximoElo;
    }
    
    @Override
    public void sacar(SaqueDinheiro saque) {
        if (saque.getValor() >= 50) {
            int notas = saque.GetValor() / 50;
            int restante = saque.getValor() % 50;
            System.out.println("Liberando " + notas + " de 50 reais");
            if (restante != 0)
                this.proximoDspenser.sacar(new SaqueDinheiro(restante);
        } else {
            this.proximoDispenser.sacar(saque)
        }
    }
}

public class DispenserNota20 implements DispenserCaixaeletronico {

    private DispenserCaixaeletronico proximoDispenser;
    
    @Override
    public void setProximoElo(DispenserCaixaeletronico proximoElo) {
        this.proximoDispenser = proximoElo;
    }
    
    @Override
    public void sacar(SaqueDinheiro saque) {
        if (saque.getValor() >= 20) {
            int notas = saque.GetValor() / 20;
            int restante = saque.getValor() % 20;
            System.out.println("Liberando " + notas + " de 20 reais");
            if (restante != 0)
                this.proximoDspenser.sacar(new SaqueDinheiro(restante);
        } else {
            this.proximoDispenser.sacar(saque)
        }
    }
}

public class DispenserNota10 implements DispenserCaixaeletronico {

    private DispenserCaixaeletronico proximoDispenser;
    
    @Override
    public void setProximoElo(DispenserCaixaeletronico proximoElo) {
        this.proximoDispenser = proximoElo;
    }
    
    @Override
    public void sacar(SaqueDinheiro saque) {
        if (saque.getValor() >= 10) {
            int notas = saque.GetValor() / 10;
            int restante = saque.getValor() % 10;
            System.out.println("Liberando " + notas + " de 10 reais");
            if (restante != 0)
                this.proximoDspenser.sacar(new SaqueDinheiro(restante);
        } else {
            this.proximoDispenser.sacar(saque)
        }
    }
}

public class CaixaEletronico {
    
    private DispenserCaixaeletronico dispenser100;
  
    public CaixaEletronico() {
        this.dispenser100 = new  DispenserNota100();
      
        DispenserCaixaeletronico dispenser50 = new DispenserNota50();
        this.dispenser100.setProximoElo(dispenser50);
      
        DispenserCaixaeletronico dispenser20 = new DispenserNota20();
        dispenser50.setProximoElo(dispenser20);
      
        DispenserCaixaeletronico dispenser10 = new DispenserNota10();
        dispenser20.setProximoElo(dispenser10);
    }
  
    public static void main(String[] args) {
        CaixaEletronico caixa = new CaixaEletronico();

        while(true) {
            int valor = 0;
            System.out.println("Informe uma quantia para saque");
            Scanner input = new Scanner(System.in);
            valor = input.nextInt();
          
            if (valor % 10 != 0) {
                System.out.println("O valor deve ser em múltiplos de 10");
            }
          
            caixa.dispenser100.sacar(new SaqueDinheiro(valor));
        }
    }
}
  • Usos conhecidos: utilizamos em sistemas que possuem rotinas de tomada de decisão complexas e que normalmente precisam respeitar uma hierarquia ou fluxo de processamento, sendo que encadeamos a requisição de processamento por N objetos, um chamando o próximo e assim sucessivamente até que seja concluído o processamento, com sucesso ou falha. Um outro uso bem conhecido: já ouviu falar de Middlewares? Aguardem, pois teremos um artigo dedicado a este assunto
  • Padrões relacionados: Chain of Responsibility, Command, Mediator, e Observer são como endereços, podendo ser separados entre remetentes e destinatários, mas com resultados e desafios diferentes. O Chain of Responsibility passa um pedido ao remetente junto com uma cadeia de potenciais destinatários. Podemos, inclusive, usar o padrão Command para representar pedidos como objetos. Frequentemente também aplicamos o padrão Composite, onde temos uma hierarquia dos objetos, como inclusive foi feito no exemplo em C#, utilizando uma classe abstrata, que poderia ter mais classes na hierarquia, compondo diferentes processos de encadeamento

Concluindo

Hoje aprendemos sobre o pattern Chain of Responsibility, que facilita nosso trabalho quando temos uma regra comum aplicada a uma cadeia de entidades com restrições ou detalhes extras para a regra, que irão processar em forma de uma cadeia de chamadas esta regra, conforme necessidade ou restrições de cada entidade na cadeia.

Por hoje deixo este material para vocês. Na próxima parte conheceremos o Iterator, dando continuidade ao grupo de patterns comportamentais.

Para quem perdeu a série desde o início, segue o link para a primeira parte, onde listo todos os patterns que vamos abordar, cada um com o link correspondente, servindo de índice:

Um abraço a todos e até a próxima!