[Delphi] Design Patterns GoF – State

[Delphi] Design Patterns - 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!

 

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:

if ObjetoCliente.Situacao = 'A' then // Ativo
  AplicarJurosClienteAtivo
else if ObjetoCliente.Situacao = 'P' then // Com Pendências
  AplicarJurosClientePendente
else if ObjetoCliente.Situacao = 'B' then // Bloqueado
  AplicarJurosClienteBloqueado;

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:

ObjetoCliente.AplicarJuros;

 

Ué, como assim?!
Simples. 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.

Acredito que essas analogias já foram suficientes, não é? Abraço!

Mentira. Partiremos 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

 

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:

type
  IState = interface
    procedure AdicionarItem(const ValorItem: real);
    procedure RemoverItem(const ValorItem: real);
 
    function ObterTotalPedido: real;
    function ObterValorDesconto: real;
    function ObterValorFrete: real;
  end;

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.

 

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

type
  TStateBase = class(TInterfacedObject, IState)
  protected
    FTotalPedido: real;
 
    // Métodos protegidos que serão sobrescritos pelas classes herdadas
    function ObterValorDesconto: real; virtual;
    function ObterValorFrete: real; virtual;
  public
    constructor Create(const TotalPedido: real);
 
    procedure AdicionarItem(const ValorItem: real);
    procedure RemoverItem(const ValorItem: real);
    function ObterTotalPedido: real;
  end;
 
implementation
 
{ TStateBase }
 
constructor TStateBase.Create(const TotalPedido: real);
begin
  FTotalPedido := TotalPedido;
end;
 
function TStateBase.ObterTotalPedido: real;
begin
  result := FTotalPedido;
end;
 
procedure TStateBase.AdicionarItem(const ValorItem: real);
begin
  FTotalPedido := FTotalPedido + ValorItem;
end;
 
procedure TStateBase.RemoverItem(const ValorItem: real);
begin
  FTotalPedido := FTotalPedido - ValorItem;
end;
 
function TStateBase.ObterValorDesconto: real;
begin
  // Será implementado nas classes herdadas
  result := 0;
end;
 
function TStateBase.ObterValorFrete: real;
begin
  // Será implementado nas classes herdadas
  result := 0;
end;

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”:

type
  TStateBronze = class(TStateBase)
  protected
    function ObterValorDesconto: real; override;
    function ObterValorFrete: real; override;
  end;
 
implementation
 
{ TStateBronze }
 
function TStateBronze.ObterValorDesconto: real;
begin
  result := 0;
 
  // Se o valor do pedido for menor que 200,00, não há desconto
  if FTotalPedido <= 200 then
    Exit;
 
  // Caso contrário, aplica um desconto de 5%
  result := FTotalPedido * 0.05;
end;
 
function TStateBronze.ObterValorFrete: real;
begin
  // Aplica uma porcentagem de 6% para o frete
  result := FTotalPedido * 0.06;
end;

 

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

type
  TStatePrata = class(TStateBase)
  protected
    function ObterValorDesconto: real; override;
    function ObterValorFrete: real; override;
  end;
 
implementation
 
{ TStatePrata }
 
function TStatePrata.ObterValorDesconto: real;
begin
  // Aplica 7% de desconto
  result := FTotalPedido * 0.07;
 
  // Se o pedido for maior que 700,00, aplica mais 2% de desconto
  if FTotalPedido > 700 then
    result := result * 0.02;
end;
 
function TStatePrata.ObterValorFrete: real;
begin
  // Aplica uma porcentagem de 5% para o frete
  result := FTotalPedido * 0.05;
end;

 

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

type
  TStateOuro = class(TStateBase)
  protected
    function ObterValorDesconto: real; override;
    function ObterValorFrete: real; override;
  end;
 
implementation
 
{ TStateOuro }
 
function TStateOuro.ObterValorDesconto: real;
begin
  // Aplica um desconto de 8%...
  result := FTotalPedido * 0.08;
 
  // ... e mais um desconto de 10% (que beleza, hein?)
  result := result * 0.1;
end;
 
function TStateOuro.ObterValorFrete: real;
begin
  result := 0;
 
  // Se o total do pedido for maior que 2 mil, o frete é grátis
  if FTotalPedido > 2000 then
    Exit;
 
  // Caso contrário, aplica uma porcentagem de 2% para o frete
  result := FTotalPedido * 0.02;
