Design Pattern Strategy com Delphi

O padrão de projetos Strategy (GoF) consiste em um padrão comportamental que se preocupa em encapsular um determinado comportamento dentro de um objeto, delegando a execução desse comportamento para outro objeto, de forma que, caso seja necessário, esse algoritmo pode ser alterado sem afetar o objeto principal. Com isso fica fácil especificar e alterar as estratégias de processamento de um objeto, independente do cliente que esteja lhe usando.

Imagine no cenário de um ponto de vendas (PDV) o processamento de uma venda. Esse processo pode ser executado de diversas formas (ou se preferir, estratégias), por exemplo: itens concomitantes ou não, emissão de cupom fiscal, impressão não fiscal, envio para um webservice, gravação local, etc. Muito provavelmente, uma solução sem uso de padrões de projetos resultaria em uma série de blocos condicionais (IF’s) encadeados. O que significa código sujo, de difícil compreensão, difícil manutenibilidade e altamente destrutivo. O pattern Strategy foi criado para evitar esse tipo de problema.

Das diversas motivações para aplicar o pattern Strategy, eu destaco as seguintes:

  • Evitar a construção de diversas classes semelhantes que diferem somente em alguns comportamentos.
  • Possibilitar a variação dinâmica de um determinado algoritmo dentro de uma classe.
  • Deixar de usar operadores condicionais para realizar operações que podem variar.
  • Novos comportamentos podem ser implementados sem afetar o comportamento atual.

Para nos localizarmos melhor, vamos conhecer os participantes do pattern Strategy e suas atribuições:

  • Contexto (contexto): O objeto que possui o comportamento que será encapsulado em uma estratégia. O contexto recebe uma estratégia concreta e interage com ela.
  • Estratégia (strategy): A interface abstrata comum de todas as estratégias. Graças a essa interface é possível que o contexto se comunique com diferentes estratégias sem alterar o seu funcionamento.
  • Estratégia concreta (concrete strategy): O objeto concreto que implementa a interface da estratégia. O objeto de estratégia concreta é o objeto que efetivamente executa os algoritmos da estratégia.
  • Cliente (client): Responsável por criar o contexto e determinar qual será a estratégia concreta utilizada.

O contexto precisa conhecer a interface das estratégias para poder aplica-las, assim como as estratégias precisam conhecer o contexto para coletar as informações necessárias para a execução do algoritmo de estratégia ou até mesmo manipular o estado do contexto. Você pode optar por passar todas informações necessárias para a estratégia via parâmetro, mas dependendo da complexidade da solução, isso se torna inviável.

Um lema muito difundido entre os entusiastas do eXtreme Programming (os desenvolvedores que adotam o XP utilizam largamente os Design Patterns) é:

Programe para uma interface e não para uma implementação

Entenda interface como algo abstrato e não concreto (no Delphi pode ser tanto uma classe com métodos virtuais e abstratos como uma interface propriamente dita), por essa razão, o pattern Strategy pode ser implementado de duas formas, através de herança e polimorfismos ou através de interfaces. Neste post estarei mesclando um pouco as duas abordagens, porém dando mais ênfase para o uso de interfaces.

Para demonstrar o diagrama do pattern Strategy, vamos voltar ao exemplo de processamento de vendas em um PDV e definir quais serão nossas entidades e seus respectivos papeis na solução:

A classe TVenda será nosso contexto. Uma venda pode ter vários tipos de comportamentos, portanto, o tipo da venda será nossa estratégia e será representado pela interface ITipo. As estratégias concretas implementam a interface de ITipo. A classe TVenda não conhece a implementação da estratégia contida nas classes concretas de ITipo, assim como as classes concretas de tipo de venda, só devem conhecer a abstração de TVenda. Lembre-se, “programe para uma interface e não para uma implementação”.

Vale ressaltar que esse é um exemplo didático, onde o intuito é apresentar o conceito e não propor uma solução final para um problema que imagino que muitas softwares houses sofrem. Sendo assim, não faremos nada de muito prático, apenas uma aplicação do tipo console para validar a proposta do pattern Strategy.

Antes de partirmos para a solução elegante, vamos fazer um exercício e tentar imaginar como seria o método “Finalizar()” da classe TVenda, sem o uso do pattern Strategy. Talvez algo como:

function TVenda.Finalizar: Boolean;
begin
  if Self.Tipo = 0 then //não fiscal
  begin
    Writeln('imprimir itens');
  end
  else if Self.Tipo = 1 then //fiscal
  begin
    Writeln('Enviar itens para o ECF');
    Writeln('Imprimir o cupom fiscal');
  end
  else if Self.Tipo = 2 then //concomitante
  begin
    Writeln('Imprimir o cupom fiscal');
  end;
  Writeln('finalizar venda');
