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. Acompanhe o artigo!
Introdução
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 refere-se à aprovação de um orçamento. Neste exemplo, o orçamento, que é a “mensagem”, é enviado primeiro para o gerente, que possui permissão para aprovar valores até 10 mil reais. Se o valor do pedido for maior, até 20 mil reais, é enviado para o próximo cargo na hierarquia que, neste caso, é o diretor. E finalmente, caso seja maior que 20 mil reais, o orçamento é enviado para o presidente. Observe que, se forem emitidos três pedidos nos valores de 6, 18 e 25 mil reais cada um, a aprovação será feita por pessoas diferentes.
Lembram-se que 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?
Exemplo de codificação do Chain of Responsibility
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
1 |
Código,Nome,Cidade |
• XML
1 2 3 4 5 6 7 |
<importacao> <dados> <codigo>Código</codigo> <nome>Nome</nome> <cidade>Cidade</cidade> </dados> </importacao> |
• JSON
1 2 3 4 5 6 7 8 9 |
{ "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.
Uma nota importante aqui: a hierarquia que discutimos até agora não precisa necessariamente ser vertical. No exemplo deste artigo, você verá que o fluxo da hierarquia é horizontal. O maior objetivo dessa hierarquia é separar as responsabilidades de cada classe e mantê-las desacopladas, mas, ao mesmo tempo, prover um mecanismo eficiente de processamento de dados.
Interface Handler e classes Concrete Handlers
A primeira etapa é criar a Interface que será comum para todos os “importadores de dados”, chamada Handler:
1 2 3 4 5 6 7 8 9 |
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
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 |
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
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 |
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
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 |
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; |
Em ação!
Nossa codificação já está praticamente pronta. O 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:
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 |
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.
Conclusão
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, atendemos à s 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. Além disso,  adicionei 4 arquivos com dados dentro do diretório da aplicação, um em cada formato, para facilitar os testes.
Esse foi bem fácil, né?
Preparem-se para o próximo Design Pattern! Até lá!
Esse padrão é top.
Uso ele no java, no Delphi fiz uma vez.
Se todo mundo usasse para regras de negócio de impostos seria top, masssss…..
parabéns pelo artigo
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!
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?
Bom dia, Elton!
Rapaz, fiz um grande equÃvoco! Já corrigi o artigo.
MuitÃssimo obrigado, Elton! Prometo que não vai ser repetir, rsrs.
Abraço!
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!
Obrigado pelo feedback, Elton!
Isso me motiva a continuar o trabalho no blog.
Abraço!
Bom dia, André tudo bem?
Estudando o Command Pattern em seu artigo notei que perguntaram para você a respeito de um retorno de umas chamadas e senão estiver engando você menciona da seguinte forma “a função desse padrão é para empilhar”, ou seja, executar todos métodos desta forma não seria possÃvel pegar o retorno, então você sugeriu o Chain of Responsibility, analisando sua excelente matéria neste artigo tomei a liberdade de usá-lo da seguinte forma, não executando um ou outro como demonstra o artigo, mas sim executando todos, pois quero pegar uma resposta de todos, fiz correto?
Pelo menos funcionando está (rs).
Grato por sua atenção, poucas pessoas explicam tão bem assim, sempre entendo seus exemplos.
Olá, Ronaldo, bom dia!
Em primeiro lugar, agradeço pelo feedback sobre os artigos e também pode deixar este comentário 🙂
Ronaldo, eu costumo dizer que não precisamos seguir os padrões de projeto “à risca” somente para atender o propósito teórico. Às vezes, esse fundamentalismo excessivo pode mais atrapalhar do que ajudar. Penso eu que podemos partir para uma opção mais prática e adaptar um padrão de projeto conforme a nossa necessidade, e foi justamente isso que você fez!
O objetivo do Chain of Responsibility é encaminhar uma mensagem para uma “corrente de classes” para que uma delas saiba como interpretá-la. No seu caso, você criou uma hierarquia de classes para executar um método especÃfico de cada uma delas. Tudo OK!
A única diferença que eu faria é alterar o método “Executar” de forma que ele seja executado em todos os parsers de uma vez só, ficando então dessa forma:
Abraço!
André, bom dia! Agradeço muito seu retorno e sua dica, mas vou te fazer uma pergunta idiota, mas não consegui pensar em como fazer uma única linha chamar todas como você mencionou. A ideia me parece excelente mas não consegui por em prática.
Olá, Ronaldo! Peço desculpas pela demora.
Não cheguei a fazer o teste, mas, como você tem referência ao próximo Parser dentro da classe, bastaria você fazer dessa forma dentro do método “Executar”:
Por exemplo, no método “Executar” da classe
TParserDeleteHistorico
, você teria esse código:É como se fosse basicamente uma hierarquia de heranças.
Abraço!
Bom dia, Muito obrigado André, funcionou perfeitamente, e pode ser útil numa necessidade de gerar vários procedimentos ao mesmo e tempo e esperando algum retorno como por exemplo copia de e-mail enviado para determinadas pessoas.
Grande abraço.
Isso mesmo, Ronaldo! Dá pra atender diversas necessidades.
Obrigado por retornar ao blog e deixar esse comentário!
Grande abraço!