[Delphi] Design Patterns – Chain of Responsibility

E aí, pessoal, tudo bem?
O artigo de hoje marca o início dos padrões de projetos Comportamentais. Recebem este nome por propor e recomendar soluções que envolvam interações entre objetos, de forma que, mesmo que exista essa interação, eles não dependam fortemente um do outro, ou seja, fiquem fracamente acoplados. O primeiro deste padrões, muito fácil de compreender, é o Chain of Responsibility. Já ouviu falar? Não? Então acompanhe o artigo!

 

Uma das premissas mais importantes na elaboração da arquitetura de um software é manter o baixo acoplamento e a alta coesão. O primeiro requisito refere-se à eliminação de fortes dependências entre classes, enquanto o segundo empenha a responsabilidade única de cada classe, respondendo a princípio chamado Single Responsibility. Uma arquitetura com baixo acoplamento e alta coesão, portanto, significa que as classes são bem delimitadas e cada uma assume apenas uma função exclusiva no sistema.
O padrão de projeto Chain of Responsibility representa uma solução para a redução de dependências entre classes. O maior propósito é permitir que mensagens (ou dados) naveguem entre diferentes objetos dentro de uma hierarquia (ou cadeia) até que um desses deles tenha a capacidade de assumi-la, ou melhor, processá-la, mas com um detalhe importante: nessa hierarquia, cada objeto não conhece os detalhes do outro.

O exemplo mais clássico encontrado na internet sobre este padrão de projeto descreve a aprovação de um orçamento. O pedido, que é a “mensagem”, passa primeiro pelo gerente, que possui autonomia para aprovar valores apenas entre 5 a 10 mil reais. Se o valor do pedido for maior, é enviado para o próximo cargo na hierarquia que, neste caso, é o diretor. Caso o valor do pedido seja de até 20 mil reais, o orçamento será autorizado pelo diretor, caso contrário, é enviado para o presidente. Observe que se forem emitidos três pedidos nos valores de 2, 18 e 25 mil reais cada um, a aprovação será feita por pessoas diferentes.

 

O conceito é interessante, mas qual a vantagem técnica deste padrão?
Lembra-se que eu mencionei o baixo acoplamento? Pois bem, no contexto do Chain of Responsibility, cada “elo da corrente”, ou seja, cada objeto, não conhece os outros participantes da hierarquia, mas sabe que eles existem. Em termos mais técnicos, cada objeto sabe que o seu superior (ou sucessor) implementa uma determinada Interface, porém, não conhece a sua identidade. Esse cenário permite que não existam fortes dependências entre classes, de modo que a cadeia de responsabilidades possa ser alterada a qualquer momento sem impactar na funcionalidade existente.
Considere a ilustração abaixo:

Se o 2º elo for removido, o sucessor do 1º elo passará a ser o 3º. A mensagem, por sua vez, não sofre impacto com a alteração da corrente e continuará sendo transmitida naturalmente pela hierarquia até que seja processada por algum objeto.

Vamos desenvolver um exemplo prático para solidificar a definição deste padrão de projeto?
Temos a seguinte situação: o nosso sistema deve disponibilizar uma funcionalidade de importação de dados para evitar que os usuários tenham que inclui-los manualmente. No entanto, cada cliente que utiliza o sistema trabalha com um formato diferente de arquivo, conforme apresentado abaixo:

• CSV

Código,Nome,Cidade

• XML

<importacao>
    <dados>
      <codigo>Código</codigo>
      <nome>Nome</nome>
      <cidade>Cidade</cidade>
    </dados>
</importacao>

• JSON

{  
   "dados":[  
      {  
         "codigo": Código,
         "nome": Nome,
         "cidade": Cidade
      }
   ]
}

A nossa proposta é criar uma cadeia de responsabilidades que receba o arquivo de importação e processe os dados, independente do formato. Cada elo da corrente será o Parser de um dos formatos apresentados. Se o elo atual identificar que não consegue processar o formato (mensagem), o arquivo é delegado para o próximo elo.

 

