Classes Mediadoras (interposer)

Categoria: Artigos
Categoria Pai: Addicted 2 Delphi!
Acessos: 9784
Imagem meramente ilustrativa

Se você tem um bom conhecimento de OO, você pode criar um componente derivado do componente que você quer estender, no qual você incluiria todas as características e comportamentos que você precisasse. Existem dois problemas nessa abordagem. O primeiro deles é que você teria que dar um nome diferente para a classe de seu componente derivado e, caso seu sistema já use extensivamente o componente que seria o pai do novo componente sendo criado, você seria obrigado a substituir todas as ocorrências do componente antigo pelo componente derivado para que seu programa tire proveito dos benefícios que você implementou e essa operação de troca de componentes pode ser maçante ou perigosa.

O segundo problema dessa abordagem, na verdade não é um problema em si, mas um incômodo. Ao criar um novo componente você precisa instalá-lo no Delphi para que ele possa ser usado. Apesar do procedimento de instalação de componentes ser bem simples, você seria obrigado a fazer isso toda vez que trocasse de ambiente.

Como então estender um componente sem precisar instalá-lo e principalmente mantendo o mesmo nome de classe? A solução para seu problema se chama Classe Mediadora (Interposer Class), doravante denominada simplesmente de CM.

Tinha uma classe no meio do caminho. No meio do caminho tinha uma classe

O que seriam então as CMs? Para entender melhor observemos o nome em inglês: Interposer Class. Class, eu não vou nem dizer o que é, logo, o que seria interposer afinal? Interposer é uma palavra que deriva do Latin "interpōnere" e que significa "aquilo que é colocado entre (duas coisas)". Uma CM, é, pois, uma classe que é colocada entre duas coisas. O nome não poderia ser mais autoexplicativo, mesmo assim vou explicar mais detalhadamente:

Uma Classe Mediadora (Interposer Class) é uma classe que, hierarquicamente falando, se posiciona entre uma classe ancestral (da qual ela deriva) e a declaração de um objeto do tipo desta classe. Possui como principal característica o fato de ter o mesmo nome da sua classe ancestral, o que torna sua aplicação simplificada em sistemas que já possuem objetos declarados do tipo dessa classe.

Eu sei que você não entendeu nada ainda. O conceito de CM é mais simples de se entender com exemplos do que com meras palavras, portanto, vamos logo pôr a mão na massa.


Usando Classes Mediadoras

Vamos supor que você tenha um componente TEdit e queira que ele, por padrão, só aceite números inteiros[1]. Além disso, você quer que a cor do texto mude de acordo com este número ser maior, menor ou igual a um número fixo também informado. Como resolver esse problema sem criar um novo componente? Observe inicialmente o TForm principal de uma aplicação simples:

unit UFormPrincipal;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TFormPrincipal = class(TForm)
    EDIT: TEdit;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  FormPrincipal: TFormPrincipal;

implementation

{$R *.dfm}

end.

Note que não há nada no TForm a não ser um único TEdit. Nossa intenção é fazer o TEdit se comportar do modo que queremos sem precisarmos criar um novo componente derivado de TEdit. Observe agora como fica esta unit após a introdução da CM para o TEdit:

unit UFormPrincipal;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TEdit = class(Vcl.StdCtrls.TEdit)

  end;

  TFormPrincipal = class(TForm)
    EDIT: TEdit;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  FormPrincipal: TFormPrincipal;

implementation

{$R *.dfm}

end.

As linhas 10 a 12 no fonte acima representam a CM vazia. Se você executar a aplicação verá que absolutamente nada mudou. O TEdit é exibido e funciona exatamente da mesma forma que um TEdit ordinário. Isso era de se esperar, já que ainda não introduzimos qualquer código na CM, mas serve para demonstrar o quanto o uso de uma CM é tranquilo. Não foi necessário fazer nada, a não ser declarar a CM entre a declaração original (contida em Vcl.StdCtrls) e a declaração do componente TEdit em TFormPrincipal. Aliás, é bom que isso seja bem destacado:

Para que uma CM funcione é imprescindível que a declaração da mesma esteja entre a declaração da classe original (classe ancestral) e a declara do objeto do tipo da CM. A interposer pode estar mesmo em uma unit separada, desde que esta unit esteja posicionada APÓS a unit que contém a classe ancestral na cláusula uses e desde que a cláusula uses em questão esteja ANTES da declaração dos objetos do tipo da CM.

Veja novamente o fonte acima e leia-o de cima pra baixo, da esquerda para a direita. Note que:

  1. A declaração original de TEdit está contida em Vcl.StdCtrls (cláusula uses)
  2. A declaração da nossa Classe Mediadora está em TFormPrincipal (abaixo da cláusula uses)
  3. A declaração do objeto do tipo de nossa Classe Mediadora (TEdit) está abaixo da declaração da nossa CM

Observando a lista acima fica claro que o nome "Interposer" faz todo sentido, pois nossa CM está ENTRE duas coisas, tal como foi dito no início deste artigo. Da forma como o fonte está agora com a CM vazia, se você removê-la, o código continua compilando! Para deixar isso mais claro, vou reescrever a lista anterior qualificando totalmente os nomes das classes com seus respectivos namespaces:

  1. Declaração da classe Vcl.StdCtrls.TEdit
  2. Declaração da classe UFormPrincipal.TEdit, que herda da classe Vcl.StdCtrls.TEdit
  3. Declaração de um objeto do tipo UFormPrincipal.TEdit

Olhando a lista acima, com a classe mediadora, EDIT é do tipo UFormPrincipal.TEdit. Sem a classe mediadora, EDIT automaticamente passa a ser Vcl.StdCtrls.TEdit. Acho que agora deu pra entender bem como as CMs funcionam, porque eu já estou sendo muito redundante.

Uma pergunta que pode surgir agora é: e se eu tiver dois TEdit no form, três, quatro, ou 100? Bom, é exatamente isso que você está pensando! Usando CMs, todos os componentes que vem depois de sua declaração serão objetos do tipo da CM e não mais objetos do tipo original. Isso é bom ou isso é ruim? Depende! Se você tivesse optado por criar um componente, você poderia usar o TEdit original ou seu TMyEdit (exemplo), mas teria que lidar com a instalação deste componente. Usando CMs todos os componentes do tipo TEdit se comportam do mesmo modo, mas como você viu, a inclusão de uma CM em um projeto não trás qualquer problema para código preexistente. Enfim, o uso de um componente ou de uma CM depende daquilo que você pretende. Choose Wisely!

Implementando a Classe Mediadora

Voltando ao nosso exemplo, vamos agora incrementar a classe mediadora para que ela faça o que queremos. A implementação não difere do que seria feito se tivéssemos feito um componente de forma correta, por exemplo. O legal da CM é que ela não tem nada de especial, a não ser o seu posicionamento específico dentro da hierarquia.

Primeiramente vamos fazer com que o nosso TEdit aceite apenas números. O Isso é bem manjado. Segue abaixo o código:

unit UFormPrincipal;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TEdit = class(Vcl.StdCtrls.TEdit)
  protected
    procedure KeyPress(var PKey: Char); override;
    procedure Loaded; override;
  public
  end;

  TFormPrincipal = class(TForm)
    EDIT: TEdit;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  FormPrincipal: TFormPrincipal;

implementation

{$R *.dfm}

{ TEdit }

