Antes de mais nada quero esclarecer que não estou me referindo aos atributos de uma classe e sim de um conceito novo (Delphi 2010) mas já existente em outras plataformas de desenvolvimento.
Do que NÃO estamos falando:
TPessoa = class
private
FNome: string;
FIdade: Integer; {<== Isto é um atributo de uma classe e NÃO é disto que estamos falando}
public
property Nome: string read FNome write FNome;
property Idade: Integer read FIdade write FIdade;
end;
Do que estamos falando:
TPessoa = class
private
FNome: string;
public
property Nome: string read FNome write FNome;
[TIdadeMinima(21)] {<== É disto que estamos falando!}
property Idade: Byte read FIdade write FIdade;
end;
Mas o que significa atributo?
No dicionário:
S. m. 1. Aquilo que é próprio de um ser. 2. Emblema distintivo; símbolo. 3. Característica, qualitativa ou quantitativa, que identifica um membro de um conjunto observado. ...
Ou seja, atributo é, basicamente, uma qualidade atribuída a um elemento.
Usando no Delphi
O uso mais recorrente é em frameworks ORM. Mas as possibilidades são muitos mais amplas que isso. Poderíamos utilizar esta facilidade para atender regras de formatação de um documento eletrônico, como por exemplo o EFD Pis/Cofins ou um protocolo de comunicação com algum equipamento.Então, indo ao ponto, os atributos são classes descendentes de TCustomAttribute. O TCustomAttribute por sua vez não implementa nada de especial.
Seguindo o nosso exemplo inicial, o atributo TIdadeMinima seria declarado da seguinte maneira:
TIdadeMinima= class(TCustomAttribute)
private
FIdadeMinima: Byte;
public
constructor Create(AIdadeMinima: Byte);
function IsIdadeMaior(AIdade: Byte): Boolean;
property IdadeMinima: Byte read FIdadeMinima;
end;
//Implementação//
{ TIdadeMinima }
constructor TIdadeMinima.Create(AIdadeMinima: Byte);
begin
Self.FIdadeMinima := AIdadeMinima;
end;
function TIdadeMinima.IsIdadeMaior(AIdade: Byte): Boolean;
begin
Result := AIdade >= Self.FIdadeMinima;
end;
No que devemos focar?
O construtor da classe deve solicitar os parâmetros que fazem parte do escopo do atributo. No exemplo, o atributo está especificando uma idade mínima para a instância de TPessoa. Portanto o constructor do atributo pede justamente a idade mínima.
Para efetivamente tiramos proveito do atributo temos que recorrer à RTTI, pois só com ela temos acesso a um atributo. No exemplo abaixo, temos o código completo de um programa console, que pede o nome e a idade do usuário e valida a idade.
program UsandoAtributos;
{$APPTYPE CONSOLE}
{$R *.res}
uses
System.SysUtils, Rtti;
type
TIdadeMinima= class(TCustomAttribute)
private
FIdadeMinima: Byte;
public
constructor Create(AIdadeMinima: Byte);
function IsIdadeMaior(AIdade: Byte): Boolean;
property IdadeMinima: Byte read FIdadeMinima;
end;
TPessoa = class
private
FNome: string;
FIdade: Byte;
public
property Nome: string read FNome write FNome;
[TIdadeMinima(21)]
property Idade: Byte read FIdade write FIdade;
end;
{ TIdadeMinima }
constructor TIdadeMinima.Create(AIdadeMinima: Byte);
begin
Self.FIdadeMinima := AIdadeMinima;
end;
function TIdadeMinima.IsIdadeMaior(AIdade: Byte): Boolean;
begin
Result := AIdade >= Self.FIdadeMinima;
end;
var
oPessoa : TPessoa;
sNome : string;
bIdade : Byte;
_ctx : TRttiContext;
_typ : TRttiType;
_pro : TRttiProperty;
oAtt : TCustomAttribute;
begin
Writeln('****************************');
Writeln('*** Bem vindo ao sistema ***');
Writeln('****************************');
Writeln(EmptyStr);
oPessoa := nil;
try
oPessoa := TPessoa.Create;
Write('Digite o seu nome: ');
Readln(sNome);
oPessoa.Nome := sNome;
Write('Digite a sua idade: ');
Readln(bIdade);
oPessoa.Idade := bIdade;
Writeln(EmptyStr);
Writeln('Dados');
Writeln(oPessoa.Nome,' - ',oPessoa.Idade);
_ctx := TRttiContext.Create;
_typ := _ctx.GetType(oPessoa.ClassType);
for _pro in _typ.GetProperties do
begin
for oAtt in _pro.GetAttributes do
begin
if oAtt is TIdadeMinima then
begin
Writeln('A idade mínima é de: ',TIdadeMinima(oAtt).IdadeMinima);
if (TIdadeMinima(oAtt).IsIdadeMaior(oPessoa.Idade)) then
begin
Writeln('A idade de ',oPessoa.Nome,' passou!');
end else
begin
Writeln('A idade de ',oPessoa.Nome,' nao passou!');
end;
end;
end;
end;
finally
if (Assigned(oPessoa)) then
begin
oPessoa.Free;
end;
_ctx.Free;
end;
Writeln(EmptyStr);
Writeln('Tecle [ENTER] para encerrar');
Readln;
end.
Vale ressaltar que os atributos podem ser atribuídos a qualquer elemento da sua classe. É o seu código RTTI que terá que ser moldado para tirar proveito delas. Outro ponto importante é que um elemento pode ter vários atributos.
Uso prático
Como mencionei no decorrer, muitos exemplos sobre este tema esta ligado diretamente com a ORM, onde um atributo para a classe indica uma tabela enquanto que alguns atributos para as propriedades seriam os campos e as constraints.Mas no meu caso, onde precisei gerar um documento eletrônico (EFD Pis/Cofins) baseado em regras, eu criei uma classe base e para cada tipo de registro criei uma classe descendente com os campos e, principalmente, seus atributos: tamanho, tamanho exato, se somente números etc e etc. Por fim, na tal classe base, foi disponibilizado um método chamado GetLine que tanto valida as informações da classe quanto gera a linha correspondente. Esta solução foi ótima pois o código ficou mais limpo e tenho certeza que mudanças ou implementações futuras serão muito tranquilas.
Também poderíamos usar para implementar uma validação em uma tela de entrada, afinal, o TForm é uma classe como qualquer outra. Este seria um ótimo uso.
É isto.