Isso é uma hierarquia?
Sim! A hierarquia que discutimos até agora não precisa necessariamente ser vertical. No exemplo deste artigo, você verá que o fluxo da hierarquia é horizontal, ao menos que consideremos que cada formato de arquivo é uma “evolução” do outro. 🙂
Brincadeiras à parte, o maior objetivo da hierarquia é separar as responsabilidades de cada classe e mantê-las desacopladas, mas, ao mesmo tempo, prover um mecanismo eficiente de processamento de dados.

A primeira etapa é criar a Interface que será comum para todos os “importadores de dados”, chamada Handler:

type
  { Handler }
  IParser = interface
    // Setter para atribuir a referência do Concrete Handler superior
    procedure SetProximoParser(Parser: IParser);
 
    // Método para processar a inclusão de dados no DataSet
    procedure ProcessarInclusao(const NomeArquivo: string; DataSet: TClientDataSet);
  end;

Os Concrete Handlers são as implementações concretas da Interface Handler. No nosso caso, cada classe será responsável pelo processamento de um formato específico de arquivo.

• CSV

type
  { Concrete Handler - Processador de CSV}
  TParserCSV = class(TInterfacedObject, IParser)
  private
    // Referência para o Concrete Handler superior
    ProximoParser: IParser;
  public
    // Atribui a referência do Concrete Handler superior
    procedure SetProximoParser(Parser: IParser);
 
    // Método para processar a inclusão de dados no DataSet
    procedure ProcessarInclusao(const NomeArquivo: string; DataSet: TClientDataSet);
  end;
 
implementation
 
{ TParserCSV }
 
procedure TParserCSV.ProcessarInclusao(const NomeArquivo: string; DataSet: TClientDataSet);
var
  Valores: TStringList;
  Linha: TStringList;
  Contador: integer;
begin
  // Verifica se a extensão do arquivo é compatível com a função da classe
  if UpperCase(ExtractFileExt(NomeArquivo)) <> '.CSV' then
  begin
    // Se não houver um Parser superior, significa que a mensagem chegou ao fim da cadeia
    if not Assigned(ProximoParser) then
      raise Exception.Create('Formato desconhecido.');
 
    // Transfere a mensagem para o próximo Parser (Concrete Handler)
    ProximoParser.ProcessarInclusao(NomeArquivo, DataSet);
    Exit;
  end;
 
  // Cria a TStringList que irá carregar o arquivo selecionado
  Valores := TStringList.Create;
 
  // Cria a TStringList que receberá os valores de cada linha
  Linha := TStringList.Create;
  try
    // Carrega o arquivo
    Valores.LoadFromFile(NomeArquivo);
 
    // Executa um loop nos itens da TStringList
    for Contador := 0 to Pred(Valores.Count) do
    begin
      Linha.Clear;
 
      // Utiliza o ExtractStrings para quebrar os valores
      // que estão separados por vírgula
      ExtractStrings([','], [' '], PChar(Valores[Contador]), Linha);
 
      // Preenche o DataSet com os dados da linha
      DataSet.AppendRecord([Linha[0], Linha[1], Linha[2]]);
    end;
  finally
    // Libera as variáveis da memória
    FreeAndNil(Linha);
    FreeAndNil(Valores);
  end;
end;
 
procedure TParserCSV.SetProximoParser(Parser: IParser);
begin
  // Atribui o próximo Parser
  ProximoParser := Parser;
end;

• XML

type
  { Concrete Handler - Processador de XML }
  TParserXML = class(TInterfacedObject, IParser)
  private
    // Referência para o Concrete Handler superior
    ProximoParser: IParser;
  public
    // Atribui a referência do Concrete Handler superior
    procedure SetProximoParser(Parser: IParser);
 
    // Método para processar a inclusão de dados no DataSet
    procedure ProcessarInclusao(const NomeArquivo: string; DataSet: TClientDataSet);
  end;
 
implementation
 
{ TParserXML }
 
