Classes Mediadoras (interposer)
Escrito por Carlos B. Feitoza Filho | |
Categoria: Artigos | |
Categoria Pai: Addicted 2 Delphi! | |
Acessos: 10204 |
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:
- A declaração original de TEdit está contida em Vcl.StdCtrls (cláusula uses)
- A declaração da nossa Classe Mediadora está em TFormPrincipal (abaixo da cláusula uses)
- 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:
- Declaração da classe Vcl.StdCtrls.TEdit
- Declaração da classe UFormPrincipal.TEdit, que herda da classe Vcl.StdCtrls.TEdit
- 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, FGreatherThanColor e FReferenceNumber).
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:
- Não é possível aplicar CMs a componentes específicos de um mesmo tipo. No nosso exemplo, se tivéssemos mais componentes TEdit, todos eles seriam herdados de nossa CM e teriam o mesmo comportamento. Se você precisa aplicar comportamentos a apenas alguns componentes específicos você pode adicionar flags à CM para que ela execute ou não determinado comportamento, ou, preferencialmente você deve criar um componente, que é uma forma bem mais limpa. Os flags deixarão seu código complexo.
- Não é possível visualizar no Object Inspector propriedades e eventos criados em CMs na seção published. Isso acontece porque para que propriedades e eventos apareçam no OI, é necessário que o componente seja instalado, logo, a única forma de fazer isso é criando um componente num pacote (dpk) e instalá-lo na IDE.
- Eventos que não possuem manipuladores virtuais internos correspondentes precisam de tratamento especial. Em nosso exemplo os métodos KeyPress e Change são os responsáveis por disparar os eventos OnKeyPress e OnChange, respectivamente, por este motivo a codificação ficou simplificada, pois bastou sobrescrever estes métodos. Alguns eventos não possuem métodos disparadores e algumas vezes estes métodos não são virtuais. Em ambos os casos não é possível sobrescrevê-los na CM de forma direta e por isso precisamos de codificação extra, que apesar de não ser tão complicada, foi deixada de fora deste artigo pois não é o foco do mesmo.
- É necessário cuidado especial com as cláusulas uses. Se seu projeto tiver muitas CMs em units distintas, você deve cuidar para que as referências a elas estejam sempre no final das cláusulas uses ou, mais especificamente, após a declaração da unit que contém a classe pai da CM em questão. Isso pode ser um fardo para alguns, por isso o citei como desvantagem.