[Delphi] Design Patterns GoF – State

Saudações, pessoal!
Sabemos que uma das premissas da Orientação a Objetos é trabalhar com estados e comportamentos dos objetos. O padrão de projeto State, que será abordado neste artigo, fornece um meio muito simples e intuitivo de controlar o estado atual de um objeto. Veremos que a sua implementação visa não só a organização no código, mas também a facilidade na manutenção. Let’s do it!

Introdução

Objetos em uma aplicação tendem a mudar de estado constantemente em função das regras de negócio. Um método chamado AplicarJuros em um sistema financeiro, por exemplo, pode apresentar diferentes comportamentos conforme a situação atual do cliente, variando valores, parâmetros e até mesmo a saída para o usuário.

Em primeiro momento, alguns desenvolvedores podem considerar a seguinte solução:

No entanto, observe a quantidade da estrutura condicionais. Inadequado, não é? imagine, então, que duas novas situações sejam acrescentadas à regra de negócio. Além da adição de dois novos IFs, o desenvolvedor também terá que criar dois novos métodos para processá-las, aumentando a complexidade ciclomática.
O nosso objetivo com o padrão de projeto State é desfazer esse aninhamento condicional, transformando o código acima em:

Bem melhor, não? O cliente, naquele momento, terá um dos estados possíveis: ativo, pendente ou bloqueado. Cada estado é uma instância de uma classe que contém as regras daquele estado específico, portanto, o método AplicarJuros pode ter diferentes comportamentos. Uma vantagem que já podemos identificar é que, caso uma nova situação seja declarada (como “suspenso”), a chamada acima continua a mesma. Quem irá definir a regra da aplicação de juros é o estado do próprio objeto.

Não paramos por aí. No State, um dos elementos recebe inteligência para trocar o estado de acordo com regras estabelecidas. Considere, por exemplo, que a aplicação de juros determine a situação do cliente. Sendo assim, um cliente com pendências pode entrar para o estado “bloqueado” caso o valor de juros seja muito alto. Essa habilidade é exercida de forma encapsulada no State.

Exemplo de codificação do State

Partiremos, agora, para uma codificação prática do State para apresentar o seu funcionamento. Para isso, precisamos de três elementos:

  • State: Interface que declara os métodos que poderão ser executados pelos estados do objeto;
  • Concrete State: implementa a Interface State para definir cada estado possível do objeto;
  • Context: representa o estado atual do objeto, invocando seus métodos.

Como exemplo, desenvolveremos uma aplicação simples para cadastro de pedido de componentes eletrônicos. O usuário poderá adicionar itens e, conforme o valor total, o pedido poderá ser classificado como “Bronze”, “Prata” ou “Ouro”, fornecendo benefícios de desconto e frete para o comprador. Essas categorias serão os estados do pedido, logo, ao atingir um valor específico, o pedido recebe um novo estado com novas regras. Veja o protótipo:

Exemplo de formulário para aplicação do Design Pattern State

Interface State

Começaremos pela Interface que leva o nome do padrão de projeto. Nela, adicionaremos métodos que serão comuns entre todos os estados do objeto:

Os métodos AdicionarItem e RemoverItem serão responsáveis por somar e subtrair, respectivamente, o valor do item a uma variável que armazena o total do pedido. O método ObterTotalPedido apenas retorna o valor dessa variável. A diferença entre os estados acontecerá nos métodos ObterValorDesconto e ObterValorFrete, já que, de acordo com a categoria do pedido, os benefícios são distintos.

Classes Concrete States

Para evitar a redundância de código, criaremos uma classe Concrete State que servirá como generalização para os estados:

Um detalhe: ao trocar o estado, devemos manter a informação do total do pedido para continuar avaliando a categoria, caso o usuário adicione mais itens ou os remova. Daí a necessidade de receber o valor no construtor.

A partir de agora, codificaremos os estados, modelando classes que herdam de TStateBase. As regras de desconto e frete estarão comentadas no próprio código.

O primeiro estado é “Bronze”:

O categoria superior ao “Bronze” é “Prata”, que representa mais um dos estados:

Por fim, se o valor total for alto, o pedido entrará na categoria “Ouro”, finalizando os estados possíveis para o objeto:

Classe Context

O último passo é criar a classe Context, incumbida de trocar os estados conforme o valor total. Além disso, essa classe deverá ter uma variável privada para armazenar o estado atual do objeto (Bronze, Prata ou Ouro). Observe, a seguir, que teremos basicamente os mesmos métodos que os estados. A diferença é que o Context ficará responsável por chamá-los utilizando a variável que mantém a referência do estado atual, tornando essas chamadas completamente encapsuladas.

O grande destaque da classe Context está no método AlterarEstado. A cada inserção ou remoção de um item, este método será chamado para trocar o estado caso necessário:

Bem fácil, não?

Em ação!

O nosso formulário – que atual como Client – não conhecerá o estado atual do objeto. Apenas consumirá as regras através do Context, portanto, é preciso utilizar uma instância dessa classe:

Na inserção de um novo item, após adicioná-lo no TClientDataSet, basta chamar o método AdicionarItem do Context, que encaminhará a chamada ao estado atual do objeto. Em seguida, podemos obter os valores, que são calculados de acordo com a categoria.

Ao remover um item, o procedimento é o mesmo, com exceção de que utilizamos o método RemoverItem:

Faça o teste. Quando o pedido atingir 500,00 ou 1.000,00, o estado do objeto será automaticamente alterado e as novas regras de desconto e frete entrarão em vigor, lembrando que, no Client, tudo permanece da mesma forma.

Exemplo de implementação do Design Pattern State

Conclusão

André, e aquela indicação da categoria do pedido na parte inferior do formulário?

Trata-se de uma codificação “extra” que adicionei ao projeto. O link para download está logo abaixo. Além dessa alteração, você encontrará outras melhorias, como a utilização de tipos enumerados, constantes e comentários explicando cada linha de código. 🙂

Antes de finalizar o artigo, gostaria de citar mais um cenário para aplicação deste Design Pattern. Considere um sistema de gestão de processos judiciais. Cada processo pode transitar por várias situações em sua linha do tempo. Por exemplo, a situação inicial de um processo é “Em cadastro”. Após a juntada de petições e documentos, o processo é movido para a situação “Em andamento”. Neste caso, as regras do processo (valor de custas, solicitação de cancelamento, intimações) recebem um novo comportamento.

Conseguiu identificar as semelhanças com o exemplo do artigo? Cada situação é um estado do objeto. A troca de situações – quando o processo recebe valores ou documentos – é realizada por um Context. No Client, que pode ser um formulário de acompanhamento, os métodos chamados sempre serão os mesmos. Já os comportamentos dependem da situação atual em que ele se encontra.

 

Fico por aqui, leitores!
Grande abraço!


 

André Celestino