[Delphi] Design Patterns GoF – Command

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!

 

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 passado, 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.

 

Não entendi…
Sem problemas. 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:

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:

Pedido := TPedido.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, o “Pedido”, 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 Command. A classe “Processamento”, que armazena a lista de comandos, recebe o nome de Invoker. Só a título de curiosidade, quando estudei o padrão, lembrei do Invoker do Dota 2:

 

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 sistema de extração de informações do computador, no qual pode ser útil para algumas funcionalidades. 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 a nossa 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.
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. 🙂

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;

 

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.

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:

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;

 

O último passo é elaborar a classe Invoker do Dota 2, que possui uma codificação bem simples:

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:

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. 😉

 

Eu sinto que você tem algumas ressalvas sobre o Command…
Tenho, sim. 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.

Exemplo de Command com Delphi

 

Espero que o artigo tenha sido útil, leitores!
Um grande abraço a todos!


 

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

2 comentários

  1. Parabéns André
    Fica fácil aplicar padrões de projeto no dia-a-dia depois de ler o seus artigos.

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.