Este artigo aborda a orientação a objetos com o Delphi, usando uma metodologia simples, didática, de fácil aprendizado. Veremos na teoria, e também na prática, todos os conceitos, fundamentos e recursos oferecidos pelo Delphi para promover a POO. A POO pode e deve ser aplicada de forma inteligente, ela serve para construir sistemas mais robustos, mais confiáveis, de fácil manutenção, que permitam maior reaproveitamento de código.
A POO é útil em qualquer sistema, seja ele Web, Desktop, Mobile, não importa o tipo de aplicação. Os conceitos aqui apresentados, em um exemplo simples, podem ser utilizados em aplicações reais, como é apresentado em um estudo de caso no final desta série. Neste artigo aprenderemos mais sobre métodos virtuais e polimorfismo, dando um passo além, estudando métodos abstratos. Também examinaremos o que são construtores e destrutores. Entendermos o que são e para que servem os especificadores de visibilidade (modificadores) public, protected e private. Criaremos propriedades para nossas classes e reforçaremos princípios básicos de encapsulamento. E finalmente, entenderemos o que são métodos get / set.
Nas partes anteriores deste mini-curso, aprendemos importantes técnicas da orientação a objetos com o Delphi. Vimos como criar classes, aplicar a herança (TCarro e TAviao agora herdam de TMeioTransporte), vimos como criar métodos virtuais e aplicar o polimorfismo, como sobrescrever métodos virtuais e chamar funcionalidades da classe base com inherited. Nesta terceira parte do nosso curso, vamos avançar mais na orientação a objetos, chegando em um nível muito próximo de aplicações reais, inclusive fazendo novamente comparações com o design de classes da VCL.
Métodos abstratos
Se lembrar da última parte do curso, implementamos no método Mover a funcionalidade básica para todos os meios de transporte, que é o ato de ligar. Mas imagine o seguinte: e se cada meio de transporte for ligado de uma forma? O que é comum nesse caso é a chamada do método, ou seja, todos devem ser ligados antes de entrar em movimento. O que muda é a maneira como são ligados (já viu que vamos falar de polimorfismo novamente). Então altere o código do método Mover da classe TMeioTransporte como mostrado na Listagem 1.
procedure TMeioTransporte.Mover();
begin
Ligar();
end;
Isso diz que um meio de transporte deve ser ligado sempre que entrar em movimento (tudo bem que na vida real não seja exatamente assim, mas para fins didáticos, está bom). Agora declare o método Ligar na interface da classe TMeioTransporte (Listagem 2).
unit uMeioTransporte;
interface
type
TMeioTransporte = class
Descricao : string;
Capacidade : integer;
procedure Mover; virtual;
procedure Ligar; virtual; abstract;
end;
implementation
{ TMeioTransporte }
uses
Dialogs;
procedure TMeioTransporte.Mover;
begin
ShowMessage('Ligando ' + Descricao);
end;
end.
Abstract é sempre usado junto da diretiva virtual e indica que o método não possui implementação na classe em que é declarado (algo muito semelhante a uma interface, técnicas que discutiremos adiante neste curso). Ligar não é implementado nessa classe, ou seja, ele é totalmente abstrato, só possui a definição para poder ser chamado (no método Mover). Ou seja, sabemos que todos os meios de transporte são ligados antes de entrar em movimento, então podemos fazer essa chamada na classe base para não precisar fazer isso em todas as classes descendentes. Todos os TMeioTransporte são ligados antes de mover, mas são ligados de forma diferente, novamente, polimorfismo. Um carro é ligado com chave, um avião..., bom deixa pra lá.
Em uma situação real, por exemplo, poderíamos ter um cadastro base que tem um método polimórfico chamado Gravar. Sabemos que antes de Gravar dados no BD precisamos sempre Validar as informações, mas essa validação vai depender das classes concretas descendentes. Validamos um cliente de uma forma, um produto de outra, e assim por diante. Então, poderíamos ter uma classe base chamada TCadastro que possui um método virtual chamado Gravar, que sempre chama Validar, que por sua vez é abstrato. Nunca mais precisamos chamar o Validar, apenas implementar.
Usando recursos de polimorfismo vamos sobrescrever o Ligar na classe descendente. Abra a classe TCarro e sobrescreva o método Ligar e o implemente como na Listagem 3. Fazemos o mesmo para a classe TAviao (Listagem 4).
unit uCarro;
interface
uses
// coloque essa unit p/ acessar a classe TMeioTransporte
uMeioTransporte;
type
// observe que TCarro agora herda de TMeioTransporte
TCarro = class(TMeioTransporte)
// observe que retiramos os campos Capacidade e Descricao daqui
Quilometragem : integer;
procedure Mover(); override;
procedure Ligar(); override;
end;
implementation
uses Dialogs;
{ TCarro }
procedure TCarro.Ligar();
begin
// repare que não vai inherited aqui
// pois não existe nada na classe base
ShowMessage('Ligando o carro ' + Descricao);
end;
procedure TCarro.Mover();
begin
inherited; // isso vai chamar o Ligar
ShowMessage(Descricao + ' entrou em movimento.');
end;
end.
unit uAviao;
interface
uses
// coloque essa unit p/ acessar a classe TMeioTransporte
uMeioTransporte;
type
// observe que TAviao agora herda de TMeioTransporte
TAviao = class(TMeioTransporte)
// observe que retiramos os campos Capacidade e Descricao daqui
HorasVoo : integer;
procedure Mover(); override;
procedure Ligar(); override;
end;
implementation
uses Dialogs;
{ TAviao }
procedure TAviao.Ligar();
begin
// repare que não vai inherited aqui
// pois não existe nada na classe base
ShowMessage('Ligando o aviao '+Descricao);
end;
procedure TAviao.Mover();
begin
inherited; // isso vai chamar o Ligar
ShowMessage(Descricao+' está voando.');
end;
end.
E na classe base, TMeioTransporte, simplesmente chamamos o Ligar:
procedure TMeioTransporte.Mover();
begin
Ligar();
end;
Vamos executar a aplicação agora e ver como tudo isso vai funcionar. No form principal preencha os Edits e crie um carro. Veja o que acontece passo a passo:
- Ao clicar no botão Mover, será chamado o Mover da classe TMeioTransporte. Como esse método é virtual, ele identifica o método pela instância criada e não pelo tipo declarado, logo será chamado Mover de TCarro;
- Mover de TCarro faz uma chamada à inherited o que invoca o método de mesmo nome na classe base, TMeioTransporte.Mover;
- TMeioTransporte.Mover chama Ligar para ligar o meio de transporte;
- Ligar é um método virtual / abstrato, logo será chamado TCarro.Ligar;
- Após a chamada à inherited retornamos a TCarro.Mover, que então executa o restante do código.
Vamos ver um exemplo na VCL? Na classe TDataSet, achamos o seguinte método abstrato:
procedure InternalClose; virtual; abstract;
Ou seja, ele está apenas aí declarado, não vamos achar sua implementação em TDataSet, porém, ele pode ser chamado, como um comportamento comum, que mais tarde vai tomar diferentes formas. Ele é chamado no método TDataSet.CloseCursor, que é um método virtual (porém não abstrato).
Construtores e Destrutores
Um construtor é um método especial encarregado de alocar a memória para um objeto. Ao contrário, um destrutor libera a memória alocada para o objeto. Muitas vezes precisamos adicionar código personalizado a um construtor, por exemplo, para alocar memória para um objeto interno ou inicializar variáveis. Em nosso exemplo, vamos utilizar construtores para inicializar as variáveis do objeto. Declare o seguinte na classe TCarro:
constructor create();
Aperte Shift+Ctrl+C e implemente o construtor como mostrado na Listagem 5. Faça o mesmo para a classe TAviao (Listagem 6). Como você pode ver, usamos o construtor para inicializar atributos das classes. Podemos inicializar as variáveis comuns (como Descricao e Capacidade) no construtor da classe base TMeioTransporte (Listagem 7).
constructor TCarro.create();
begin
inherited; // chama o construtor da classe base
Quilometragem := 0;
end;
constructor TAviao.create();
begin
inherited; // chama o construtor da classe base
HorasVoo := 0;
end;
constructor TMeioTransporte.create();
begin
inherited;
Capacidade := 0;
Descricao := 'Sem Nome';
end;
Construtores inicializam objetos e são chamados quanto um objeto é instanciado com Create (ou new no Delphi Prism). Quando chamamos free de um objeto, ele é liberado, isso vai fazer uma chamada ao seu destrutor, oportunidade que temos para liberar objetos internos que alocamos em nossa classe. Para criar um destrutor, você deve declará-lo na interface da classe e implementá-lo como na Listagem 8.
destructor destroy; override;
...
destructor TMeioTransporte.destroy;
begin
// seu código de limpeza aqui
inherited;
end;
Observe que destroy é declarado como virtual em TObject, logo devemos dar um override ao sobrescrever. Ao contrário, o constructor é estático, e só passa a ser virtual a partir de TComponent.
Na VCL temos um exemplo clássico. Vamos examinar a classe TMemo, na verdade, TCustomMemo (Listagem 9). Note que um Memo, obviamente, armazena uma lista de strings (TStrings, que é uma classe abstrata). Logo, vemos o atributo privado FLines do tipo TStrings. Uma propriedade chamada Lines mapeia para o atributo privado interno. No construtor, FLines é inicializado, alocado em memória, e no destrutor, é destruído. Com isso, quando um Memo for destruído, ele mesmo se encarrega de liberar todos os seus objetos internos alocados, evitando memory leaks na aplicação, já que no Win32 não temos um coletor de lixo (Garbage Collector) como no .NET.
TCustomMemo = class(TCustomEdit)
private
FLines: TStrings;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
property Lines: TStrings read FLines write SetLines;
...
end;
...
constructor TCustomMemo.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
...
FLines := TMemoStrings.Create;
end;
...
destructor TCustomMemo.Destroy;
begin
FLines.Free;
inherited Destroy;
end;
Existem muitas classes TCustomXXX na VCL. Elas se encarregam de implementar toda a funcionalidade de uma classe, de um controle, porém, não publicam suas propriedades. Um exemplo clássico na VCL é o TCustomClientDataSet. Ele implementa toda a funcionalidade do TClientDataSet. Mas se observar o código-fonte de TClientDataSet, em dbclient.pas, verá que o tão popular e falado componente da VCL, sendo considerado por muitos (inclusive eu – não o DevMan, o autor) como o melhor componente da VCL, na verdade não faz nada! Ele apenas muda o especificador de visibilidade de propriedades declaradas e implementadas na classe custom base. Por exemplo, CommandText é todo implementado em TCustomClientDataSet e declarado como protected. TClientDataSet simplesmente publica a propriedade:
TClientDataSet = class(TCustomClientDataSet)
published
property CommandText;
…
Especificadores de Visibilidade
Até aqui nossas classes utilizaram um único especificador de visibilidade para os atributos e métodos (public). Um atributo / método com esse modificador pode ser acessado (visto) por outras classes. Porém, temos inúmeros outros especificadores no Object Pascal, como podemos ver na Listagem 10.
type
TMinhaClasse = class(TObject)
private
protected
public
published
end;
Vejamos brevemente o que significa cada um:
- Private – Membros definidos com esse especificador são visíveis somente na classe atual e classes “amigas” (friendly classes), ou seja, classes declaradas na mesma unit;
- Protected - Visível somente na classe atual, descendentes e por classes amigas;
- Public - Visível a partir de qualquer outra classe;
- Published - Visível a partir de qualquer outra classe, ativando suporte à RTTI, com a principal finalidade de serem vistas no Object Inpector.
Automated surgiu no Delphi 2 e era originalmente usada para suportar OLE Automation / COM. Foi descontinuado pelo uso de Type Libraries para esse propósito. Veja meu artigo da edição 19 para mais informações sobre OLE e COM.
O .NET Framework define mais alguns modificadores, como friendly (acessível para classes no mesmo pacote ou diretório) e internal (acesso limitado ao programa / assembly). Além disso, não há o conceito de published no .NET.
Existe um grande discussão em torno do Published. Vamos pensar: todos os componentes que largamos em um Form / DM são automaticamente published para poderem usar RTTI e terem suas propriedades persistidas no DFM. Porém, isso fere princípios básicos da POO. Ou seja, obrigatoriamente, esses componentes, mesmo que não queiramos, são públicos e podem ser acessados de outras classes. Se movermos um ClientDataSet usado em design (DFM) para a seção private, por exemplo, para reforçar o encapsulamento, obteremos um erro (a não ser que o criemos todo em runtime). Por conta disso, encontrei na Internet no blog do time do Delphi pedidos para a implementação do modificador strict published, que resolveria esse problema. Aproveitando, nas versões mais recentes do Delphi já podemos encontrar modificadores strict private, strict protected, que têm o intuito de proteger o acesso a membros internos de uma classe a partir de classes amigas.
Uma curiosidade: existe uma prática muito usada na VCL por conta desse “recurso” de classes amigas. Se precisarmos, por exemplo, acessar um membro privado de um ClientDataSet a partir de um Form, podemos fazer algo como:
type
THackCDs = class (TClientDataSet)
end;
TForm1 = class(TForm)
ClientDataSet1: TClientDataSet;
…
end;
...
THackCDs(ClientDataSet1).AcessoAMembroPriviado
A técnica é simples, a classe hack está na mesma unit da classe do Form, logo, viram amigas. E portanto, podemos fazer um type-cast do CDS para a classe hack e acessar seus membros internos. POG.
Voltando ao nosso exemplo, localize a declaração da classe TMeioTransporte e altere como mostrado na Listagem 11. Observe que foi colocada a letra “F” no início dos campos privados, para indicar que se tratam de Fields. Observe também que o método abstrato Ligar foi declarado na seção protected. Isso porque esse método não é chamado a partir de outra classe, é chamado internamente pela própria classe TMeioTransporte e é sobrescrito pelas classes descendentes. Por exemplo, se criarmos uma instância de TMeioTransporte e chamarmos o método Ligar, obteremos um Abstract Error.
Na prática, nunca instanciamos classes abstratas, mas sim classes concretas que herdam e implementam classes abstratas. Na VCL, por exemplo, você nunca vai instanciar um TDataSet (apesar de isso ser possível), você declara um TDataSet e dentro dele coloca um TClientDataSet, TSqlDataSet etc. Interfaces resolvem esse problema. Veremos interfaces adiante neste curso.
O motivo de movermos as variáveis para a seção private foi encapsular o seu acesso. Porém, se tentarmos compilar a aplicação, vamos ter alguns erros, pois variáveis privadas não são acessíveis fora do escopo da classe. Criaremos então propriedades para permitir o acesso às variáveis privadas.
TMeioTransporte = class
private
FCapacidade : integer;
FDescricao : string;
protected
procedure Ligar(); virtual; abstract;
public
constructor create();
destructor destroy(); override;
procedure Mover(); virtual;
end;
Propriedades
Adicione então o último especificador de visibilidade à nossa classe, declarando duas propriedades que fazem o mapeamento para os campos privados. Utilize a palavra-reservada property para definir uma propriedade, como na Listagem 12.
...
published
property Capacidade: integer read FCapacidade write FCapacidade;
property Descricao: string read FDescricao write FDescricao;
end;
Altere a definição de TCarro e TAviao, criando propriedades e definindo os especificadores de visibilidade, como fizemos em TMeioTransporte. O código final das classes pode ser visto na Listagem 13. Note que já estou usando métodos get / set, tópicos que vamos discutir a seguir.
[TMeioTransporte]
unit uMeioTransporte;
interface
type
TMeioTransporte = class
private
FCapacidade : integer;
FDescricao : string;
protected
procedure Ligar(); virtual; abstract;
public
procedure Mover(); virtual;
published
property Capacidade: integer read FCapacidade write FCapacidade;
property Descricao: string read FDescricao write FDescricao;
end;
implementation
{ TMeioTransporte }
uses
Dialogs;
procedure TMeioTransporte.Mover();
begin
Ligar();
end;
end.
[TCarro]
unit uCarro;
interface
uses
// coloque essa unit p/ acessar a classe TMeioTransporte
uMeioTransporte;
type
// observe que TCarro agora herda de TMeioTransporte
TCarro = class(TMeioTransporte)
// observe que retiramos os campos Capacidade e Descricao daqui
private
FQuilometragem : integer;
function GetQuilometragem: integer;
procedure SetQuilometragem(const Value: integer);
protected
procedure Ligar; override;
public
procedure Mover; override;
published
property Quilometragem: integer
read GetQuilometragem write SetQuilometragem;
end;
implementation
uses Dialogs;
{ TCarro }
function TCarro.GetQuilometragem(): integer;
begin
result := FQuilometragem;
end;
procedure TCarro.Ligar();
begin
// repare que não vai inherited aqui
// pois não existe nada na classe base
ShowMessage('Ligando o carro '+Descricao);
end;
procedure TCarro.Mover();
begin
inherited;
ShowMessage(Descricao + ' entrou em movimento.');
end;
procedure TCarro.SetQuilometragem(const Value: integer);
begin
if Value < 0 then
FQuilometragem := 0
else
FQuilometragem := Value;
end;
end.
[TAviao]
unit uAviao;
interface
uses
// coloque essa unit p/ acessar a classe TMeioTransporte
uMeioTransporte;
type
// observe que TAviao agora herda de TMeioTransporte
TAviao = class(TMeioTransporte)
// observe que retiramos os campos Capacidade e Descricao daqui
private
FHorasVoo: integer;
function GetHorasVoo: integer;
procedure SetHorasVoo(const Value: integer);
protected
procedure Ligar(); override;
public
procedure Mover(); override;
published
property HorasVoo: integer
read GetHorasVoo write SetHorasVoo;
end;
implementation
uses Dialogs;
{ TAviao }
function TAviao.GetHorasVoo(): integer;
begin
result := FHorasVoo;
end;
procedure TAviao.Ligar();
begin
// repare que não vai inherited aqui
// pois não existe nada na classe base
ShowMessage('Ligando o aviao '+Descricao);
end;
procedure TAviao.Mover();
begin
inherited;
ShowMessage(Descricao + ' está voando.');
end;
procedure TAviao.SetHorasVoo(const Value: integer);
begin
if Value < 0 then
FHorasVoo := 0
else
FHorasVoo := Value;
end;
end.
A Partir da versão 2009, o Delphi passou a suportar NameSpaces nos nomes das Units, com a finalidade de organizar melhor as bibliotecas e preparar a chegada de novas bibliotecas como foi o caso da chegada do FireMonkey. Assim é necessário acrescentar um sufixo à Unit Dialogs pois esta possui duas versões: a da biblioteca VCL ( Vcl.Dialogs) e a da biblioteca do Firemonkey (FMX.Dialogs). Assim o sufixo faz-se necessário para distinguir com qual biblioteca se está trabalhando se bem que em uma compilação parametrizada por linha de comando é possível omitir o sufixo forçando uma compilação independente de plataforma.
Métodos Get/Set
Uma grande vantagem de se usar propriedades ao invés de campos (Fields) é a possibilidade de usar métodos que fazem o mapeamento da atribuição e leitura das variáveis privadas. Por exemplo, na definição da classe TCarro temos o seguinte:
property Quilometragem: integer
read GetQuilometragem write SetQuilometragem;
Ao implementar uma propriedade assim, é só apertar Shift+Ctrl+C para que o Delphi crie os cabeçalhos e implementação dos métodos get / set. A implementação pode ser vista na mesma Listagem 13. Observe que nossa rotina Get apenas repassa o valor para a variável privada, reforçando assim princípios de encapsulamento. Em nossa rotina Set testamos se o valor passado não foi negativo, e se for, configuramos a variável privada como zero. Observe o seguinte código:
Carro.Quilometragem := -3;
Isso fará ser disparado o método Set, e o valor da Quilometragem passará a valer 0.
Veremos mais sobre propriedades, rotinas get/set e especificadores de visibilidade nas próximas partes desta série, quando criarmos um componente.
Nossas classes TMeioTransporte, TCarro e TAviao estão praticamente finalizadas. Estão agora usando na prática excelentes recursos da POO, como herança, polimorfismo e encapsulamento. Vimos como compartilhar dados e comportamento. Criamos propriedade, estudamos os especificadores de visibilidade, vimos que até mesmo a VCL tem alguns “defeitos” de modelagem por conta de restrições do Object Pascal do Delphi Win32. Aprendemos o que são construtores e destrutores, além de métodos get / set. Ainda tem mais! Na próxima parte deste curso vamos dar um grande salto usando algo que é marca registrada do Delphi desde a sua primeira versão: vamos transformar nossas classes em componente e colocá-las dentro de pacotes. Veremos ainda como usar herança visual de formulários e também um dos recursos mais fantásticos da OO, interfaces. E para finalizar com chave de ouro, um estudo de caso de uma aplicação real que vai mostrar na prática como usar OO, incluindo herança, interfaces e polimorfismo, reforçando tudo o que vimos até aqui. E futuramente, vamos ver que a OO também tem seus defeitos, e usando a própria OO encontramos soluções, através de Design Patterns.
Até aqui já aprendemos muito sobre classes, herança, abstração, encapsulamento, grandes pilares da orientação a objetos. E como se já não bastasse, colocamos um ingrediente especial na receita, o polimorfismo, o que considero ser a técnica mais poderosa da OO, a capacidade de comandar o presente deixando a implementação aberta para o futuro. Se adicionarmos classes abstratas e interfaces, então, a arquitetura fica ainda mais flexível.
O que nos interessa neste artigo é dar um passo além das classes. Classes são ótimas, mas se você quer aproveitar o melhor do RAD do Delphi, dos editores, usará componentes. Praticamente tudo o que fazemos hoje no Delphi é usando, ligando e configurando componentes. Antes de mais nada e para não deixar dúvidas, componentes são classes, que obrigatoriamente descendem de TComponent ou um dos seus vários descendentes.
TPersistent, classe base de TComponent, permite que objetos sejam serializados. Serializar um objeto significa persisti-lo em disco (leia-se suas propriedades) e posteriormente ler esses dados de volta para o objeto. Essa técnica é a chave para o uso de frameworks de persistência e mapeamento objeto / relacional, como Hibernate, NHibernate, ADO.NET Entity Framework, ECO etc. A persistência (mais exatamente serialização) de objetos também é usada para transmissão de dados via rede, por exemplo, em uma aplicação multicamadas. No Delphi, os objetos (ou melhor, componentes) são serializados em arquivos .DFM, graças a todo TComponent ser obrigatoriamente um TPersistent. Para um melhor entendimento da hierarquia do core da VCL, veja a Figura 1. Você não instancia nenhuma dessas classes diretamente, elas são base para classes mais específicas, como formulários, botões, componentes de acesso a dados etc.
É TPersistent que ativa a RTTI (Runtime Type Information). RTTI é o grande segredo da persistência de objetos no Delphi. Quer ver? Abra os fontes de qualquer projeto seu em Delphi, segure a tecla Control, clique sobre a classe TForm que está no cabeçalho da declaração do seu form e vá clicando nas classes e subindo até chegar em TPersistent. Chegaremos ao seguinte:
{$M+}
TPersistent = class(TObject)
O que você vê acima da classe é uma diretiva de compilação, que ativa a RTTI para todos os descendentes de TPersistent. Convenhamos, “M” não diz nada para quem lê o código, obrigatoriamente o desenvolvedor vai ter que buscar no Help ou Google para descobrir o que é. Então, as diretivas também têm uma versão extensa. Por exemplo, o seguinte seria equivalente ao exemplo acima, bem mais intuitivo:
{$TYPEINFO ON}
TPersistent = class(TObject)
Muitas das diretivas de compilação podem ser configuradas no Project>Options. Por exemplo, se você marcar a opção Complete Boolean Eval, na verdade vai estar adicionando a seguinte diretiva ao seu projeto:
{$B+}
Ou o equivalente:
{$BOOLEVAL ON}
Essa diretiva é relativa à análise de expressões booleanas, por exemplo:
if (1 = 1) or (3 <> 3) then ...
Veja que não há motivos para o compilador processar o segundo teste (3 <> 3), pois o primeiro teste (1 = 1), retorna true, e como o operador é um or, true com false vai ser sempre true (lembra das aulas de lógica dos tempos da faculdade?). Então, o padrão é {$B-}.
Essas configurações ficam gravadas no arquivo [NomeDoProjeto].cfg. Se você não quiser usar o arquivo .cfg, há uma técnica interessante que permite adicionar as diretivas atuais do projeto ao próprio fonte, nesse caso o local mais indicado é no próprio .dpr. Faça um teste, abra o .dpr e logo abaixo do program aperte Ctrl+O.
Graças à ativação da RTTI logo em TPersistent, todo TComponent pode ser serializado em .dfm. É por esse motivo que você configura as propriedades de um ClientDataSet, Button, Memo, ListBox, fecha o projeto, e quando reabre, tudo está lá configurado.
O melhor livro que já li que aborda RTTI, entre outras coisas, foi o Delphi in a Nutshell, de Ray Lischner. Ele está disponível em quase sua totalidade para leitura on-line no Google Books (veja sessão links).
Em nosso projeto, TCarro e TAviao não são persistentes. Isto é, se você criar um ASTRA com capacidade 5 e quilometragem 5 mil quilômetros, quando o objeto for destruído e a aplicação fechada, já era. Então, vamos dar vida eterna aos nossos objetos, tornando-os persistentes, indo direto ao ponto, transformando classes em componentes.
Você já parou para pensar por que existe a classe TPersistent no meio de TObject e TComponent? Por que a implementação da persistência não foi feita em TComponent, se basicamente, somente componentes precisam ser persistidos? Ou seja, existe algum descendente de TPersistent que não seja TComponent? Sim. Isso é aplicado constantemente na VCL em componentes que têm atributos de tipo classes que também precisam ser persistidos. Um exemplo clássico:
TStrings = class(TPersistent)
Você não larga um TStrings na tela, pois ela descende diretamente de TPersistent, é por esse motivo que você digita um texto em um Memo (em design), fecha o projeto, e ao reabri-lo lá estão as linhas. A propriedade Lines não é um componente, mas precisa ser persistente.
Outro exemplo é a classe TPicture. Não colocamos essa classe no form, pois não é um componente, mas quando você coloca um Image e configura a propriedade Picture para mostrar um banner da empresa no topo do form, lá fica ela persistida no .dfm.
A partir desta parte do artigo, nosso projeto vai ser migrado para Delphi 2010. Não se preocupe caso ainda esteja usando o Delphi 7 (dá para continuar o curso nele). No entanto, não abordarei ainda as novidades do RTTI que são inúmeras na versão 2010 (confira um artigo nessa edição sobre as novidades).
Criando um novo pacote e um novo componente
Componentes obrigatoriamente ficam dentro de pacotes. Um pacote é, basicamente, um conjunto de units que quase sempre definem componentes, embora você possa ter units que não contêm componentes. Um pacote permite, entre outras coisas, que uma aplicação fique “modularizada”. Pacotes podem diminuir o tamanho do seu executável final e auxiliar na distribuição de atualizações. Um pacote, depois de compilado, é um arquivo .bpl (um tipo especial de .dll), e pode ser de 3 tipos: RunTime, Design-Time e RunTime & Design-Time. A extensão do arquivo-fonte do projeto é .dpk. A título de curiosidade, o suporte a pacotes surgiu no Delphi 3.
O ponta pé inicial então é criar um pacote. Clique em File > New > Package e salve o pacote no mesmo diretório da aplicação que estamos construindo, para facilitar. Chame o pacote de “pkPessoa”. Antes de adicionarmos nossas classes ao pacote, vamos criar um novo componente nele. Clique em Compone nt > New VCL Component, no editor que se abre escolha a classe base para nosso novo componente, nesse caso, TComponent (Figura 2). Clique em Next e preencha os dados da tela como mostra a Figura 3. Repare: nome do componente será TPessoa, a paleta que ficará é ClubeDelphi, o nome da unit é uPessoa.pas e deve ficar no mesmo diretório da aplicação. Clique Next, na próxima tela escolha Install to Existing Package, localize nosso pacote recém criado, salve a unit e pronto. O editor do pacote deve agora estar como mostra a Figura 4.
Se você quiser compilar suas classes (.DCU) em um outro diretório, o recomendado, crie uma Lib e indique esse caminho no Tools > Options > Library Path.
Vamos alterar a classe TPessoa, abrindo a unit uPessoa.pas e adicionando a ela a seguinte propriedade na sessão published:
property Nome: string;
Aperte Shift+Ctrl+C para que o Delphi 2010 complemente a propriedade gerando o Field / getter e setter (estudamos isso no artigo da edição passada). Salve tudo, dê um Build no pacote, no editor dê um clique de direita no nome projeto e escolha Install. Pronto, a classe, ou seja, componente, está agora disponível na paleta de componentes em ClubeDelphi.
Vamos fazer um teste simples, inicie uma nova aplicação VCL e coloque no form nosso componente recém criado. Configure um nome para a pessoa, aperte Alt+F12 e veja que isso foi persistido no .dfm. Salve tudo, reabra o projeto, e veja que nosso objeto é reconstruído com todas as suas propriedades, ou seja, persistência na prática (Figura 5). Repare que, como nossas propriedades foram declaradas com o modificador published, elas aparecem no Object Inspector.
Transformando Classes em Componentes
O que faremos agora é transformar em componentes nossas três classes criadas nas partes anteriores deste curso (TMeioTransporte, TCarro e TAviao). Primeiro, reabra o projeto do pacote e com o item Contains selecionado, clique no botão Add. Adicione as units uMeioTransporte, uCarro e uAviao. Atenção, não coloque o form.
Abra a unit uMeioTransporte e defina TComponent como classe ancestral de TMeioTransporte. Adicione Classes à cláusula uses. Como agora nossa classe não descende mais de TObject, e sim TComponent, o construtor precisa ser modificado. Primeiro, ele agora deve receber um proprietário (Owner). Segundo, em TComponent ele é virtual, logo, precisamos usar override (lembra do polimorfismo? Olha ele de novo aqui, auxiliando os construtores). O código completo da classe pode ser visto na Listagem 1. Nas classes TAviao e TCarro faça a mesma coisa, declare classes no uses e modifique o construtor. Lembre-se de que o construtor deve ser modificado na interface e implementation.
unit uMeioTransporte;
interface
uses Classes;
type
TMeioTransporte = class(TComponent)
private
FCapacidade : integer;
FDescricao : string;
protected
procedure Ligar(); virtual; abstract;
public
procedure Mover(); virtual;
published
property Capacidade: integer read FCapacidade write FCapacidade;
property Descricao: string read FDescricao write FDescricao;
end;
implementation
{ TMeioTransporte }
uses
Dialogs;
procedure TMeioTransporte.Mover();
begin
Ligar();
end;
end.
Por tabela, ou melhor dizendo na linguagem OO, por herança, agora TCarro e TAviao são componentes também. E já temos um pequeno framework. Agora precisamos indicar ao Delphi 2010 para registrar os componentes TCarro e TAviao no IDE. Vá até a unit uCarro e declare o seguinte procedimento imediatamente antes da cláusula implementation:
procedure Register;
A linguagem Delphi, como sabemos, não diferencia maiúsculas e minúsculas (case-insensitive). A única exceção é o procedimento Register, que deve ter “R” maiúsculo. Implemente esse procedimento na seção implementation da unit, logo após a cláusula uses:
procedure Register;
begin
RegisterComponents('ClubeDelphi',[TCarro]);
end;
RegisterComponents registra o(s) componente(s) no IDE na paleta indicada no primeiro parâmetro. O segundo parâmetro é um array de TComponentClass (referência de classe). Faça o mesmo para unit uAviao. Recompile o pacote e instale os novos componentes. Veja os componentes instalados na Figura 6.
Veja que não registramos TMeioTransporte, pois a classe não é usada diretamente, é uma classe abstrata que serve de base para as classes mais específicas. Pelo mesmo motivo, é que você vai encontrar na paleta um ClientDataSet, SqlDataSet, Table, mas nunca um TDataSet.
Abra o Image Editor do Delphi. Clique em File > New > Component Resource File (.dcr). Salve o arquivo no mesmo diretório do pacote, como nome pkIcones.dcr. Clique de direita em Contents e escolha New > Bitmap. Defina as dimensões 24x24 e 256 cores. Renomeie Bitmap1 para o mesmo nome da classe, tudo em maiúsculo, neste caso “TPESSOA”. Dê um duplo clique nesse item e desenhe ou cole o ícone no editor que aparece. Repita o mesmo procedimento para as demais classes. Salve o arquivo. Volte ao pacote no Delphi 2010 e clique em Project > View Source. Logo após a declaração do pacote inclua o arquivo .dcr como abaixo e recompile.
package pkPacote;
{$R pkIcones.dcr}
Polimorfismo e abstração em componentes
Agora imagine que um objeto Pessoa precise fazer uso de um Carro e de um Aviao. Poderíamos simplesmente declarar duas propriedades na classe TPessoa (atenção, não altere seu código, apenas observe):
property Carro : TCarro read FCarro write FCarro;
property Aviao : TAviao read FAviao write FAviao;
Pronto, uma pessoa poderá agora utilizar carros e aviões. Mas temos um problema. Se um dia for criado um novo meio de transporte, como TBike, nosso componente TPessoa precisaria de uma nova propriedade. E se novos meios de transporte forem surgindo, serão necessárias novas alterações em TPessoa. E pior, nós nunca vamos saber que tipos de meio de transporte poderão existir no futuro. Quem sabe um TTeleTransporte? Na OO, temos que programar pensando em baixo acoplamento, ou seja, depender o mínimo possível de outras classes, cuidando para não sofrer com suas mudanças ou novas implementações. Aí entra em cena outra técnica, abstração. E conseguimos programar para abstração usando classes bases abstratas, mais genéricas, ou interfaces. Devemos sempre programar pensando nessas classes bases, no padrão, na interface.
Já imaginou a tecla Enter do seu notebook estragar e você ter que trocar a tela, CPU, mouse, pen-drive, WebCam, HD, driver DVD, adaptador de rede, wireless etc. E se o pneu do seu carro furar e ficar sem concerto, e por tabela você ter que trocar todo o sistema elétrico? (notou que um não pode depender do outro?) Não é assim que funciona na vida real, não é assim que funciona na orientação a objetos, não é assim que funciona no Delphi.
Lembra daquela analogia que fiz nas partes anteriores do artigo? Você não tem um furo na lateral do micro para cada tipo de dispositivo, você usa um padrão, USB, e nele pluga qualquer dispositivo. E no Delphi? Mesma coisa. Um QuickReport aponta para um TDataSet, um tipo mais genérico, o que torna a arquitetura plugável, extensível. Ou seja, hoje eu conecto o QR a qualquer tipo descendente de TDataSet. O mesmo acontece com o TDataSource, ele esconde (abstrai) dos DataControls qual o tipo específico de TDataSet será usado, ou seja, ele joga um padrão.
Em nosso exemplo, vamos fazer exatamente a mesma coisa que a VCL faz e que a OO manda. Vamos criar uma única propriedade que possa apontar para qualquer meio de transporte. Então declare a seguinte propriedade na sessão published de TPessoa:
property Transporte : TMeioTransporte read FTransporte write FTransporte;
Aperte Shift+Ctrl+C para gerar a declaração do campo privado. Declare uMeioTransporte na cláusula uses. Recompile o pacote. Feito, essa é a nossa “entrada USB”.
Criando uma aplicação para testar os componentes
Inicie uma nova aplicação VCL e salve os arquivos do projeto. Coloque no formulário principal um Button com o Caption “Mover”. Coloque um Carro e uma Pessoa. Defina as propriedades do objeto Carro. Aponte a propriedade Transporte da Pessoa para o objeto Carro (veja Figura 7). O código .dfm do form é mostrado na Listagem 2.
object Form1: TForm1
Caption = 'Extreme OO @ ClubeDelphi'
...
object Button1: TButton
Caption = 'Mover'
OnClick = Button1Click
...
end
object Astra: TCarro
Capacidade = 5
Descricao = 'ASTRA'
Quilometragem = 5000
...
end
object Guinther: TPessoa
Nome = 'Guinther Pauli'
Transporte = Astra ß repare o componente apontando para o outro
...
end
end
No evento OnClick do botão Mover escreva:
Guinther.Transporte.Mover();
Execute a aplicação e clique no botão, veja o resultado na Figura 8.
Agora coloque um componente Avião no formulário, e configure suas propriedades. Aponte a propriedade Transporte da Pessoa para o objeto Aviao. Clique no botão e veja que a pessoa pode também colocar um avião em movimento, graças ao polimorfismo (Figura 9).
O que isso tudo significa? Que conclusões podemos tirar?
- Nossos objetos agora são persistentes. Você pode fechar o projeto, reabrir, que as propriedades estarão lá, configuradas, graças à RTTI e persistência;
- A Pessoa possui um atributo chamado Transporte, que não é de um tipo específico, mas de um tipo genérico, TMeioTransporte. Isso abstrai da pessoa detalhes específicos do tipo do meio de transporte que ela vai operar;
- A abstração torna nossa arquitetura plugável e extensível. Se criarmos um TBike, ótimo, basta a pessoa apontar para o novo objeto que já terá condições de colocá-lo em movimento, graças ao polimorfismo. Estamos programando no presente prontos para o futuro;
- E finalmente, é exatamente assim que a VCL trabalha. Compare as duas linhas abaixo, a do nosso exemplo, e de outro exemplo que seria usando componentes de acesso a dados do Delphi, alguma semelhança?
Guinther.Transporte.Mover();
DataSource1.DataSet.Post();
Mover é polimórfico, então não importa para que meio de transporte a pessoa esteja apontando, o método correto será invocado. No DataSource, o TDataSet pode apontar para um TClientDataSet, TTable, não importa, o método polimórfico será chamado de acordo com a classe instanciada (e não a declarada, o que caracterizaria um método estático).
Mais uma coisinha que você precisa saber. Quer ver o IDE dar pau, travar tudo e obter inúmeros AV’s (Access Violation’s)? Apague o componente Aviao, ou aquele que esteja sendo apontando pela propriedade Transporte da Pessoa. O que vai acontecer? A Pessoa não vai ficar sabendo que o objeto que ela estava apontando foi excluído, logo, vai apontar para algo que não existe mais. Para isso, precisamos fazer que a propriedade Transporte seja zerada (nil) no momento em que o componente de transporte associado for excluído. Fazemos isso através do Notification. De volta ao pacote, declare o método na seção protected da classe TPessoa e implemente-o como na Listagem 3. Não se esqueça de dar um Build no pacote.
procedure Notification(AComponent: TComponent;
Operation: TOperation); override;
…
procedure TPessoa.Notification(AComponent: TComponent;
Operation: TOperation);
begin
inherited;
if (Operation=opRemove) and
(AComponent=Transporte) then
Transporte := nil;
end;
Eventos
Muito bem, nossos componentes contêm propriedades, inclusive as herdadas de TComponent (Name e Tag). Porém, se você clicar em Events no Object Inspector, verá que nada existe. Muitos consideram o uso de eventos algo que vai de encontro à orientação a objetos. Mas em um ambiente RAD como o Delphi, eventos jogam a seu favor, e muito. Vamos ver como poderíamos implementar um evento em nossa hierarquia. Vamos criar um evento chamado BeforeLigar. Então, o desenvolvedor que for usar nosso componente, poderá usar esse evento que será disparado antes do transporte entrar em movimento. É mesma coisa do TDataSet. Você ou algum controle de tela chama Post, e o TDataSet dispara o BeforePost para você programar por exemplo alguma validação.
Declare o seguinte na seção published da classe TMeioTransporte:
property BeforeLigar : TNotifyEvent;
Aperte Shift+Ctrl+C e altere o método Mover da classe como mostra a Listagem 4.
procedure TMeioTransporte.Mover();
begin
if Assigned(FBeforeLigar) then
BeforeLigar(self);
Ligar();
end;
Recompile o pacote. Volte ao exemplo anterior. Selecione o componente Carro e veja o novo evento no Object Inspector (Figura 10). Adicione o código da Listagem 5 ao manipulador do evento BeforeLigar.
procedure TFrmExemplo.Carro1BeforeLigar(Sender: TObject);
begin
raise EAbort.create('Isso cancelará a chamada aos métodos Ligar e Mover');
end;
Selecione o componente Aviao e aponte seu evento BeforeLigar para o mesmo método. Dessa forma, ambos os componentes usam o mesmo manipulador. O parâmetro Sender dirá qual dos componentes está chamando o evento.
Observe a exceção silenciosa que foi levantada caso o usuário deseje cancelar a chamada. O raise fará com que a sequência de chamadas (Call Stack) seja quebrada e todos os métodos cancelados. Sim, eventos também servem para isso!
Se você observar bem, o uso do evento fará com que o usuário do nosso framework, (e não o desenvolvedor dele), tenha a possibilidade de "injetar” código exatamente em um ponto específico da implementação da classe, usando tipos definidos de eventos. No nosso caso, o código é chamado antes do Ligar, para dar a oportunidade do usuário realizar algum procedimento antes do meio de transporte ser colocado efetivamente em movimento.
Conclusão
Simples, rápido, produtivo, elegante e extremamente poderoso. OO + Componentes + RAD, mistura ideal para criar projetos complexos, tornar o código reaproveitável, modularizado, ao mesmo tempo que fazemos uso de boas práticas. Desde o começo deste curso, aprendemos muitas coisas. Tudo bem que foram com carrinhos e aviões, mas veja do ponto de vista do design: é exatamente assim que a VCL foi modelada, é exatamente assim que a vida real funciona, é assim que você deve desenvolver. Abraço e até a edição 112.