Olá, leitores! Já estamos no 14º artigo sobre Design Patterns!
Dentre todos os padrões de projetos já abordados até o momento, o Command, que será visto neste artigo, foi um dos mais custosos para compreender. Não pelo propósito, mas pela arquitetura de classes que são exigidas e também pela empregabilidade em um ambiente real. Procurei ser o mais objetivo possível para evitar que o artigo ficasse muito extenso. Check it out!
Introdução
Um fato bem interessante que observei ao estudar os padrões de projeto comportamentais é a possibilidade de “personalizar” as ações dos objetos. No artigo anterior, sobre o Chain of Responsibility, por exemplo, nos é concedida a liberdade de compor uma cadeia de responsabilidades por qualquer objeto e em qualquer ordem. Neste artigo você também irá notar essa mesma natureza de customização de ações.
O objeto principal do Command, em poucas palavras, é encapsular requisições como objetos, possibilitando a criação de filas de comandos a serem executados sob determinada ordem. Logo, podemos “empilhar” vários comandos e executá-los de uma só vez somente quando necessário.
Analogia
Para introduzir o conceito do Command, considere um wizard de instalação de um programa, como o da imagem abaixo:
Para muitos, esses instaladores são popularmente conhecidos como “Next, Next, Next, Finish”. 😛
Pois bem, através destes wizards, sabemos que é possível personalizar a instalação do programa, selecionando as permissões, pasta de cópia dos arquivos, recursos que serão instalados e criação de atalhos, certo? Porém, lembre-se que a instalação, de fato, só ocorre quando o usuário pressiona o botão “Finalizar” (ou Finish). Isso significa que o wizard “empilha” os comandos (que são as opções de instalação) e somente os executa sob a ação do botão “Finalizar”.
Bom, de forma bem simples, estamos basicamente falando de uma aplicação tradicional do padrão de projeto Command. Esse procedimento poderia ser traduzido no código abaixo:
1 2 3 4 5 |
Instalador.Add(ComandoConfigurarPermissoes); Instalador.Add(ComandoCopiarArquivos); Instalador.Add(ComandoCriarAtalhos); Instalador.InstalarPrograma; |
Utilizar o Command é o mesmo que dizer: “Tenho várias operações preparadas, mas elas só serão executados sob meu comando!” . Interessante, não?
Embora o conceito esteja um pouco mais claro, ainda não paramos por aqui. A teoria do padrão de projeto estabelece que os comandos, por si só, não executam nada. Cada comando deve estar associado a um objeto que, por fim, possui o conhecimento de como executar a operação. Portanto, existe mais um elemento nesse contexto, comentado logo a seguir.
Considere a seguinte analogia: trabalhamos em um setor de processamento de dados e devemos realizar todo o trâmite de emissão de nota fiscal quando um pedido de venda é recebido. Este pode ser um bom cenário para a aplicação do Command:
1 2 3 4 5 6 7 8 |
Pedido := TPedido.Create; Processamento := TProcessamento.Create; Processamento.Add(ComandoLancarMovimentoCaixa(Pedido)); Processamento.Add(ComandoEmitirNotaFiscal(Pedido)); Processamento.Add(ComandoEnviarParaSetorContabil(Pedido)); Processamento.ExecutarTarefas; |
Cada comando recebe a instância do objeto que utilizaremos na execução de nossas tarefas. Este objeto poderia, por exemplo, ser substituído por uma ordem de serviço, mas os comandos continuariam os mesmos. Do mesmo modo, é possível também adicionar novos comandos, como exportar a nota fiscal para um formato específico. Por esse motivo que mencionei no primeiro parágrafo que padrões dessa família nos permitem configurar ações.
Por que os comandos precisam estar atrelados a um objeto?
Os comandos atuam como “conexões” entre o cliente e as ações desejadas do objeto. Essa definição nos leva a um alto índice de desacoplamento entre classes, já que os comandos não conhecem os detalhes de implementação das ações que serão executadas. Os comandos, então, apenas recebem uma instância do objeto que possui as implementações e chamam um de seus métodos.
Na analogia acima, TPedido
, que é a classe que contém as implementações, é chamada de Receiver. Os comandos (lançar movimento do caixa, emitir nota fiscal e enviar para a contabilidade) são classes que implementam uma Interface chamada Command e são definidas como Concrete Commands. A classe TProcessamento
, que armazena a lista de comandos, recebe o nome de Invoker.
Exemplo de codificação do Command
A próximo parte do artigo é apresentar uma codificação prática do Command, no entanto, o exemplo não será um wizard de instalação e nem uma aplicação para processamento de pedidos.
Codificaremos um utilitário para extração de informações do computador, no qual pode ser útil para algumas finalidades. Por exemplo, imagine que uma equipe de suporte de uma empresa de software esteja recebendo vários chamados referentes à falhas em algumas rotinas do sistema. Para averiguar a causa, o analista de suporte poderá executar essa aplicação e extrair a lista de processos em execução, programas instalados e as variáveis de ambiente do computador do cliente. Chique, hein?
Essa aplicação terá a seguinte interface visual:
O botão “Executar Comandos” irá empilhar a lista de comandos para executá-los de uma só vez, extraindo todas as informações e carregando-as nos componentes TMemo
.
Classe Receiver
O primeiro passo é criar a classe Receiver, responsável por compreender a implementação de todas essas extrações. O código a seguir é um pouco extenso por englobar as três operações, porém, não deixa de servir como uma fonte de pesquisa de como extrair essas informações pelo Delphi. 🙂
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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
type { Receiver } TReceiver = class public procedure ExtrairProcessos; procedure ExtrairProgramas; procedure ExtrairVariaveisAmbiente; end; implementation uses Dialogs, Messages, Windows, TlHelp32, psAPI, SysUtils, Registry, Classes; { TReceiver } procedure TReceiver.ExtrairProcessos; var Handle: THandle; ProcessEntry: TProcessEntry32; ListaProcessos: TStringList; begin // Método responsável por extrair a lista de processos em execução Handle := CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); ListaProcessos := TStringList.Create; try ProcessEntry.dwSize := SizeOf(TProcessEntry32); Process32Next(Handle, ProcessEntry); // Percorre os processos que estão em execução, // adicionando-os na variável ListaProcessos repeat ListaProcessos.Add(ProcessEntry.szExeFile) until not Process32Next(Handle, ProcessEntry); // Grava a lista de processos no arquivo "Processos.txt" ListaProcessos.SaveToFile(GetCurrentDir + '\Processos.txt'); finally CloseHandle(Handle); end; end; procedure TReceiver.ExtrairProgramas; const CHAVE_REGISTRO = 'Software\Microsoft\Windows\CurrentVersion\Uninstall\ '; var Registro: TRegistry; Contador: integer; ListaChaves: TStringList; ListaProgramas: TStringList; begin // Método responsável por extrair a lista de programas instalados // Obs: a aplicação deve ser executada como Administrador Registro := TRegistry.Create; ListaChaves := TStringList.Create; ListaProgramas := TStringList.Create; try // Obtém a lista de chaves existentes no caminho do registro // que armazena as informações de programas instalados Registro.RootKey := HKEY_LOCAL_MACHINE; Registro.OpenKey(CHAVE_REGISTRO, False); Registro.GetKeyNames(ListaChaves); Registro.CloseKey; // Percorre a lista de chaves para acessar o valor do atributo "DisplayName" for Contador := 0 to Pred(ListaChaves.Count) do begin Registro.RootKey := HKEY_LOCAL_MACHINE; Registro.OpenKey(CHAVE_REGISTRO + ListaChaves[Contador], False); if Registro.ReadString('DisplayName') <> EmptyStr then // Adiciona o nome do programa na variável ListaProgramas ListaProgramas.Add(Registro.ReadString('DisplayName')); Registro.CloseKey; end; // Grava a lista de processos no arquivo "Programas.txt" ListaProgramas.SaveToFile(GetCurrentDir + '\Programas.txt'); finally // Libera os objetos da memória FreeAndNil(ListaProgramas); FreeAndNil(ListaChaves); FreeAndNil(Registro); end; end; procedure TReceiver.ExtrairVariaveisAmbiente; var Variavel: PChar; ListaVariaveis: TStringList; begin // Método responsável por extrair a lista das variáveis de ambiente ListaVariaveis := TStringList.Create; try // Obtém a lista das variáveis de ambiente Variavel := GetEnvironmentStrings; while Variavel^ <> #0 do begin ListaVariaveis.Add(StrPas(Variavel)); ListaVariaveis.Add(EmptyStr); Inc(Variavel, lStrLen(Variavel) + 1); end; FreeEnvironmentStrings(Variavel); // Grava a lista de variáveis no arquivo "Variaveis.txt" ListaVariaveis.SaveToFile(GetCurrentDir + '\Variaveis.txt'); finally // Libera o objeto da memória FreeAndNil(ListaVariaveis); end; end; |
Interface Command e classes Concrete Commands
No segundo passo, codificaremos a Interface Command, que representa uma abstração de todos os comandos que serão implementados. Vale destacar que essa Interface declara apenas um método – tradicionalmente chamado de Execute
– no qual irá acessar os métodos do Receiver.
1 2 3 4 5 6 |
type { Command } ICommand = interface ['{68D92B0C-3342-4BB2-9C69-5AA46069A467}'] procedure Execute; end; |
Em seguida, precisamos criar os comandos concretos, ou melhor, as classes Concrete Command, cada qual se responsabilizado por uma ação específica do Receiver:
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 |
type { Concrete Command } TProcessos = class(TInterfacedObject, ICommand) private // Variável para armazenar a referência do Receiver FReceiver: TReceiver; public constructor Create(Receiver: TReceiver); // Assinatura da Interface - Método de execução da operação procedure Execute; end; { Concrete Command } TProgramas = class(TInterfacedObject, ICommand) private // Variável para armazenar a referência do Receiver FReceiver: TReceiver; public constructor Create(Receiver: TReceiver); // Assinatura da Interface - Método de execução da operação procedure Execute; end; { Concrete Command } TVariaveisAmbiente = class(TInterfacedObject, ICommand) private // Variável para armazenar a referência do Receiver FReceiver: TReceiver; public constructor Create(Receiver: TReceiver); // Assinatura da Interface - Método de execução da operação procedure Execute; end; implementation { TProcessos } constructor TProcessos.Create(Receiver: TReceiver); begin FReceiver := Receiver; end; procedure TProcessos.Execute; begin FReceiver.ExtrairProcessos; end; { TProgramas } constructor TProgramas.Create(Receiver: TReceiver); begin FReceiver := Receiver; end; procedure TProgramas.Execute; begin FReceiver.ExtrairProgramas; end; { TRecursos } constructor TVariaveisAmbiente.Create(Receiver: TReceiver); begin FReceiver := Receiver; end; procedure TVariaveisAmbiente.Execute; begin FReceiver.ExtrairVariaveisAmbiente; end; |
Classe Invoker
O último passo é elaborar a classe Invoker, que possui uma codificação bem simples:
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 |
type { Invoker } TInvoker = class private // Lista para armzenar os comandos Comandos: TInterfaceList; public constructor Create; procedure Add(Comando: ICommand); procedure ExtrairInformacoes; end; implementation { TInvoker } procedure TInvoker.Add(Comando: ICommand); begin // Adiciona o comando na lista Comandos.Add(Comando); end; constructor TInvoker.Create; begin // Cria a lista que armazenará os comandos Comandos := TInterfaceList.Create; end; procedure TInvoker.ExtrairInformacoes; var Contador: integer; begin // Percorre a lista de comandos armazenados, // executando a operação de cada um for Contador := 0 to Pred(Comandos.Count) do begin ICommand(Comandos[Contador]).Execute; end; end; |
O código do botão “Executar Comandos” (do Client), conforme já dito anteriormente, será incumbido de criar as instâncias das classes acima e configurar as interações entre elas:
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 |
var Receiver: TReceiver; Invoker: TInvoker; begin // Cria o Receiver (objeto que contém a codificação das operações) Receiver := TReceiver.Create; // Cria o Invoker (objeto que armazena os comandos para executá-los) Invoker := TInvoker.Create; try // Armazena o comando de extração de programas no Invoker Invoker.Add(TProgramas.Create(Receiver)); // Armazena o comando de extração de processos no Invoker Invoker.Add(TProcessos.Create(Receiver)); // Armazena o comando de extração das variáveis de ambiente no Invoker Invoker.Add(TVariaveisAmbiente.Create(Receiver)); // Solicita ao Invoker que execute todos os comandos armazenados Invoker.ExtrairInformacoes; finally // Libera os objetos da memória FreeAndNil(Invoker); FreeAndNil(Receiver); end; MemoProgramas.Lines.LoadFromFile(GetCurrentDir + '\Programas.txt'); MemoProcessos.Lines.LoadFromFile(GetCurrentDir + '\Processos.txt'); MemoVariaveis.Lines.LoadFromFile(GetCurrentDir + '\Variaveis.txt'); end; |
Ao clicar no botão, toda as extrações são realizadas em apenas uma ação, salvando-as em disco, e depois carregando-as nos componentes do formulário. Novamente, se quisermos atribuir outro Receiver ou adicionar/remover comandos, o mecanismo de execução, em sua integridade, continuará funcionando, visto que não há dependências entre as classes. Uma ideia seria adicionar um novo comando para enviar os arquivos gerados para o e-mail do suporte. 😉
Conclusão
Tenho algumas ressalvas. Você deve ter notado que este padrão de projeto estimula o “micro gerenciamento” de classes. Por um lado, é um ponto negativo por exigir a criação de várias classes, tornando a arquitetura um pouco volumosa. Por outro lado, cada classe tem uma responsabilidade pequena e única, promovendo o desacoplamento e facilitando a manutenção.
Além disso, você pode estar se perguntando: “Por que precisamos de um Invoker? Não bastaria apenas instanciar o Receiver e executar os três métodos em sequência?”. Sim, você pode. Se houver um formulário no sistema que exiba apenas a lista de programas instalados, só o Receiver realmente já é o suficiente.
Porém, a empregabilidade do Command vai além dessa necessidade. O Invoker pode manter as instâncias armazenadas e executá-las em horários específicos, como um agendador de tarefas. Além disso, por ter controle da lista de comandos, o Invoker pode aplicar configurações adicionais, validações, controles de execução é até disponibilizar um recurso de desfazer comandos, que é mencionado em algumas literaturas do Command.
Clique no link abaixo para baixar o projeto de exemplo deste artigo. Para personalizar a lista de comandos, adicionei três componentes TCheckBox
, um para cada tipo de extração. Os comandos executados pelo Invoker serão apenas os que estão selecionados no formulário.
Experimente também adicionar novos Concrete Commands e/ou Receivers e observe a simplicidade de manutenção da arquitetura.
Espero que o artigo tenha sido útil, leitores!
Um grande abraço a todos!
Parabéns André
Fica fácil aplicar padrões de projeto no dia-a-dia depois de ler o seus artigos.
Muito obrigado, Edson!
Esse é o objetivo! Fiquei contente com o seu comentário!
Abraço!
Muito bons seus artigos, porém este padrão ficou um pouco confuso para mim, a função da unit “Pattern.ConcreteCommand”
Abraço.
Olá, Fábio, tudo bem?
Compreendo a confusão, já que este padrão de projeto é um dos mais complexos.
Analisando o código, poderíamos passar o Receiver diretamente para o Invoker, dispensando o Concrete Command, não é? Neste caso, o Concrete Command realmente aparenta ser desnecessário. No entanto, essa classe é responsável por aplicar possíveis regras e condições antes ou depois de chamar os métodos do Receiver. Este comportamento não ocorre no projeto de exemplo, mas pode ser comum em projetos em produção.
Por exemplo, imagine que, após extrair a lista de processos, seja necessário enviá-la para algum e-mail. Poderíamos codificar essa responsabilidade no Concrete Command:
Abraço!
Olá, muito bom o artigo, mas como ficaria para pegar o retorno por exemplo de uma classe que será executada via invoker?
EX: A classe abaixo precisava retornar se a nota foi emitida ou não!
Processamento.Add(ComandoEmitirNotaFiscal(Pedido));
Olá, Cassiano. Excelente pergunta!
A função do Invoker é exclusivamente empilhar a lista de comandos e executá-los posteriormente. Como todos os comandos são executados de uma só vez, não é possível obter um retorno de uma das funções e tratá-lo de forma particular.
Sendo assim, uma ideia é levantar uma exceção dentro da classe de comando (ConcreteCommand) caso a nota não tenha sido emitida, de modo que a execução dos comandos seja interrompida.
Cassiano, dependendo das regras da rotina, talvez seja mais viável utilizar o Chain of Responsibility.
Abraço!
André, tudo bem? Mais um grande e excelente artigo. Parabéns.
Uma dúvida, uma vez que eu tenha no Invoker, qual a necessidade do add? Tipo no caso eu teria que adicionar as bibliotecas somente para fazer o add, poderia e estaria correto, criar funções que fizesse isso dentro do próprio Invoker?
Olá, Claudiano! Ótima pergunta!
Sim, você pode fazer todo o processamento dentro do próprio Invoker, porém, ele só poderá ser utilizado em uma única funcionalidade, ou seja, você não conseguirá reaproveitá-lo em outras rotinas na sua aplicação. O objetivo do Invoker é atuar de forma genérica, sem processamentos específicos. Portanto, o seu objetivo é armazenar tarefas e executá-las, independente de quem são essas tarefas e de qual funcionalidade elas pertencem. Com isso, podemos reaproveitá-lo para diferentes ocasiões.
Abraço!