procedure TParserXML.ProcessarInclusao(const NomeArquivo: string;
  DataSet: TClientDataSet);
var
  XMLDocument: IXMLDocument;
  NodeImportacao: IXMLNode;
  NodeDados: IXMLNode;
  Contador: Integer;
begin
  // Verifica se a extensão do arquivo é compatível com a função da classe
  if UpperCase(ExtractFileExt(NomeArquivo)) <> '.XML' then
  begin
    // Se não houver um Parser superior, significa que a mensagem chegou ao fim da cadeia
    if not Assigned(ProximoParser) then
      raise Exception.Create('Formato desconhecido.');
 
    // Transfere a mensagem para o próximo Parser (Concrete Handler)
    ProximoParser.ProcessarInclusao(NomeArquivo, DataSet);
    Exit;
  end;
 
  // Carrega e abre o arquivo XML
  XMLDocument := LoadXMLDocument(NomeArquivo);
  XMLDocument.Active := True;
 
  // Seleciona o nó principal do XML (importacao)
  NodeImportacao := XMLDocument.DocumentElement;
 
  // Executa um loop nos filhos do nó principal
  for Contador := 0 to Pred(NodeImportacao.ChildNodes.Count) do
  begin
    // Acessa o nó filho atual
    NodeDados := NodeImportacao.ChildNodes[Contador];
 
    // Preenche o DataSet com os dados do nó
    DataSet.Append;
    DataSet.FieldByName('Codigo').AsString := NodeDados.ChildNodes['codigo'].Text;
    DataSet.FieldByName('Nome').AsString := NodeDados.ChildNodes['nome'].Text;
    DataSet.FieldByName('Cidade').AsString := NodeDados.ChildNodes['cidade'].Text;
    DataSet.Post;
  end;
end;
 
procedure TParserXML.SetProximoParser(Parser: IParser);
begin
  // Atribui o próximo Parser
  ProximoParser := Parser;
end;

• JSON

type
  { Concrete Handler - Processaor de JSON }
  TParserJSON = class(TInterfacedObject, IParser)
  private
    // Referência para o Concrete Handler superior
    ProximoParser: IParser;
  public
    // Atribui a referência do Concrete Handler superior
    procedure SetProximoParser(Parser: IParser);
 
    // Método para processar a inclusão de dados no DataSet
    procedure ProcessarInclusao(const NomeArquivo: string; DataSet: TClientDataSet);
  end;
 
implementation
 
{ TParserJSON }
 
procedure TParserJSON.ProcessarInclusao(const NomeArquivo: string; DataSet: TClientDataSet);
var
  Valores: TStringList;
  JSON: TJSONObject;
  ArrayDados: TJSONArray;
  Contador: integer;
begin
  // Verifica se a extensão do arquivo é compatível com a função da classe
  if UpperCase(ExtractFileExt(NomeArquivo)) <> '.JSON' then
  begin
    // Se não houver um Parser superior, significa que a mensagem chegou ao fim da cadeia
    if not Assigned(ProximoParser) then
      raise Exception.Create('Formato desconhecido.');
 
    // Transfere a mensagem para o próximo Parser (Concrete Handler)
    ProximoParser.ProcessarInclusao(NomeArquivo, DataSet);
    Exit;
  end;
 
  // Cria a TStringList que irá carregar o arquivo selecionado
  Valores := TStringList.Create;
  try
    // Carrega o arquivo
    Valores.LoadFromFile(NomeArquivo);
 
    // Interpreta o conteúdo do arquivo como JSON
    JSON := TJSONObject.ParseJSONValue(TEncoding.UTF8.GetBytes(Valores.Text),0) as TJSONObject;
 
    // Seleciona o array "dados" do JSON
    ArrayDados := JSON.GetValue('dados') as TJSONArray;
 
    // Executa um loop nos itens do array
    for Contador := 0 to Pred(ArrayDados.Count) do
    begin
      // Converte o item atual do array para um objeto JSON
      JSON := ArrayDados.Items[Contador] as TJSONObject;
 
      // Preenche o DataSet acessando os pares do item do array
      DataSet.Append;
      DataSet.FieldByName('Codigo').AsString := JSON.Pairs[0].JsonValue.ToString;
      DataSet.FieldByName('Nome').AsString := JSON.Pairs[1].JsonValue.Value;
      DataSet.FieldByName('Cidade').AsString := JSON.Pairs[2].JsonValue.Value;
      DataSet.Post;
    end;
  finally
    // Libera a variável da memória
    FreeAndNil(Valores);
  end;
