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:
1 2 3 4 5 6 |
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:
1 |
ObjetoCliente.AplicarJuros; |
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:
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:
1 2 3 4 5 6 7 8 9 |
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.
Classes Concrete States
Para evitar a redundância de código, criaremos uma classe Concrete State que servirá como generalização para os estados:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
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”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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; |
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
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?
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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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.
1 2 3 4 5 6 7 8 9 10 11 12 |
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
:
1 2 3 4 5 6 7 |
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.
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!
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!
Parabéns pelo excelente conteúdo, André. Ajuda bastante a compreender os padrões.
Muito obrigado, Rafael!
Grande abraço!