end;

 

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:

type
  TContext = class
  private
    // Mantém uma referência ao estado atual do objeto
    FState: IState;
 
    // Método principal do padrão de projeto
    procedure AlterarEstado;
  public
    constructor Create;
 
    procedure AdicionarItem(const ValorItem: real);
    procedure RemoverItem(const ValorItem: real);
 
    function ObterTotalPedido: real;
    function ObterValorDesconto: real;
    function ObterValorFrete: real;
  end;
 
implementation
 
uses
  Pattern.StateBronze, Pattern.StatePrata, Pattern.StateOuro;
 
{ TContext }
 
constructor TContext.Create;
begin
  // Inicia o pedido com o estado "Bronze", pois não há itens adicionados
  FState := TStateBronze.Create(0);
end;
 
procedure TContext.AdicionarItem(const ValorItem: real);
begin
  FState.AdicionarItem(ValorItem);
  AlterarEstado;
end;
 
procedure TContext.RemoverItem(const ValorItem: real);
begin
  FState.RemoverItem(ValorItem);
  AlterarEstado;
end;
 
function TContext.ObterTotalPedido: real;
begin
  result := FState.ObterTotalPedido;
end;
 
function TContext.ObterValorDesconto: real;
begin
  result := FState.ObterValorDesconto;
end;
 
function TContext.ObterValorFrete: real;
begin
  result := FState.ObterValorFrete;
end;
 
procedure TContext.AlterarEstado;
var
  TotalPedido: real;
begin
  TotalPedido := FState.ObterTotalPedido;
 
  // Se o total do pedido for até 500,00, devolve uma instância do estado "Bronze"
  if (TotalPedido <= 500) then
  begin
    FState := TStateBronze.Create(TotalPedido);
    Exit;
  end;
 
  // Se o total do pedido for até 1.000,00, devolve uma instância do estado "Prata"
  if (TotalPedido <= 1000) then
  begin
    FState := TStatePrata.Create(TotalPedido);
    Exit;
  end;
 
  // Acima de 1.000,00, devolve uma instância do estado "Ouro"
  FState := TStateOuro.Create(TotalPedido);
end;

 

Bem fácil, nã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:

type
  TfFormulario = class(TForm)
  { ... }
  private
    FContext: TContext;
 
{ ... }
 
procedure TfFormulario.FormCreate(Sender: TObject);
begin
  FContext := TContext.Create;
end;
 
procedure TfFormulario.FormDestroy(Sender: TObject);
begin
  FContext.Destroy;
end;

 

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.

ClientDataSet.Append;
{ preenche os campos... }
ClientDataSet.Post;
 
// Chama o método "AdicionarItem" do estado atual
FContext.AdicionarItem(ClientDataSet.FieldByName('Total').AsFloat);
 
// Obtém os valores conforme a categoria do pedido
EditDesconto.Text := FormatFloat('###,##0.00', FContext.ObterValorDesconto);
EditFrete.Text := FormatFloat('###,##0.00', FContext.ObterValorFrete);
EditTotal.Text := FormatFloat('###,##0.00',
  FContext.ObterTotalPedido - FContext.ObterValorDesconto + FContext.ObterValorFrete);

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

FContext.RemoverItem(ClientDataSet.FieldByName('Total').AsFloat);
ClientDataSet.Delete;
 
EditDesconto.Text := FormatFloat('###,##0.00', FContext.ObterValorDesconto);
EditFrete.Text := FormatFloat('###,##0.00', FContext.ObterValorFrete);
EditTotal.Text := FormatFloat('###,##0.00',
  FContext.ObterTotalPedido - FContext.ObterValorDesconto + FContext.ObterValorFrete);

 

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

 

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. 🙂

Exemplo de State com Delphi

Ou:

https://github.com/AndreLuisCelestino/Delphi-DesignPatterns/tree/master/State-AndreCelestino

 

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!


 

Compartilhe!
Share on FacebookTweet about this on TwitterShare on LinkedInShare on Google+Pin on PinterestEmail this to someone

4 comentários

Deixe uma resposta

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

Preencha o campo abaixo * Time limit is exhausted. Please reload CAPTCHA.