Mostrando postagens com marcador oop. Mostrar todas as postagens
Mostrando postagens com marcador oop. Mostrar todas as postagens

terça-feira, 21 de fevereiro de 2012

Trabalhando com Atributos (Custom Attributes)

Hoje gostaria de falar sobre Atributos, mais especificamente Custom Attributes e compartilhar uma experiência recente onde esta facilidade foi crucial.

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.

quinta-feira, 20 de janeiro de 2011

Classes aninhadas: um uso prático

Olá!

Há um recurso muito interessante no Object Pascal que é a classe aninhada - nested classes em inglês. É um recurso existente em Java e .NET a tempos.

A grosso modo permite você implementar uma classe dentro de outra. O ganho disso é você conseguir elaborar uma classe "auto-suficiente", protegendo - se for este o caso - as soluções que você desenvolveu.

Bom, indo ao ponto. Desenvolvi a unit abaixo exemplificando a implementação de uma classe aninhada. Para efeitos didáticos o objetivo do programa é o seguinte:

Implementar um liberador de objetos.

Para isso, desenvolvi a classe TLiberador que é descendente de TObject. Ela implementa, então, a classe TLiberadorThread que por sua vez descende de TThread.

Logo, TLiberadorThread só é visível pela classe TLiberador e é isso que é o pulo do gato.

Compartilho da opinião que a legibilidade do código fica inicialmente comprometida mas creio ser uma questão de adaptação.

unit Unit2;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, SyncObjs, Contnrs, ComCtrls, Unt_MemoryMonitor,
  Unt_RotinasLog;

type

  {
    Classe responsável por liberar objetos
  }
  TLiberador = class(TObject)
  strict private type {<== Declaração de tipo dentro da classe!}
    TLiberadorThread = class(TThread)
    strict private
      FQueue: TObjectQueue;
      FCritical: TCriticalSection;
      procedure WhenTerminate(Sender: TObject);
    protected
      procedure Execute; override;
    public
      procedure AfterConstruction; override;
      procedure BeforeDestruction; override;
      procedure AddObject(poObject: TObject);
    end;
  strict private
    FDetail: TLiberadorThread; {<== Instância da classe aninhadaS}
  public
    procedure AfterConstruction; override;
    procedure BeforeDestruction; override;
    procedure AddObject(poObject: TObject);
  end;

  {
    Formulário com um botão que adiciona um objeto na pilha
  }
  TForm2 = class(TForm)
    Button1: TButton;
    g4MemoryMonitor1: Tg4MemoryMonitor;
    StatusBar1: TStatusBar;
    procedure FormCreate(Sender: TObject);
    procedure FormDestroy(Sender: TObject);
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
  public
    FTeste: TLiberador;
    { Public declarations }
  end;

var
  Form2: TForm2;

implementation

{$R *.dfm}

{ TLiberador.TLiberadorThread }

procedure TLiberador.TLiberadorThread.AddObject(poObject: TObject);
begin
  Self.FCritical.Enter;
  try
    Self.FQueue.Push(poObject);
  finally
    Self.FCritical.Release;
  end;
end;

procedure TLiberador.TLiberadorThread.AfterConstruction;
begin
  inherited;
  Self.OnTerminate := Self.WhenTerminate;
  Self.FQueue := TObjectQueue.Create;
  Self.FCritical := TCriticalSection.Create;
end;

procedure TLiberador.TLiberadorThread.BeforeDestruction;
begin
  inherited;
  Self.FQueue.Free;
  Self.FCritical.Free;
end;

procedure TLiberador.TLiberadorThread.Execute;
begin
  inherited;
  NameThreadForDebugging(AnsiString(Self.ToString));
  IdentificarPID(Self.ToString);
  while not (Self.Terminated) do
  begin
    Sleep(10);
    if (Self.FQueue.Count > 0) then
    begin
      Self.FQueue.Pop.Free;
    end;
  end;
end;

procedure TLiberador.TLiberadorThread.WhenTerminate(Sender: TObject);
begin
  Sleep(10);
end;

{ TLiberador }

procedure TLiberador.AddObject(poObject: TObject);
begin
  Self.FDetail.AddObject(poObject);
end;

procedure TLiberador.AfterConstruction;
begin
  inherited;
  Self.FDetail := TLiberadorThread.Create(True);
  Self.FDetail.Start;
end;

procedure TLiberador.BeforeDestruction;
begin
  inherited;
  Self.FDetail.Terminate;
  if not (Self.FDetail.Suspended) then
    Self.FDetail.WaitFor;
  Self.FDetail.Free;
end;

{ TForm2 }

procedure TForm2.Button1Click(Sender: TObject);
begin
  Self.FTeste.AddObject(TObject.Create);
end;

procedure TForm2.FormCreate(Sender: TObject);
begin
  Self.g4MemoryMonitor1.Active := True;
  Self.FTeste := TLiberador.Create;
end;

procedure TForm2.FormDestroy(Sender: TObject);
begin
  Self.FTeste.Free;
end;

end.


Existem outras novidades que talvez os programadores Delphi da "old school" não conheçam (a começar por mim). Dê uma olhada neste link: http://edn.embarcadero.com/article/34324

terça-feira, 8 de junho de 2010

AfterConstruction & BeforeDestruction

Existe uma opção à reescrever o constructor e o destructor da sua classe.

É implementar na sua classe os métodos AfterConstruction e BeforeConstruction, como no exemplo abaixo:

TMyClasst = class(TObject)
public
  procedure AfterConstruction; override;
  procedure BeforeDestruction; override;
end;

Bom, é isso.

Minha lista de blogs