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:
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.
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. 🙂
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!
Daria para mesclar com o Chain of Responsibility, né? Com isso daria para ‘matar’ alguns ifs.
Mas achei interessante esse padrão. Me lembra os estados do DataSet, kkk.
Fala, Oliveira!
Rapaz, não só o CoR, mas também o Proxy ou o Prototype! Até cogitei em utilizá-los, mas o artigo ficaria muito extenso.
Abração!
Parabéns pelo artigo. Até hoje só utilizei o case, vou testar aqui.
Obrigado, Saulo!
Grande abraço!