O objetivo deste artigo é apresentar, de uma forma simples e prática, os principais conceitos da programação orientada a objetos com o Delphi. É um artigo voltado ao público iniciante, tanto que utilizarei uma abordagem bastante didática e fácil, ao invés de aplicar os conceitos em um exemplo complexo. Aprendendo o básico sobre os principais recursos da POO, entendendo como o Delphi permite a orientação a objetos em uma linguagem elegante, permitirá a você aplicar futuramente os seus conhecimentos em projetos reais.
Você aprenderá como criar classes, instanciar objetos, usar a herança, abstração e polimorfismo. Definirá propriedades, entenderá os diferentes especificadores de visibilidade (private, public, protected e published), criará métodos de classe, get/set, estáticos e abstratos, construtores e destrutores. Conhecerá importantes palavras-chave da Delphi Language usadas na POO. Aprenderá a dar um passo além da POO, ao transformar classes em componentes, a utilizar pacotes para organizar e agrupar units, verificar dependências e muito mais.
Princípios básicos da Orientação a Objetos
A Orientação a Objetos está fundamentada em quatro pilares: abstração, encapsulamento, herança e polimorfismo. Mesmo para aqueles que desenvolvem há anos com o Delphi, esses conceitos podem passar até despercebidos. Ao contrário de linguagens totalmente orientadas a objeto, como Java e C#, o Delphi não obriga que você desenvolva um sistema usando técnicas de OO.
O apelo RAD da ferramenta faz com que os desenvolvedores construam seu software parcialmente ou totalmente orientado a eventos (EDP - Event-Driven Programming). É muito simples colocar um ClientDataSet no DataModule, criar um manipulador para o evento OnNewRecord e inserir código para inicializar dados. Da mesma forma, é simples inicializar controles de um formulário no seu evento OnShow. É possível que você tenha criado um sistema completo sem sequer ter criado uma única classe! Veja que o Delphi cria uma classe para o formulário, uma para o DataModule etc. Mas e você, cria uma classe para processar regras de negócio?
É impossível falar em orientação a objetos sem recorrer ao mundo real, e sem recorrer à teoria (não se preocupe com a parte teórica, vamos ver tudo isso na prática mais adiante), o que consequentemente também nos obrigará a fazer muitas analogias. De fato, muito da OO é uma cópia fiel do que temos ao nosso redor. Um carro é um objeto, composto de outros objetos, que podem ser trocados. Um carro possui atributos, como sua cor, tamanho, modelo, número de portas, ano de fabricação, combustível, autonomia etc. Então, nada melhor que usar a vida real para entender a OO, técnica que usarei exaustivamente durante todo esse artigo.
Antes de começar, vamos rever aquilo que aprendemos no primeiro dia de aula de programação, a diferença entre classe e objeto. A analogia perfeita, uma classe é uma “receita de bolo”. Objetos são os “bolinhos” que você faz a partir desta receita. TForm é uma classe. Form1, ou FormCliente, é um objeto, uma instância real da classe. Podemos ter várias instâncias de uma mesma classe. No Delphi, por exemplo, temos em uma aplicação com vários ClientDataSets, mas todos construídos a partir da classe TClientDataSet.
Vamos agora examinar a definição dos quatro pilares básicos de qualquer linguagem orientada a objetos:
- Abstração: capacidade de representar conceitos do domínio do problema, ressaltando apenas o que for relevante para a aplicação em questão. A definição da classe em Delphi atende a esse requisito, pois ela é um modelo limitado de alguma entidade real, que implementa apenas as características que são necessárias para a aplicação, sendo desenvolvida em um dado contexto.
- Encapsulamento: a linguagem deve permitir a definição de módulos auto-suficientes, possibilitando a implementação do conceito de ocultação de informação (information hiding), ou seja, esconder os detalhes internos. O uso dos especificadores de visibilidade (private e protected, por exemplo) e o comportamento dos objetos em Delphi reforçam esse princípio, principalmente se o desenvolvedor usar propriedades nas classes, como forma de acesso aos campos internos.
- Herança: mecanismo que permite a definição de uma hierarquia de classes, com o objetivo de reutilizar características já definidas e estendê-las em níveis maiores de detalhes ou especialização. O Delphi implementa um mecanismo eficiente de herança simples (apenas uma classe ancestral – não há suporte para herança múltipla de classes).
- Polimorfismo: classes descendentes podem implementar de forma diferente um conjunto de operações comuns definidas na classe base (no Delphi, usamos as diretivas virtual e override para aplicar o polimorfismo).
Existem também algumas “regras” quando desenvolvemos de forma orientada a objetos. São boas práticas que devem ser seguidas, para construir um software melhor, com boa qualidade, de fácil manutenção, com código que possa ser facilmente reutilizado em outros sistemas.
Quando falamos em encapsulamento, dizemos que uma classe deve ser o mais independente possível de outras classes. Imagine, por exemplo, que se toda vez que sua TV apresentasse um defeito, você tivesse que levar o aparelho de DVD junto para o conserto? Na POO, isso seria um problema grave de modelagem. Classes devem depender o mínimo de outras classes, para permitir uma fácil reutilização. Por esse motivo, elas também devem desempenhar uma única função.
Outra analogia. Em qualquer framework de acesso a dados bem modelado (como o ADO.NET), temos uma classe responsável pela execução de comandos SQL no banco de dados (nesse caso, o SqlCommand). E só! A conexão com o banco de dados, que é outra função, é desempenhada por outra classe (nesse caso, o SQLConnection). Para transações, outra classe. Para armazenar os dados lidos, outra classe. E assim por diante. Cada classe tem sua função, única, reaproveitável.
Mas classes não trabalham sozinhas, elas precisam ser ligadas, geralmente isso acontece por associação. Um objeto que executa comandos SQL no banco é inútil sem um objeto de conexão. Essa necessidade nos leva a outra regra: classes devem ser ligadas por um único ponto. No exemplo do ADO.NET, o objeto SqlCommand possui uma propriedade, um único ponto de ligação, que aponta para um objeto de conexão SqlConnection. E geralmente, esse ponto de ligação usa um tipo mais genérico, uma classe base, uma interface, que permita que outros tipos de objeto com a mesma função, mas diferente implementação, possam ser conectados.
O que isso significa? Novamente, vamos ao mundo real. Um computador é fabricado hoje. Ok. Porém, como criar entradas de conexão para ligar diferentes tipos de periféricos, alguns inclusive que ainda nem foram inventados? É definido um padrão, um protocolo, uma regra. O computador tem uma ou mais entradas USB, e qualquer tipo de objeto que venha a ser criado, que queira se conectar a um micro, seguirá esse padrão. Assim surgem impressoras USB, Pen-Drives, Mouses etc. O ponto de ligação é único. Se estragou, você retira o objeto e coloca outro. Assim é a vida real, assim é a POO. Imagine se existisse um tipo de conexão para cada tipo de periférico? Provavelmente seu Notebook teria mais furos nas laterais que teclas no teclado. E quando inventassem um novo dispositivo, teria que levar na fábrica para instalarem uma nova porta de conexão. Não é assim que funciona na vida real e não é assim na POO.
Vagões de trem, outro exemplo clássico. Eles são conectados por um único ponto. Podem ser desconectados facilmente, outros modelos de vagões podem ser conectados, desde que o ponto de conexão siga um padrão. Se você já desenvolveu relatórios no QuickReport, reparou que ele não aponta para um tipo específico de objeto de dados. Ele aponta para um tipo genérico, no caso, TDataSet. Por quê? É o mesmo princípio da USB. Dessa forma, um relatório pode trabalhar com diferentes mecanismos de acesso a banco de dados, mesmo aqueles que ainda possam ser inventados. A única regra é que, para que um objeto de manipulação de dados possa servir de fonte de dados para um relatório, ele deve seguir a regra, mais especificamente, ser um descendente de TDataSet e implementar sua funcionalidade.
Quando falamos em herança, estamos falando obrigatoriamente em reutilização. Criamos uma classe básica, que possui as características comuns a todos os seus descendentes. Depois, vamos especializando essa classe, criando descendentes, adicionando características específicas. Por exemplo, na vida real, temos os meios de transporte. Isso poderia ser uma classe, se modelado no mundo OO, seria TMeioTransporte. Nessa classe, colocaríamos atributos e funcionalidades a todos os meios de transporte. Poderíamos definir a capacidade de se locomover, andar, movimentar. Na OO, isso daria origem a um método, Movimentar. Isso é comum a todos os meios de transporte. Depois, poderíamos criar classes mais específicas, como TCarro, TBarco, TBicicleta, TMoto, TTrem, TCarroca, TAviao.
Mas veja que cada meio de transporte se movimenta de uma forma diferente. Um carro se movimenta de forma bem diferente de um avião, claro. Fazer a mesma coisa, mas de formas diferentes, traz alguma coisa a sua mente? Se pensou polimorfismo, acertou em cheio.
Nota:
No Delphi, iniciar o nome de classes com um “T” é um padrão. T vem de type, ou tipo. O mesmo acontece para interfaces, todas começam com “I”, por exemplo, IUnknown, IInterface etc. Aliás, se quiser ver como interfaces podem ser usadas ao extremo na POO, dê uma olhada no código fonte do WebSnap, no diretório de mesmo nome dentro dos fontes da VCL do Delphi. No .NET, as classes não seguem esse padrão, de ter o “T” para definir um tipo classe (ex.: DataSet, SqlConnection, StringBuilder, XmlReader), o que não vale para interfaces, que levam “I”.
Muito bem, estamos começando a entender a POO. Mas ela é muito bonita na teoria, mas na prática, funciona? Por que implementar uma classe chamada DAL (Data Access Layer), criar nela métodos para manipular informações no banco de dados, instanciar objetos de acesso a dados, definir classes para mapear tabelas do banco, se eu posso simplesmente largar um ClientDataSet e... Deixa pra lá. O que proponho é mostrar a POO na prática, mas de uma forma extremamente SIMPLES. Eu não vou propor aqui a criação de um grande sistema financeiro, ou hospitalar, ou acadêmico. Ao invés disso, vamos usar objetos simples, como carros e pessoas. Você vai ver como é divertido usar a POO para resolver problemas cotidianos, e melhor do que isso, ao aprender a verdadeira POO no exemplo que faremos, saberá como resolver os seus próprios problemas e dos seus clientes em seus sistemas reais. Vamos lá!
Exemplo prático (e simples)
No Delphi, inicie uma nova aplicação VCL Win32. Dê o nome de FrmExemplo ao formulário e salve-o nomenado a unit como uFrmMain.pas. Dê o nome de ClassesObjetos ao projeto. Crie o formulário mostrado na Figura 1 (o texto dentro dos Edits indica o nome que você deve dar a eles). Esse formulário servirá para entrarmos com os dados que serão usados para popular objetos que criaremos a seguir. Observe que ele possui duas sessões, delimitadas por dois GroupBoxes. Os Edits serão usados para o usuário informar os valores para os atributos dos objetos. Os botões vão criar e liberar os objetos da memória.
Vamos agora criar algumas classes, mas especificamente, uma classe para representar um carro e uma para representar um avião. Clique em File>New>Unit. Salve a nova unit com o nome de uCarro.pas. Usando o mesmo procedimento crie uma unit chamada uAviao.pas. Veja na Listagem 1 o código das units. Observe a forma como declaramos uma classe no Delphi. Veja também que cada classe define atributos. Por exemplo, um carro tem uma capacidade.
unit uCarro;
interface
type
TCarro = class
Descricao : string;
Capacidade : integer;
Quilometragem : integer;
end;
implementation
end.
<p align="left">------------------------------------------------
unit uAviao;
interface
type
TAviao = class
Descricao : string;
Capacidade : integer;
HorasVoo : integer;
end;
implementation
end.
Boa Prática
Seguindo um padrão da maioria das linguagens orientadas a objeto, procure sempre declarar uma classe por unit. Mas veja, no entanto, que a própria VCL do Delphi não segue esse padrão.
Volte ao formulário principal e adicione as units uCarro e uAviao à cláusula uses da interface, para podermos ter acesso às classes definidas. Agora declare duas variáveis na seção public do formulário:
public
Carro : TCarro;
Aviao : TAviao;
public é um especificador de visibilidade, conceito que discutiremos mais adiante. Por enquanto, basta entender que o que está declarado dentro da sessão public pode ser acessado sem restrição em qualquer parte do código.
Essas variáveis representarão instâncias das classes que acabamos de criar, e serão manipuladas pelos controles de tela do formulário principal. No botão Criar (do objeto Carro) digite o código da Listagem 2. Como o próprio nome sugere, o botão criará uma instância do tipo TCarro, atribuindo isso ao objeto chamado Carro. Isso é feito chamando o construtor da classe, um método especial chamado Create. Ainda no código, após criar o objeto, inicializamos seus atributos com base nos valores digitados na tela.
procedure TFrmExemplo.BtnCriarCarroClick(Sender: TObject);
begin
// cria o objeto e inicializa campos conforme valores dos edits
Carro:=TCarro.Create;
if EdtDescCarro.Text<>'' then
Carro.Descricao:=EdtDescCarro.Text;
if EdtCapCarro.Text<>'' then
Carro.Capacidade:=StrToIntDef(EdtCapCarro.Text,0);
if EdtQuilometragem.Text<>'' then
Carro.Quilometragem:=StrToIntDef(EdtQuilometragem.Text,0);
end;
Agora vamos fazer o mesmo para objetos do tipo TAviao. No botão Criar (do objeto Avião) digite o código da Listagem 3. O que fazemos aqui é a mesma coisa que fizemos para o carro, instanciamos uma classe e inicializamos seus atributos conforme os dados que o usuário digitar na tela.
procedure TFrmExemplo.BtnCriarAviaoClick(Sender: TObject);
begin
// cria o objeto e inicia campos conforme valores dos edits
Aviao:=TAviao.Create;
if EdtDescAviao.Text<>'' then
Aviao.Descricao:=EdtDescAviao.Text;
if EdtCapAviao.Text<>'' then
Aviao.Capacidade:=StrToIntDef(EdtCapAviao.Text,0);
if EdtHorasVoo.Text<>'' then
Aviao.HorasVoo:=StrToIntDef(EdtHorasVoo.Text,0);
end;
Quando chamamos o Create, o objeto passa a ocupar um espaço na memória. Isso porque um objeto é uma instância real de uma classe. Após ser utilizado, ele precisa ser destruído. Já cuidamos da criação dos objetos, usando Create. Agora vamos cuidar da destruição. Isso pode ser feito chamando o método Free do objeto. Então no botão Liberar (do objeto Carro) digite o código da Listagem 4. Da mesma forma, para o avião, no botão Liberar digite o código da Listagem 5.
procedure TFrmExemplo.BtnLiberarCarroClick(Sender: TObject);
begin
Carro.Free; // ou FreeAndNil(Carro)
end;
procedure TFrmExemplo.BtnLiberarAviaoClick(Sender: TObject);
begin
Aviao.Free; // ou FreeAndNil(Aviao)
end;
Ponteiro é um importante conceito de qualquer linguagem de programação, seja ela orientada a objeto ou não. Uma variável, um objeto, é uma estrutura que ocupa espaço na memória. Quando declaramos uma variável do tipo integer, por exemplo, um espaço é alocado na memória imediatamente, dependendo do escopo desta variável (se é de um procedimento, de uma unit, de uma classe). Porém, estruturas mais complexas, como classes, que possuem vários atributos e ocupam mais espaço na memória, não são alocadas automaticamente na memória. Precisamos criar instâncias dessas classes, criar os objetos, em tempo de execução. Ou seja, a alocação de memória é dinâmica.
Esse conceito surgiu há muito tempo. A ideia é simples. Declaramos uma estrutura, que guardará informações sobre um cliente, mas não alocamos o espaço em memória para alimentar essa estrutura até que seja realmente necessário. Ao invés disso, criamos uma variável que aponta (daí o nome ponteiro) para outra variável que representa essa estrutura. No Pascal antigo, usávamos o símbolo ^ para representar um ponteiro. Então, um ponteiro é basicamente um número, um endereço de memória, que você pode usar para referenciar uma outra variável, que será realmente criada em outro momento. O verbo referenciar usado na frase anterior não foi por acaso. Variáveis que apontam para outras variáveis são conhecidas na programação como variáveis de referência.
No Delphi, ainda existe o conceito de ponteiros, mas não precisamos trabalhar diretamente com eles. Por exemplo, Form1 é um “ponteiro”. A estrutura da classe TForm1 só vai ser usada quando o método Create for chamado. Antigamente, métodos como GetMem, AllocMem etc. eram usados para alocar memória reservada por ponteiros. Quando um objeto, que na verdade é um ponteiro, não está apontando para nada, ele possui o valor NIL (nulo, ou null, como usado em outras linguagens).
FreeAndNil, recurso criado no Delphi 5 e citado no comentário do código das Listagens 4 e 5, pode ser usado para automaticamente liberar um objeto e fazer com que o ponteiro seja anulado, não apontado para uma posição de memória onde não existe mais o objeto real.
Pronto! Já podemos testar as funcionalidades básicas implementadas até aqui. Rode a aplicação com F9 e faça os testes. Informe os valores para os atributos do Carro e clique em Criar. Depois libere-o. Estamos dando nossos primeiros passos na POO.
Criando métodos
Como você pode ter visto, até agora criamos apenas as classes com atributos, ou seja, definimos as características de um carro, de um avião. Não tratamos ainda de outro importante papel de qualquer classe, a funcionalidade, seu comportamento. Enquanto atributos e propriedades definem as características de uma classe, métodos servem para definir o que uma classe pode fazer. Por exemplo, um carro pode entrar em movimento. Vamos então criar um método chamado Mover. Abra a declaração de TCarro e declare o método Mover, como mostrado na Listagem 6.
TCarro = class
Descricao : string;
Capacidade : integer;
Quilometragem : integer;
procedure Mover;
end;
Aperte Shift+Ctrl+C para que o Delphi gere o cabeçalho da implementação para o método que acabamos de definir. Implemente o método como mostrado na Listagem 7.
procedure TCarro.Mover;
begin
ShowMessage(Descricao+' entrou em movimento.');
end;
Declare Dialogs na cláusula uses da unit uCarro, para que possamos usar o ShowMessage.
Agora vamos permitir que o usuário, após criar um objeto Carro usando o formulário, possa colocá-lo em movimento, chamando seu método Mover. No formulário principal, nas opções do objeto carro, coloque então um botão com o Caption Mover. No seu evento OnClick digite o código da Listagem 8.
procedure TFrmExemplo.BtnMoverCarroClick(Sender: TObject);
begin
Carro.Mover;
end;
Agora repita os mesmos passos para classe TAviao. Na Listagem 9 veja a implementação do método Mover para TAviao.
procedure TAviao.Mover;
begin
ShowMessage(Descricao+' está voando.');
end;
Execute a aplicação. Preencha os Edits com dados para criar os objetos, crie-os e a seguir coloque-os em movimento clicando nos respectivos botões. A Figura 2 mostra o resultado obtido até aqui.
Muito bem, isso finaliza a primeira parte desta série. Ainda é pouco sobre tudo o que ainda vamos ver sobre OO com Delphi. Mas o que foi visto aqui é fundamental: aprendemos a declarar classes, com atributos e métodos. Vimos como instanciar essas classes, que a partir desse processo, geram objetos. Nos objetos, preenchemos seus atributos com valores para definir suas características e chamamos métodos para testar sua funcionalidade.
Nos próximos artigos, vamos aprofundar, e muito, os conceitos da POO. Você já pode notar, por exemplo, que ambas as classes que criamos – TCarro e TAviao – possuem características em comum, como a descrição e capacidade. Além disso, ambas as classes tem comportamentos semelhantes, como a capacidade de se movimentar. Isso nos levará a dois importantes conceitos da orientação a objeto, a herança e polimorfismo, assuntos que discutiremos na próxima edição.
Parte II
Veja abaixo a segunda parte do artigo - Agora as partes I e II foram compiladas em um único artigo. Bons estudos :)
Introdução à POO – Parte 2
No artigo da edição anterior, aprendemos importantes conceitos da orientação a objetos, como Abstração, Encapsulamento, Herança e Polimorfismo. Vimos tudo isso na teoria, por enquanto, pois conforme eu mesmo comentei, a OO é bastante conceitual. E para podermos aplicá-la na prática, precisamos ter esses pilares bem fundamentados.
Vamos continuar nos aprofundando na orientação a objetos com o Delphi, desta vez, entendo o que considero os principais recursos da OO que são aplicados no Delphi: herança e polimorfismo. A herança, na Delphi Language, é conseguida através do uso da palavra-chave class, antigamente object (que por questões óbvias mudou, visto que uma classe não é um objeto). O polimorfismo é conseguido aplicando-se várias palavras-reservadas, como virtual, dynamic, abstract e override. Veremos tudo na prática em nosso exemplo.
Iniciamos nossos trabalhos criando uma interface que permitia manipular carros e aviões, através de Edits. O usuário informava valores para as propriedades dos objetos, que podiam ser do tipo TCarro e TAviao, e então criava instâncias dessas classes. Aqui já temos uma boa prática sendo aplicada, nossas classes de negócio estão separadas do código de interface (o formulário). Vamos então conhecer o que é a herança e como ela funciona.
Herança
Nosso exemplo da edição anterior terminou com um grave defeito de modelagem. Claro, proposital. Vamos observar o código das classes TCarro e TAviao (Listagem 1).
unit uCarro;
interface
type
TCarro = class
Descricao : string;
Capacidade : integer;
Quilometragem : integer;
procedure Mover();
end;
implementation
uses Dialogs;
{ TCarro }
procedure TCarro.Mover();
begin
ShowMessage(Descricao+' entrou em movimento.');
end;
end.
<p align="left">------------------------------------------------
unit uAviao;
interface
type
TAviao = class
Descricao : string;
Capacidade : integer;
HorasVoo : integer;
procedure Mover();
end;
implementation
uses Dialogs;
{ TAviao }
procedure TAviao.Mover();
begin
ShowMessage(Descricao+' está voando.');
end;
end.
Como você deve ter notado, muitas características estão presentes tanto em TCarro com em TAviao, como os campos Descricao, Capacidade e o método Mover. O que faremos agora é generalizar as classes TCarro e TAviao, criando uma classe base chamada TMeioTransporte, que conterá as características comuns a todos os meios de transporte. Dessa forma, TAviao e TCarro herdarão dessa classe e adicionarão funcionalidades específicas.
Esse é um dos princípios mais fundamentais para permitir a reutilização de código. Antigamente, na programação estruturada, conseguíamos reutilizar código criando units com rotinas (procedures e functions) que fossem bastante parametrizadas, sem depender uma da outra, que pudessem funcionar bem separadamente. Na orientação a objetos, a herança é muito, mas muito mais superior do que isso, como vamos comprovar.
Clique em File>New>Unit. Salve a nova unit como “uMeioTransporte.pas”. Defina a nova classe como mostrado a seguir na Listagem 2. Nossa nova classe TMeioTransporte define o comportamento básico de todos os meios de transporte, sejam eles aviões ou carros (ou outros que venhamos a criar). Todo meio de transporte tem uma descrição e uma capacidade, que são atributos. Todos possuem a capacidade de se movimentar, que é sua funcionalidade. Então, podemos dizer que uma classe é basicamente a união de dados mais funcionalidade.
Quem desenvolveu em Pascal antigo, deve lembrar que não era possível criar estruturas que contivessem dados e funcionalidades (somente a partir do Turbo Pascal 5.5), era necessário criar um record (o struct do C) e colocar a funcionalidade que trabalhava essas informações em código à parte.
unit uMeioTransporte;
interface
type
TMeioTransporte = class
Descricao : string;
Capacidade : integer;
procedure Mover();
end;
implementation
{ TMeioTransporte }
procedure TMeioTransporte.Mover();
begin
end;
end.
Ok, já definimos nossa classe base. Agora vamos fazer TCarro herdar de TMeioTransporte, ou seja, todas as características definidas na classe base passarão a ser acessíveis na classe descendente (desde que sejam públicas ou protegidas – veremos mais sobre especificadores de visibilidade futuramente). Abra então a unit uCarro.pas e altere a definição da classe TCarro como mostrado na Listagem 3.
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();
end;
implementation
uses Dialogs;
{ TCarro }
procedure TCarro.Mover();
begin
ShowMessage(Descricao+' entrou em movimento.');
end;
end.
Os campos Capacidade e Descricao são agora herdados de TMeioTransporte. Mas e o método Mover, por que não foi removido? Porque nem todos os meios de transporte se movimentam da mesma forma, ou seja, cada descendente deve implementar da sua forma o método Mover. O que fizemos foi apenas declarar o método Mover na classe base para dizer que ele existe para todas as classes descendentes. Falaremos mais sobre isso a seguir, quando estudarmos o polimorfismo.
Repita os mesmos passos para a classe TAviao, fazendo a herança e removendo os campos Capacidade e Descricao. O novo código da classe TAviao é mostrado na Listagem 3.
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();
end;
implementation
uses Dialogs;
{ TAviao }
procedure TAviao.Mover();
begin
ShowMessage(Descricao+' está voando.');
end;
end.
Feito. Simples, fácil, direto, produtivo, elegante e prático. E funcional.
Métodos Estáticos
Existem dois tipos de métodos no Delphi: estáticos ou virtuais. Por padrão, todos os métodos de uma classe são estáticos, a menos que você indique que são virtuais através das palavras-chave virtual ou dynamic. Não se preocupe, vamos conhecer como funcionam essas palavras logo a seguir. Diferente de outras linguagens, não existe a necessidade de se usar algo como static para indicar que o método é estático.
A palabra-reservada dynamic surgiu no Delphi 4, é a mesma coisa que virtual, porém instrui o compilador a gerar uma VMT (Virtual Method Table) mais otimizada, especialmente para cadeias de classes com grande hierarquia (muitos ancestrais).
Mas qual é a diferença entre um método estático e virtual. Essa é a peça-chave para o polimorfismo, e aqui, ao invés de fazer analogia com o mundo real para entendermos esse funcionamento, vamos direto ao ponto, na prática. Vá até o formulário principal e localize a seguinte declaração, na seção public da classe:
public
Carro : TCarro;
Aviao : TAviao;
Substitua por:
public
Carro,Aviao : TMeioTransporte;
Declare uMeioTransporte na cláusula uses. Se você compilar o programa agora, receberá erros. Como a variável Carro agora é um TMeioTransporte ela não reconhece mais o campo Quilometragem. Faça então o seguinte TypeCasting:
TCarro(Carro).Quilometragem:=StrToIntDef(EdtQuilometragem.Text,0);
Faça o mesmo para a atribuição de HorasVoo de TAviao:
TAviao(Aviao).HorasVoo:=StrToIntDef(EdtHorasVoo.Text,0);
TypeCasting é um recurso da linguagem que permite converter um tipo de objeto em outro, desde que sejam compatíveis. O typecast pode ser feito de duas formas em Delphi:
-
Estilo C, direto:
TTipoDesejado(Objeto)
-
Com operador RTTI “as”:
(Objeto as TTipoDesejado)
A diferença entre o primeiro e o segundo é que, no estilo C, o compilador não usa RTTI (Runtime type information) para verificar se você está realmente convertendo um objeto compatível. Com o “as”, o compilador nos bastidores faz um teste como outro operador RTTI, o “is”, é mais seguro, mas mais lento (RTTI é sempre lento). Então, prefira usar o estilo C desde que tenha certeza do que está convertendo.
Isso que acabamos de fazer (RTTI) é uma má prática, má prática mesmo. Vou explicar mais adiante o porquê, quando estudarmos o polimorfismo, e assim resolveremos o problema de uma forma muito mais elegante.
Execute a aplicação. Você verá que tudo funciona perfeitamente, menos o botão Mover. Quando você chama o método Mover, você está chamado o método da classe TMeioTransporte, que não possui implementação. Isso porque Mover é um método estático (o padrão), e dessa forma ele fica vinculado à classe que você *declarou*. Observe a declaração das variáveis Aviao e Carro:
Observe agora como foram criados Carro e Aviao:
Aviao:=TAviao.Create;
Carro:=TCarro.Create;
Como você viu é possível declarar uma variável de um tipo e instanciar a partir de outro tipo (desde que seja descendente). Isso é possível porque TAviao e TCarro são TMeioTransporte. A recíproca não é verdadeira. Nem todo TMeioTransporte é um TAviao ou TCarro. Outro exemplo, na vida real, podemos dizer que gatos e cachorros são animais. Mas nem todo animal é um gato ou cachorro.
Quando você chama o método Carro.Mover, não importa se a instância atual faz referência a um TCarro. Como o objeto foi declarado com TMeioTransporte, o método Mover sempre será dessa classe (pois, novamente, ele é estático).
Veja que a Delphi Language, assim como o antigo Pascal e muitas outras linguagens de programação, é uma linguagem fortemente tipada. Não podemos, por exemplo, colocar uma string dentro de uma variável inteira. Porém, se usarmos herança, podemos atribuir objetos a diferentes tipos desde que tenha um ancestral comum.
Isso é amplamente utilizado na VCL do Delphi. Você já parou para se perguntar por que o gerador de relatórios Quick Report aponta para um TDataSet e não um TClientDataSet? Por que os controles data-aware são ligados a um TDataSource, que por sua vez se liga também a um TDataSet?
TDataSet funciona como um “contrato”. Ele foi definido há mais de 10 anos, como poderiam os engenheiros do Delphi adivinhar que tecnologias de acesso a dados seriam criadas nos anos seguintes? BDE, ADO, dbExpress, Zeos, ADO, ClientDataSet etc., todas jogam as regras de TDataSet, ou seja, implementam seus métodos virtuais, implementam o comportamento padrão. Logo, qualquer objeto que saiba trabalhar com esse padrão, poderá usar seus recursos sem conhecer detalhes sob sua implementação, isso é abstração.
É por isso que o Quick Report funciona (pelo menos em teoria) com todos esses engines de acesso a dados. E por isso que os controles data-aware independem de tecnologia de acesso. TDataSet esconde, abstrai, serve como base, um protocolo, um padrão. Não fosse isso, teríamos que ter classes especializadas para tratar cada uma das tecnologias de acesso, algo como TQuickReportForBDE, TQuickReportForDBX, TQuickReportForADO, ou pior ainda, TDBEditForBDE, TDBEditForDBX, TDBEditForClientDataSet (imagine multiplicar todos os tipos de TDataSets por todos os tipos de controles data-aware, certamente o IDE do Delphi precisaria de uma centena de paleta de componentes e o seu computador alguns GB de memória a mais).
Não é assim que funciona na vida real, não é assim que funciona na OO, não é assim que funciona na Delphi Language. Imagine que se para cada tipo de impressora, você tivesse que ter uma porta diferente do micro? Algo como PortaHP, PortaCanon, PortaElgin (agora forcei, lembrei da minha Lady 90). E se criarem um novo tipo, você não vai querer ir na fábrica do seu note para abrir um buraco na lateral e conectar uma impressora nova? Não é assim que as coisas funcionam. Inventaram um protocolo, um padrão, uma interface (veremos que interfaces na OO têm exatamente esta função), por exemplo a USB, e pronto, você conecta qualquer dispositivo ao seu micro, desde que siga esse padrão. A mesma coisa com o TQuickReport, com os controles data-aware, e por aí vai.
A partir do Delphi 7, a linguagem Object Pascal utilizada pelo Delphi, por questões de marketing, passou a se chamar Delphi Language. Então quando você disser “Delphi é uma linguagem” e alguém responder, “Não, Delphi é a ferramenta, a linguagem é o Pascal”, saiba que você está certo.
Lembra quando eu falei que usar typecast era horrível? Pois bem, agora você entenderá o porquê. Se não fosse o polimorfismo, uma classe que precisasse se ligar a outra teria que testar um por um dos possíveis tipos para poder chamar um método, como na Listagem 5. E o pior, se um novo tipo fosse criado (o que seria a coisa mais comum do mundo), precisaríamos abrir a classe e incluir novo código. Na mesma listagem, você vê a diferença brutal ao se optar por polimorfismo (nunca prefira RTTI ao polimorfismo, nem se precisar recorrer à POG).
procedure TFormulario.MoverMeioTransporteDeFormaBurra();
begin
// sem polimorfismo
if MeioTransporte is TCarro then
(MeioTransporte as TCarro).Mover();
if MeioTransporte is TAviao then
(MeioTransporte as TAviao).Mover();
if MeioTransporte is TBicicleta then
(MeioTransporte as TBicicleta).Mover();
if MeioTransporte is TBarco then
(MeioTransporte as TBarco).Mover();
if MeioTransporte is TTrem then
(MeioTransporte as TTrem).Mover();
if MeioTransporte is TCarroca then
(MeioTransporte as TCarroca).Mover();
if MeioTransporte is TDromedario then
(MeioTransporte as TDromedario).Mover();
if MeioTransporte is TOvni then
(MeioTransporte as TOvni).Mover();
if MeioTransporte is TMula then
(MeioTransporte as TMula).Mover();
...
end;
procedure TFormulario.MoverMeioTransporteDeFormaInteligente();
begin
// com polimorfismo
MeioTransporte.Mover();
end;
Tudo bem, já entendemos que isso só funciona com polimorfismo, não adianta apenas abstrair. As coisas mudam constantemente na vida real. Na tecnologia, pior ainda. Então precisamos preparar nosso código para as mudanças, que venham a acontecer. Mudar, tomar outra forma, uma nova implementação (desde que siga o padrão), isso é polimorfismo.
Métodos Virtuais e Dinâmicos - Polimorfismo
Vamos então ver como isso funciona na prática em nosso exemplo. Abra a unit uMeioTransporte e altere a declaração do método Mover como mostrado a seguir:
procedure Mover; virtual;
Virtual define um método que pode ser sobrescrito e assim tomar múltiplas formas em classes descendentes. Sobrescrito significa anular a implementação da classe base, fornecer uma outra forma de implementação, uma forma diferente. No entanto, podemos sobrescrever para adicionar mais funcionalidade, nesse caso, herdando funcionalidade da classe base e incluindo mais comportamento. Sim, isso é possível, com o uso de inherited.
Como se sobrescreve um método virtual? Indo direto ao ponto, com override. Abra a unit uCarro.pas e altere o método Mover como mostrado a seguir:
procedure Mover(); override;
Faça o mesmo para TAviao.Mover().
Dessa forma, se você agora chamar o método Mover de um objeto instanciado como TCarro, mas originalmente declarado como TMeioTransporte, fará uma chamada ao método TCarro.Mover, pois o método deixou de ser estático e passou a ser virtual (Figura 1), ou ainda conhecido como método polimórfico.
Inherited
Como você pode ver, o método Mover não possui implementação na classe base TMeioTransporte. Podemos implementar o comportamento padrão nessa classe e deixar que as classes descendentes chamem esse método, adicionando alguma funcionalidade mais específica.
Imagine que, por exemplo, todo o veículo precisa ser ligado antes de entrar em movimento. Você não teria que implementar isso em todas as classes descendentes de TMeioTransporte, pois esse comportamento é padrão para todas. Abra então a unit uMeioTransporte.pas e implemente o método Mover como mostrado na Listagem 6.
procedure TMeioTransporte.Mover;
begin
ShowMessage('Ligando '+Descricao);
end;
Declare Dialogs na cláusula uses. Agora localize a classe TCarro, vá até a implementação do método Mover e adicione a diretiva inherited como mostrado na Listagem 7.
procedure TCarro.Mover;
begin
inherited;
ShowMessage(Descricao+' entrou em movimento');
end;
Execute a aplicação. Adicione um carro à lista e clique no botão Mover. Como o método é virtual, será disparado o método Mover de TCarro. A primeira linha da implementação desse método contém um inherited, o que faz uma chamada ao método Mover da classe base (TMeioTransporte), e logo em seguida é executado o restante do código. Por último, chame inherited também no método Mover de TAviao. Veja o resultado na Figura 2.
Esse recurso é bastante poderoso, visto que podemos compartilhar uma mesma implementação herdada da classe base e adicionar alguma funcionalidade específica. Em uma linguagem não orientada a objetos, você provavelmente precisaria criar procedimentos diferentes e fazer vários testes condicionais com IF para saber que método deve ser chamado (como na Listagem 5).
Sugestão de leitura: Se quiser se aprofundar um pouco mais no lado “obscuro” do Delphi, sugiro a leitura da obra “Delphi: O Guia Essencial”, do autor Ray Lischner, editora O’Reilly. Das mais de 50 obras de Delphi da minha coleção, é a que mais detalha aspectos avançados da linguagem e VCL.
Sem dúvida, há muito mais a se falar sobre herança e polimorfismo. E sem dúvida, vamos ver muito mais sobre essas duas técnicas ainda neste curso. Mas com certeza você já viu que o recurso não está disponível por opção na linguagem, é obrigatório utilizá-lo. Sei que muitas dúvidas pairam a sua mente agora: se isso é realmente fantástico, como podemos aplicar o que aprendemos em meu sistema real, client/server com Firebird, dbExpress e ClientDataSet, que usa formulários, DataModules e controles data-aware?
Que tal começar mudando não o seu código, mas os seus conceitos, a sua mente, a sua maneira de ver as coisas? O meu objetivo aqui foi mostrar para você amigo leitor, como usar o polimorfismo e herança de uma forma simples, em um exemplo simples, com objetos e classes mais simples ainda. Mas não se preocupe nem duvide, no final, mostrarei um exemplo realíssimo de como utilizar essas técnicas para obter produtividade, reaproveitamento de código, maior facilidade na manutenção, boas práticas e finalmente, um código bem mais profissional.
Temos muitas coisas interessantes ainda pela frente neste pequeno exemplo! Nas próximas partes, veremos métodos abstratos, construtores e destrutores, especificadores de visibilidade, propriedades, métodos get / set e finalmente, vamos dar um passo além transformando nossas classes em componentes, e ligando meios de transporte a pessoas (da mesma forma que o QR se liga a um TDataSet), consolidando ainda mais os conceitos de abstração, herança e polimorfismo.
Grande abraço e até a próxima edição.