end;
 
procedure TParserJSON.SetProximoParser(Parser: IParser);
begin
  // Atribui o próximo Parser
  ProximoParser := Parser;
end;

 

Nossa codificação já está praticamente pronta! O nosso Client será o botão “Processar Inclusão” da tela abaixo:

Neste botão, faremos a última etapa – e mais importante – do padrão de projeto: “montar” a nossa cadeia da forma como desejamos. Na codificação abaixo, configurei a hierarquia CSV -> XML -> JSON:

var
  // Variáveis do tipo da Interface
  // para utilização do recurso de contagem de referência
  ParserCSV: IParser;
  ParserXML: IParser;
  ParserJSON: IParser;
begin
  // Abre o OpenDialog para seleção do arquivo
  if not OpenDialog.Execute then
    Exit;
 
  // Cria os Parsers (Concrete Handlers)
  ParserCSV := TParserCSV.Create;
  ParserXML := TParserXML.Create;
  ParserJSON := TParserJSON.Create;
 
  // Configura a hierarquia horizontal dos Parsers
  ParserCSV.SetProximoParser(ParserXML);
  ParserXML.SetProximoParser(ParserJSON);
  ParserJSON.SetProximoParser(nil);
 
  // Limpa o DataSet
  ClientDataSet.EmptyDataSet;
 
  // Inicia a cadeia pelo primeiro elo, ou seja, o mais provável ou comum
  ParserCSV.ProcessarInclusao(OpenDialog.FileName, ClientDataSet);
end;

Perfeito! Os arquivos com formato CSV, XML e JSON serão processados pelo 1º, 2º e 3º elos, respectivamente. Observe que, no caso do JSON, a mensagem passará pelos dois primeiros elos da cadeia, mas não será processada, já que não é “interpretável” pelas classes.
Bom, agora podemos enviar a mesma versão da aplicação para todos os clientes com segurança. A importação de dados funcionará com sucesso, independente de qual formato de arquivo de importação que o cliente trabalha!
Neste exemplo, podemos identificar mais algumas vantagens. Em primeiro lugar, podemos adicionar ou remover elos da cadeia sem quebrar a funcionalidade, bem como trocá-los de ordem. Em segundo lugar, cada classe não possui vínculo com as outras, evitando a Dependência Magnética quando uma alteração é necessária em uma delas. Por último, respeitamos as características de baixo acoplamento e alta coesão! 🙂

 

Como de costume, disponibilizei o projeto de exemplo deste artigo para download. Só há uma pequena modificação: codifiquei mais um Parser para processar arquivos TXT!
Como bônus,  adicionei 4 arquivos com dados dentro do diretório da aplicação, um em cada formato, para facilitar os testes!

Exemplo de Chain of Responsibility com Delphi

 

Esse foi bem fácil, né?
Prepare-se para o próximo Design Pattern! Até lá!


 

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

6 comentários

    1. Muito obrigado, André!
      O padrão de projeto realmente seria bem adequado para cálculo de impostos. Bem pensado!
      Vamos incentivar, aos poucos, o uso das boas práticas da Engenharia de Software!

      Abraço!

  1. André, bom dia!
    Muito bons são seus artigos de padrões!
    Uma dúvida, o padrão descrito no artigo não seria um padrão Comportamental segundo do GOF?

  2. Que isso! Não há problemas! Eu que tenho a agradecer ao blog! As explicações de padrões de projetos com exemplos práticos são excelentes!
    Abraço!

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.