end;

Já posso imaginar o seu desespero, “IF’s”, “números mágicos”, “comentários”, “duplicidade”, etc. Outros devem estar pensando “esse código me lembra tanto um código que sofro para dar manutenção”. Vamos ver agora como podemos melhorar esse cenário, implementado o pattern Strategy. Vamos começar pelas interfaces e abstrações do nosso problema, TVenda e ITipo:

type
  TVenda = class;

  ITipo = interface
    procedure ProcessarItem(AVenda : TVenda);
    procedure ProcessarVenda(AVenda : TVenda);
  end

  TVenda= class
  private
    FTipo : ITipo;
    FItens : TStringList;
  public
    constructor Create(ATipo : ITipo);
    destructor Free();
    procedure AdicionarItem(const AItem : string);
    function Finalizar():Boolean;
    property Itens : TStringList read FItens;
  end;

A classe TVendas recebe a sua estratégia concreta em seu método construtor (Create()) e armazena no atributo FTipo que é do tipo ITipo (aproveito esse método também para criar a instancia da listagem de itens da venda):

constructor TVenda.Create(ATipo: ITipo);
begin
  Self.FTipo := ATipo;
  Self.FItens := TStringList.Create;
end;

Vamos partir do ponto que, a cada item adicionado (AdicionarItem()), será executado o método ProcessarItem() da estratégia e quando for solicitada a finalização da venda (Finalizar()), será executado o método ProcessarVenda() da estratégia. Desta forma:

procedure TVenda.AdicionarItem(const AItem: string);
begin
  Self.FItens.Add(AItem);
  Self.FTipo.ProcessarItem(Self);
end;

function TVenda.Finalizar: Boolean;
begin
  Self.FTipo.ProcessarVenda(Self);
  Result := true;
end;

Pronto, agora basta implementar as diversas estratégias necessárias (respeitando a interface de ITipo) e passar a interagir com a classe TVenda. Vamos começar pela estratégia concreta TTIpoNaoFiscal, que representa uma estratégia de venda não fiscal:

type
  TTipoNaoFiscal = class(TInterfacedObject, ITipo)
  public
    procedure ProcessarItem(AVenda: TVenda);
    procedure ProcessarVenda(AVenda: TVenda);
  end

Como a classe TTipoNaoFiscal implementa a interface ITipo, ela já está “credenciada” para ser uma estratégia para a classe TVenda. Em nosso cenário fictício, a estratégia de venda não fiscal não necessita realizar nenhum processamento ao adicionar itens a venda, mas, deve realizar a impressão em tela de todos os itens da venda no momento da finalização desta.

procedure TTipoNaoFiscal.ProcessarItem(AVenda: TVenda);
begin
end;

procedure TTipoNaoFiscal.ProcessarVenda(AVenda: TVenda);
var
  I: Integer;
begin
  writeln('#################################');
  Writeln('# Impressão de venda não fiscal #');
  writeln('#################################');
  Writeln(EmptyStr);

  for I := 0 to AVenda.Itens.Count - 1 do
    Writeln(Format('Item %d: %s',[I+1, AVenda.Itens.Strings[i]]));

  Writeln(EmptyStr);
  writeln('#################################');
  Writeln('#              FIM              #');
  writeln('#################################');
end;

Antes de implementarmos novas estratégias, vamos validar a estratégia TTipoNaoFiscal com o seguinte código no próprio arquivo dpr do projeto (levando em consideração que esta é uma aplicação console):

var
  oVenda : TVenda;
begin
  oVenda := TVenda.Create(TTipoNaoFiscal.Create());
  try
    try
      oVenda.AdicionarItem('Banana');
      oVenda.AdicionarItem('Maça');
      oVenda.Finalizar();
      Readln;
    except
      on E: Exception do
        Writeln(E.ClassName, ': ', E.Message);
    end;
  finally
    oVenda.Free;
  end;
end.

Ao executar a aplicação acima, a saída de console será a seguinte:

#################################
# Impressão de venda não fiscal #
#################################

Item 1: Banana
Item 2: Maça

#################################
#              FIM              #
#################################

Excelente, já estamos fazendo vendas não fiscais 🙂 . Vamos implementar agora a estratégia de venda fiscal, criando a classe concreta TTipoFiscal.

type
  TTipoFiscal = class(TInterfacedObject, ITipo)
  public
    procedure ProcessarItem(AVenda: TVenda);virtual;
    procedure ProcessarVenda(AVenda: TVenda);
  end;