procedure TEdit.KeyPress(var PKey: Char);
begin
  inherited;

  if not (PKey in ['0'..'9',#8,'-']) then
    PKey := #0;
end;

procedure TEdit.Loaded;
begin
  inherited;
  Text := '';
end;

end.

Quero abrir um parêntese para dizer que é evidente que você precisa ter conhecimentos sólidos de OO para usar CMs. Estes mesmos conhecimentos seriam necessários se você fosse fazer um componente, logo, vou considerar que você possui tais conhecimentos e abstrair maiores explicações. No código acima, nas linhas 12 e 13 foram sobrescritos dois métodos existentes em Vcl.StdCtrls.TEdit e hierarquias superiores.


O método KeyPress é o responsável por executar o evento OnKeyPress, no qual normalmente você inclui código para limitar os caracteres que são digitados num TEdit. Olhando a implementação do método KeyPress (linhas 34 a 40) vemos a codificação clássica que limita os caracteres sendo digitados. Isso não deve ser nenhuma novidade, o detalhe que merece destaque aqui é a presença de inherited no início do corpo do método. Eu falei inicialmente que o método KeyPress é o responsável por executar o evento OnKeyPress e isso é feito em algum ponto dentro da hierarquia de Vcl.StdCtrls.TEdit. Resumidamente, em algum pai de Vcl.StdCtrls.TEdit existe uma implementação do método KeyPress que faz com que o evento OnKeyPress seja executado. O inherited, como sabemos, executa o método onde ele estiver, no caso o KeyPress, no contexto da classe pai imediata da classe atual. Não entendeu? Vou explicar melhor, qualificando os nomes das classes com seus respectivos namespaces:

UFormPrincipal.TEdit, nossa CM, é filha imediata de Vcl.StdCtrls.TEdit. Ao sobrescrever (override) o método KeyPress de Vcl.StdCtrls.TEdit em UFormPrincipal.TEdit, toda vez que uma tecla for pressionada neste controle, o método UFormPrincipal.TEdit.KeyPress será executado primeiro. Dentro da implementação deste método, caso haja a palavra inherited, será executado o método Vcl.StdCtrls.TEdit.KeyPress. Usar a palvra inherited não é obrigatório, a não ser que você queira explicitamente executar o código original do método (no caso KeyPress). Como inherited executa Vcl.StdCtrls.TEdit.KeyPress, que no fim das contas é o responsável por executar o evento OnKeyPress, é desejável que ele seja usado, do contrário, um possível código colocado em OnKeyPress não será executado!

A posição do inherited é também importante. Note que ele está no início da implementação, o que significa que nosso código será executado DEPOIS que o evento OnKeyPress for executado. A intenção de colocar o código depois da execução do evento é impedir que o comportamento pretendido (permitir apenas números) seja alterado no evento OnKeyPress. Caso removêssemos o inherited, o evento OnKeyPress jamais seria executado e nossa inteção com o exemplo não é criar uma CM limitante, mas sim uma que estenda as capacidades da classe pai e introduza comportamentos padrão imutáveis. Manter o evento OnKeyPress permite que o desenvolvedor ainda possa usá-lo como quiser, ele só não será capaz de mudar o comportamento padrão de aceitar apenas números.

O outro método (Loaded), na linha 13, foi sobrescrito porque ao executar o exemplo eu notei que o campo era automaticamente preenchido com o texto contido na propriedade Text, o qual foi setado em tempo de projeto. Esse é o comportamento correto do componente, como se sabe, mas no nosso caso, como a intenção do componente é exibir apenas números eu teria duas opções, ou tratar a propriedade para que ela não aceite letras ou zerar o valor da propriedade Text. Eu optei pela segunda opção para manter esse exemplo simples e focado apenas no conceito da CM. O outro motivo é que CMs não interagem com a IDE e portanto, não seria possível realizar uma validação adequada daquilo que foi digitado no Object Inspector.

O método Loaded é um método especial, declarado na classe TComponent, ancestral de todos os componentes visuais e não visuais. Ele é executado para cada componente APÓS o carregamento das propriedades que foram salvas no arquivo .dfm. Em outras palavras, todas aquelas propriedades que foram definidas no Object Inspector, em tempo de projeto, são salvas no .dfm do TForm, TDataModule ou TFrame e quando um deles for criado em tempo de execução, após a criação (método construtor), o sistema de carregamento começa a carregar todas as propriedades a paritir do arquivo .dfm. Assim que todas as propriedades de um componente forem carregadas, o método Loaded é executado, portanto, este método é ideal para sobrescrever algumas propriedades que foram setadas em tempo de projeto.

As linhas 42 a 46 mostram a implementação do método Loaded, onde eu simplesmente limpo o conteúdo da propriedade Text. Note que eu coloquei o código APÓS o inherited e o motivo disso é o mesmo já falado anteriormente; o inherited vai executar o método Vcl.StdCtrls.TEdit.Loaded o qual, por padrão, vai carregar todas as propriedades definidas em tempo de projeto. Colocando o nosso código APÓS o inherited nós garantimos que a propriedade Text estará preenchida com o valor indicado e não com aquilo que foi carregado pelo inherited (Vcl.StdCtrls.TEdit.Loaded), descartando assim qualquer valor que tenha sido informado no Object Inspector.

Ao executar este exemplo, do jeito como ele está no momento, uma janela será exibida com um TEdit que só aceita números. Vamos agora implementar mais uma parte de nossa CM. Veja o fonte abaixo:

unit UFormPrincipal;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TEdit = class(Vcl.StdCtrls.TEdit)
  private
    FReferenceNumber: Integer;
    FGreatherThanColor: TColor;
    FLessThanColor: TColor;
    FEqualColor: TColor;
  protected
    procedure KeyPress(var PKey: Char); override;
    procedure Loaded; override;
  public
    constructor Create(POwner: TComponent); override;
    property GreatherThanColor: TColor read FGreatherThanColor write FGreatherThanColor;
    property LessThanColor: TColor read FLessThanColor write FLessThanColor;
    property EqualColor: TColor read FEqualColor write FEqualColor;
    property ReferenceNumber: Integer read FReferenceNumber write FReferenceNumber;
  end;

  TFormPrincipal = class(TForm)
    EDIT: TEdit;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  FormPrincipal: TFormPrincipal;

implementation

{$R *.dfm}

{ TEdit }

constructor TEdit.Create(POwner: TComponent);
begin
  inherited;
  FLessThanColor := clRed;
  FEqualColor := clBlue;
  FGreatherThanColor := clGreen;
  FReferenceNumber := 0;
end;

procedure TEdit.KeyPress(var PKey: Char);
begin
  inherited;

  if not (PKey in ['0'..'9',#8,'-']) then
    PKey := #0;
end;

procedure TEdit.Loaded;
begin
  inherited;
  Text := '';
end;

end.

Neste código nós declaramos quatro novas propriedades (linhas 21 a 24). Estas propriedades vão armazenar as informações necessárias para a mudança de cores, basicamente existem 3 propriedades para definição das cores em si (GreatherThanColor, LessThanColor e EqualColor) e uma que representa o número que será usado como referência, a partir do qual será feita a mudança de cores (ReferenceNumber). Também foi codificado um construtor (linhas 44 a 51) onde estamos simplesmente definindo valores iniciais para as propriedades, representadas aqui por seus campos privados correspondentes (FLessThanColor, FEqualColor, FGreatherThanColorFReferenceNumber).


Essa nova codificação não faz nada de fato, mas a seguir teremos a codificação que fará uso de todas as propriedades criadas no passo anterior:

unit UFormPrincipal;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  TEdit = class(Vcl.StdCtrls.TEdit)
  private
    FReferenceNumber: Integer;
    FGreatherThanColor: TColor;
    FLessThanColor: TColor;
    FEqualColor: TColor;
  protected
    procedure KeyPress(var PKey: Char); override;
    procedure Loaded; override;
    procedure Change; override;
  public
    constructor Create(POwner: TComponent); override;
    property GreatherThanColor: TColor read FGreatherThanColor write FGreatherThanColor;
    property LessThanColor: TColor read FLessThanColor write FLessThanColor;
    property EqualColor: TColor read FEqualColor write FEqualColor;
    property ReferenceNumber: Integer read FReferenceNumber write FReferenceNumber;
  end;

  TFormPrincipal = class(TForm)
    EDIT: TEdit;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  FormPrincipal: TFormPrincipal;

implementation

{$R *.dfm}

{ TEdit }

procedure TEdit.Change;
var
  Value: Integer;
begin
  inherited;

  if TryStrToInt(Text,Value) then
  begin
    if Value > FReferenceNumber then
      Font.Color := FGreatherThanColor
    else if Value < FReferenceNumber then
      Font.Color := FLessThanColor
    else
      Font.Color := FEqualColor;
  end;
end;

constructor TEdit.Create(POwner: TComponent);
begin
  inherited;
  FLessThanColor := clRed;
  FEqualColor := clBlue;
  FGreatherThanColor := clGreen;
  FReferenceNumber := 0;
end;

procedure TEdit.KeyPress(var PKey: Char);
begin
  inherited;

  if not (PKey in ['0'..'9',#8,'-']) then
    PKey := #0;
end;

procedure TEdit.Loaded;
begin
  inherited;
  Text := '';
end;

procedure TFormPrincipal.FormCreate(Sender: TObject);
begin
  EDIT.EqualColor := clSilver;
end;

end.

Indo direto ao ponto, na linha 19 nós sobrescrevemos o método Change. De forma análoga aos outros métodos sobrescritos, esse método é executado sempre que o conteúdo do TEdit muda e ele é o responsável por disparar o evento OnChange que seria um local adequado para a codificação da mudança de cor. As linhas 46 a 61 mostram sua codificação que simplesmente verifica o valor da propriedade Text, converte de forma segura este valor para um inteiro (TryStrToInt) e, caso a conversão tenha sido bem sucedida, compara o valor convertido com o valor de referência (FReferenceNumber), atribuindo a cor adequada à fonte em cada situação.


Anteriormente, no método construtor de nossa CM eu havia dado valores padrão para cada propriedade. Para demonstrar a forma de utilização dessas propriedades, que é bem trivial, diga-se, eu, no evento OnCreate do TForm (linhas 86 a 89), altero a cor de umas das propriedades de cores. Eu poderia alterar qualquer propriedade de forma usual em qualquer lugar que eu quisesse.

Ao executar o programa de exemplo ele já funciona como deveria, mas existe mais uma coisa que pode ser feita para deixar seu código mais limpo. Que tal remover a CM da unit do TForm, colocando-a em sua própria unit? Isso é perfeitamente possível! Você, como bom entendedor de OO no Delphi sabe como mover uma classe para outra unit então não vou explicar isso aqui (olhe os fontes do exemplo anexado a este artigo).

Abaixo está apenas a unit do TForm fazendo referência à unit da classe mediadora (UEditInterposer), através da cláusula uses. Note que a unit da classe mediadora aparece na cláusula uses APÓS a unit Vcl.StdCtrls. Isso é necessário porque nossa CM depende de Vcl.StdCtrls. A regra geral é sempre colocar as units que contém CMs no final das cláusulas uses.

unit UFormPrincipal;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, UEditInterposer;

type
  TFormPrincipal = class(TForm)
    EDIT: TEdit;
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  FormPrincipal: TFormPrincipal;

implementation

{$R *.dfm}

procedure TFormPrincipal.FormCreate(Sender: TObject);
begin
  EDIT.EqualColor := clSilver;
end;

end.

Bem melhor, não é? Cada coisa em seu devido lugar! Se você criasse mais CMs em units, todas elas precisam ser postas no final da cláusula uses. Não esqueça isso e tudo dará certo!

Desvantagens

As Classes Mediadoras são um recurso interessante e poderoso quando usadas de forma adequada. Vale ressaltar que se vários projetos distintos usam sempre as mesmas CMs pode ser mais vantajoso criar componentes a partir dessas classes. Componentes são muito mais robustos e cheios de recursos do que as CMs. Além disso, as CMs possuem algumas pequenas desvantagens que valem a pena serem citadas para que você, caro leitor, não as comece a usar como se não houvesse amanhã. Seguem algumas pequenas desvantagens:


  Arquivos anexados  
Arquivo Descrição Tamanho Modificado em
Download this file (Interposers.zip) CLME Pequeno exemplo que demonstra a criação e o uso de uma Classe Mediadora 57 KB 04/11/2016 às 23:57

1 O componente TEdit nos Delphis mais novos já possui uma propriedade NumbersOnly. Eu só descobri isso enquanto fazia a codificação do exemplo. O que vale aqui é o conceito das Classes Mediadoras. O exemplo é um mero coadjuvante