(Não se preocupe, a diretiva virtual, fará sentido mais a frente 🙂 ).
Assim como a classe TTipoNaoFiscal, a classe TTipoFiscal não fará tratamentos ao adicionar itens a venda, porém, no momento de fechamento, a venda será “enviada para o ECF”:

procedure TTipoFiscal.ProcessarItem(AVenda: TVenda);
begin
end;

procedure TTipoFiscal.ProcessarVenda(AVenda: TVenda);
begin
  writeln('################################');
  Writeln('# Impressão enviada para o ECF #');
  writeln('################################');
end;

Para testar nossa nova estratégia, vamos mudar a linha:

oVenda := TVenda.Create(TTipoNaoFiscal.Create());

Para:

oVenda := TVenda.Create(TTipoFiscal.Create());

Veja que não foi necessária nenhuma alteração na classe TVenda. Ao executar o teste da nova estratégia, teremos a seguinte saída no console:

################################
# Impressão enviada para o ECF #
################################

Implementaremos agora nossa ultima estratégia concreta, a que irá realizar vendas fiscais, porém, com impressão de itens concomitantes (simultâneo ao lançamento). Essa estratégia será representada na classe concreta TTipoFiscalConcomitante. A estratégia de venda fiscal concomitante só difere da estratégia de venda fiscal no que diz respeito a impressão concomitante dos itens, sendo assim, iremos aproveitar o rotina de “enviar a venda para o ecf” da nossa classe TTipoFiscal através de herança:

type
  TTipoFiscalConcomitante = class(TTipoFiscal)
  public
    procedure ProcessarItem(AVenda: TVenda);override;
  end;

Repare que, diretamente não estou especificando que minha classe TTipoFiscalConcomitante implementa a interface ITipo, porém, por ela ser uma herança da classe TTipoFiscal, implicitamente a minha classe TTipoFiscalConcomitante implementa a interface ITipo. Como dito anteriormente, nossa estratégia de venda fiscal concomitante somente irá se diferenciar da estratégia de venda fiscal no que diz respeito a tratamento de itens lançados na venda, sendo assim, como herdamos da classe TTipoFiscal, só precisamos sobrescrever o método ProcessarItem():

procedure TTipoFiscalConcomitante.ProcessarItem(AVenda: TVenda);
var
  I : integer;
begin
  inherited;
  I := AVenda.Itens.Count;
  Writeln(Format('Item [%d-%s] enviado para o ECF',[I, AVenda.Itens.Strings[I-1]]));
end;

Eu sei que seria possível resolver todo esse cenário apenar usando heranças da classe TVenda, porém, o pattern Strategy é uma forma de fugir das heranças. Repetindo o nosso teste agora para a classe TTipoFiscalConcomitante, vamos mudar a linha:

oVenda := TVenda.Create(TTipoFiscal.Create());

Para:

oVenda := TVenda.Create(TTipoFiscalConcomitante.Create());

Mais uma vez, repare que somente alteramos a estratégia e não o contexto, a saída desse novo teste será a seguinte:

Item [1-Banana] enviado para o E
Item [2-Maça] enviado para o ECF
################################
# Impressão enviada para o ECF #
################################

Para fechar, poderíamos criar um método que de forma dinâmica nos retorne uma instancia da estratégia concreta, que por algum motivo, será a estratégia que deverá ser usada no momento. Em nosso exemplo, essa decisão será tomada através de um parâmetro de linha de comando:

function GetTipoVenda():ITipo;
var
  sParam : string;
begin
  sParam := ParamStr(1);
  if sParam = 'fiscal' then
    Result := TTipoFiscal.Create()
  else if sParam = 'concomitante' then
    Result := TTipoFiscalConcomitante.Create
  else
    Result := TTipoNaoFiscal.Create();
end;

E finalmente, nossa estratégia de venda passa a ser dinâmica:

oVenda := TVenda.Create(GetTipoVenda());

O cenário de processamento de venda foi um exemplo de uso em uma situação real do pattern Strategy. A ideia é entender os conceitos por trás desse pattern. Você pode encontrar os fontes dos exemplos desse post e outros exemplos de design patterns neste repositório do GitHub.

Sobre Diego Garcia

Analista/Desenvolvedor Delphi desde 2008, bacharel em Ciência da Computação e entusiasta de metodologias ágeis e engenharia de software.
Esse post foi publicado em Metodologias Ágeis, Programação e marcado , , , , . Guardar link permanente.

3 respostas para Design Pattern Strategy com Delphi

  1. Parabéns pelo excelente artigo! (y)

  2. Tem o link ainda da erro

Deixe um comentário