Parte 3

<< Click to Display Table of Contents >>

Navigation:  Novatos > Orientação a objetos >

Parte 3

Previous pageReturn to chapter overviewNext page

Descrição

 

O objetivo deste artigo é continuar outros dois publicados anteriormente sobre Orientação a Objetos e Delphi. Pretende se aprofundar um pouco mais em um dos três principais conceitos da OO, o polimorfismo.

 

Pré-requisitos

 

Habilidade em programação com Delphi em qualquer versão, conhecimento de Object Pascal, cabeça aberta, e ter lido o os dois primeiros artigos já anteriormente publicados, parte I e parte II.

 

Introdução

 

Eis que aprendemos no primeiro dos artigos o que é objeto. Aprendemos, completamente, não. Conseguimos ter uma noção do assunto. Eis que no segundo, adentramos um pouco no universo da herança e os significados dela para a analogia do mundo real. A herança, que estudamos na publicação anterior, é o primeiro pilar dos três da OO. Por ter fundamental importância, fizemos questão de apresentá-lo primeiro, mostrando um pouco do seu poder de classificação. Mostramos ainda alguns recursos de reutilização de código providos pela herança.

 

Nesta seção vamos abordar o polimorfismo, que é mais um dos três requisitos fundamentais a uma linguagem ser considerada OO. Este recurso está diretamente ligado à herança, e a incrementa de tal maneira que os objetos que dele fazem uso adquirem um alto grau de flexibilidade e auto-suficiência. Nota-se que, pouco a pouco, os programadores que fazem uso deste recurso acabam por ter cada vez menos preocupação e responsabilidade sobre o comportamento de seus objetos, delegando-o para eles próprios.

O que é polimorfismo, afinal?

 

Bom, tentaremos explicar o que é polimorfismo em poucas linhas, muito embora isto não seja nada fácil. O polimorfismo, ao grosso modo, é a capacidade de objetos pertencentes a uma mesma classe se comportarem diferentemente, a depender da classe concreta a que eles pertencem. Fomos claros? Acho que ainda não...

 

Mas como temos certeza que nossos leitores são bastante espertos, sabemos que eles perceberam a palavra “comportarem” na explicação acima, embora não a tenham entendido direito. Comportamento, como já tratamos, tem a ver com métodos. E o polimorfismo se aplica exatamente a isso, aos métodos. Vejamos um exemplo de onde aplicá-lo:

 

unit Unit1;

 

type

  TVeiculo = class

    Posicao: TPoint;

    procedure MoverSe(const p: TPoint);

  end;

 

  TCarro = class(TVeiculo)

    Pneus: Integer;

  end;

 

  TAviao = class(TVeiculo)

    Asas: Integer;

  end;

 

No nosso exemplo temos três declarações de classes, sendo uma ancestral, TVeiculo, e duas descendentes, TCarro e TAviao. Nota-se que TVeiculo possui um método MoverSe, que por dedução, irá deslocar o nosso veículo em termos de posição. Nota-se ainda que ambos os subtipos de veículos – carro e avião – se movem.

 

Mas existe um problema no nosso modelo. Acontece que um avião se move de maneira diferente de um carro. Se estivéssemos falando em termos de animação gráfica, o nosso carro se moveria pela terra, girando os pneus. O avião, por sua vez, se moveria pelo ar, girando as turbinas.

 

Em outras palavras, a modelagem está correta em termos das classes. O método mover está relacionado com sua classe mais ancestral, o TVeiculo, e ele descreve muito bem o que deve ser feito. Mas não descreve com perfeição como isto deve ser feito. Aliás, este é um grande propósito do polimorfismo em relação às classes: separar o que do como, ou os fins dos meios, como diria Maquiavel.

 

A proposta do polimorfismo aqui é permitir que o carro e o avião possam se mover de forma diferente e própria, sem afetar a declaração e a abstração da sua classe ancestral TVeiculo. Assim, será possível manter os dois tipos de objeto fiéis à declaração da classe dos veículos.

 

Agora vamos corrigir nosso exemplo:

 

unit Unit1;

 

type

  TVeiculo = class

    Posicao: TPoint;

    procedure MoverSe(const p: TPoint); virtual;

  end;

 

  TCarro = class(TVeiculo)

    Pneus: Integer;

    procedure MoverSe(const p: TPoint); override;

  end;

 

  TAviao = class(TVeiculo)

    Asas: Integer;

    procedure MoverSe(const p: TPoint); override;

  end;

 

E vamos implementá-lo:

 

implementation

 

procedure TVeiculo.MoverSe(const p: TPoint);

begin

  //aqui nada interessa ser feito

end;

 

procedure TCarro.MoverSe(const p: TPoint);

begin

  ShowMessage('Estou girando minhas rodas sobre o solo');

  Posicao := p;

end;

 

procedure TAviao.MoverSe(const p: TPoint);

begin

  ShowMessage('Estou girando minhas turbinas no ar');

  Posicao := p;

end;

 

Agora vamos entender o que aconteceu. Primeiramente, declaramos o método na classe TVeiculo com uma diretiva virtual. Ainda não explicaremos o real significado disto neste parágrafo. Mas por enquanto vamos assumir que isto quer dizer que o método é polimórfico, ou seja, pode assumir outros comportamentos (diferentemente dos métodos estáticos). Depois declaramos os mesmos métodos nas classes descendentes com uma diretiva override, que é quem de fato irá implementá-los. E então, desenvolvemos os três métodos em código. O MoverSe de TVeiculo nada faz, e o MoverSe de cada tipo concreto de veículo o implementa cada um à sua maneira.

 

Pois eis que, caro quase-programador OO, nós demos implementações diferentes de um mesmo método para diferentes subclasses desta mesma classe. Traduzindo esta frase, o que fizemos foi dizer a TVeiculo que ele se move. Logo em seguida, demos a cada um dos subtipos de veículo o direito de se moverem cada um do seu jeito, e ainda sob a mesma chamada de método em TVeiculo.

 

Pois acontece que se referenciarmos uma instancia de TCarro por uma variável do tipo TVeiculo, e mandarmos que execute o método MoverSe, o nosso veículo irá se mover como um carro. E assim, chegamos ao grande pulo do gato da OO.

 

Vejamos outro exemplo:

 

unit Unit1;

 

const

  PI = 3.141592654;

  RAIZ_DE_DOIS = 1.4142135;

 

type

  TFigura = class

    Centro: TPoint;

    constructor Create(const Pos: TPoint);

    procedure Desenhar; virtual;

    function Area: Real; virtual;

  end;

 

  TCirculo = class(TFigura)

    Raio: Integer;

    procedure Desenhar; override;

    function Area: Real; override;

  end;

 

  TQuadrado = class(TFigura)

    Lado: Integer;

    procedure Desenhar; override;

    function Area: Real; override;

  end;

 

implementation

 

constructor TFigura.Create(const Pos: TPoint);

begin

  inherited Create;

  Centro := Pos;

end;

 

procedure TFigura.Desenhar;

begin

  //nada aqui

end;

 

function TFigura.Area: Real;

begin

  //nem aqui

end;

 

procedure TCirculo.Desenhar;

var 

  Rect: TRect;

begin

  Rect.Left := Centro.X – Round(Raio/RAIZ_DE_DOIS);  

  Rect.Top := Centro.Y – Round(Raio/RAIZ_DE_DOIS);   

  Rect.Right := Centro.X + Round(Raio/RAIZ_DE_DOIS); 

  Rect.Bottom := Centro.Y + Round(Raio/RAIZ_DE_DOIS);

  Form1.Canvas.Ellipse(Rect);

end;

 

function TCirculo.Area: Real;

begin

  Result := 2*PI*Raio;

end;

 

procedure TQuadrado.Desenhar;

var 

  Rect: TRect;

begin

  Rect.Left := Centro.X – Lado div 2;

  Rect.Top := Centro.Y – Lado div 2;

  Rect.Right := Centro.X + Lado div 2;

  Rect.Bottom := Centro.Y + Lado div 2;

  Form1.Canvas.Rectang(Rect);

end;

 

function TQuadrado.Area: Real;

begin

  Result := Power(Lado, 2);

end;

 

Bom, este nosso último exemplo talvez tenha trazido a coisa um pouco mais à prática, até porque imaginamos que o leitor deve ter pensado por muitas vezes “mas para que eu vou querer animais, vegetais ou veículos dentro do meu programa?”. Este nosso exemplo traz à tona o conceito de polimorfismo e tanto pode como deve ser implementado para que o leitor o veja acontecer.

 

Como se pode notar, todas as chamadas à execução fazem referencia a um objeto Form1.  Obviamente estamos nos referindo ao form principal do nosso exemplo. Entretanto isto não é uma boa prática de programação, levando a uma situação que os analistas e engenheiros de software chamam de forte acoplamento.

 

Forte acoplamento quer dizer que as suas classes são muito dependentes deste form, e uma mudança nesta parte pode requerer uma mudança em todas as classes fortemente acopladas. Isto também compromete a reutilização de suas classes para outros projetos. Mas não vamos nos preocupar com isso aqui. Em próximas publicações iremos abordar o acoplamento e boas práticas de programação.

 

O código abaixo é uma sugestão de aplicação do código exemplificado acima, para que o leitor veja o polimorfismo em funcionamento:

 

type
 
  TForm1 = class(TForm)
 
    Button1: TButton;
 
    procedure Button1Click(Sender: TObject); 
 
  private
 
    { Private declarations }
 
  public
 
    Figuras: array [1..10] of TFigura;
 
    { Public declarations }
 
  end;
 
 
 
implementation
 
 
 
procedure TForm1.FormCreate(Sender: TObject);
 
var i: Integer; NovaPos: TPoint;
 
begin
 
  Randomize;
 
  for i := 1 to 10 do
 
  begin
 
    NovaPos.X := Random(Self.Width);
 
    NovaPos.Y := Random(Self.Height); //sorteia a posição na janela
 
    if Random(10) > 5 then //sorteia qual figura irá instanciar
 
      Figuras[i] := TCirculo.Create(NovaPos)
 
    else
 
      Figuras[i] := TQuadrado.Create(NovaPos)
 
  end;
 
end
 
 
 
procedure TForm1.Button1Click(Sender: TObject);
 
var i: Integer;
 
begin
 
  Self.Repaint;
 
  for i := 1 to 10 do
 
    Figuras[i].Desenhar;
 
end

 

 

Note que, ao executar Button1Click(), todas as figuras serão desenhadas, cada uma a seu modo.

 

Métodos Abstratos

 

No artigo anterior vimos que o que torna uma classe abstrata de fato é a existência de métodos abstratos. Pois bem que métodos abstratos são aqueles que não possuem nenhuma implementação naquela classe, deixando para serem implementados apenas pelas classes descendentes.

 

Voltemos ao nosso último exemplo, envolvendo círculos e quadrados. Note que o método Desenhar foi declarado virtual na classe TFigura, e implementado nas classes TCirculo e TQuadrado. Entretanto, este método ainda é implementado nesta classe, pois não foi declarado abstrato. Se o leitor notar, existe uma declaração vazia deste método em TFigura, por mais que nenhum código tenha sido colocado ali dentro.

 

Acontece que se fosse instanciado, juntamente com os círculos e quadrados, um objeto concreto do tipo TFigura, no momento em que o método Desenhar fosse invocado, ele seria executado de fato. Para ter certeza disto, implemente-o da seguinte maneira:

 

procedure TFigura.Desenhar;
 
begin
 
  ShowMessage('Erro: este método não deveria ser chamado!!');
 
end;

 

E procure instanciar também objetos do tipo TFigura, usando o construtor TFigura.Create dentro do for-loop no método FormCreate. Você perceberá que este método será executado fiel como foi declarado na classe TFigura. Porém, se quisermos declarar o método Desenhar como abstrato, removeremos a sua implementação e faremos a sua declaração da seguinte maneira:

 
type
 
  TFigura = class
 
    Centro: TPoint;
 
    constructor Create(const Pos: TPoint);
 
    procedure Desenhar; virtualabstract;
 
    function Area: Real; virtual;
 
  end;

 

A razão da existência de métodos abstratos é “obrigar” que o desenvolvedor sempre implemente estes métodos nas classes inferiores, estabelecendo uma boa regra de consistência no comportamento das classes.

 

Mas porque então não declaramos logo todos os métodos polimórficos como abstratos? Acontece que um método polimórfico não deve ser necessariamente abstrato. A classe pode ser abstrata, contendo métodos abstratos, e ainda assim conter um método virtual não-abstrato entre eles. Dessa maneira, podemos estabelecer métodos polimórficos com um comportamento default para as outras classes, e a menos que a classe descendente o sobrescreva, ela se comportará desta maneira. Deixaremos a cargo do leitor descobrir situações deste tipo. Não se preocupe, não será difícil encontrá-las.

 

Em contrapartida, o uso de métodos abstratos pode ser desagradável caso o leitor realmente queira instanciar aquela classe. Aí, novamente, cairemos na situação em que a classe não deveria ser definida abstrata, e a chamada deste método irá retornar um erro em tempo de compilação com a mensagem “abstract error”. Como já lamentamos no artigo anterior, a permissividade de se construir classes abstratas é particular do Delphi.

 

Acessando os métodos originais

 

Suponhamos que você está escrevendo uma classe descendente de outra, aplicando polimorfismo em alguns de seus métodos. Suponha ainda que seu método não foi declarado abstrato, e ele possui uma implementação. Vejamos o exemplo:

 

  TCirculoPartido = class(TCirculo)
 
    procedure Desenhar; override;
 
  end;

 

 

Esta nova classe que inventamos precisa desenhar um círculo com duas linhas de diâmetro, partindo-o em forma de cruz. Sabe-se que, além de ser considerada um círculo, ela também precisa de muitas das implementações já feitas em TCirculo, modificando apenas o método Desenhar, e por isso fica óbvia uma situação de herança.

 

Ora, se vamos desenhar o círculo e seus dois diâmetros, eis que faremos a mesma coisa que TCirculo.Desenhar já fazia, com algo a mais. Para isso, existe um meio de evitar que tenhamos que reescrever todo este código do círculo dentro de TCirculoPartido novamente.  Assim, chamamos a palavra reservada inherited com o nome do método. Vejamos como fica o nosso novo código:

 

procedure TCirculoPartido.Desenhar;
 
var Origem, Destino: TPoint;
 
  procedure DesenhaLinha;
 
  begin
 
    Form1.Canvas.MoveTo(Origem.X, Origem.Y);
 
    Form1.Canvas.LineTo(Destino.X, Destino.Y);
 
  end;
 
begin
 
  inherited Desenhar; //que surte o mesmo efeito de chamar somente inherited;
 
  Origem.Y := Centro.Y;
 
  Origem.X := Centro.X – Raio;
 
  Destino.Y := Centro.Y;
 
  Destino.X := Centro.X + Raio;
 
  DesenhaLinha;
 
  Origem.X := Centro.X;
 
  Origem.Y := Centro.Y - Raio;
 
  Destino.X := Centro.X;
 
  Destino.Y := Centro.Y + Raio;
 
  DesenhaLinha;
 
end;

 

O objetivo do inherited é informar ao compilador que o método chamado deve ser o da classe imediatamente superior (classe pai), ignorando um mesmo método declarado na classe atual. Em Java equivale a invocar super.

 

Métodos virtuais versus Métodos dinâmicos

 

Bom, fugindo um pouco da linha do assunto, há alguns parágrafos acima tivemos que usar uma palavra reservada virtual como diretiva depois de um método, para dizer ao compilador que ele era polimórfico. Eis que agora explicaremos o porquê.

 

Existem dois tipos de métodos polimórficos, quanto à implementação interna do compilador: métodos virtuais (virtual) e métodos dinâmicos (dynamic). À visão do programador, eles são idênticos, e fazem exatamente a mesma coisa. A diferença está em como o compilador recorre a estes métodos.

 

Sabe-se que métodos virtuais contra métodos dinâmicos são uma razão entre eficiência contra tamanho do código gerado. Muito embora você perceba chamadas à diretiva dynamic nos componentes da VCL, a própria Borland recomenda que se use o virtual na grande maioria dos casos. Para saber mais detalhes sobre estes dois tipos de implementação, recomendamos que consulte a documentação da Borland, disponível na ajuda do Delphi, ou no site http://www.borland.com.

 

Considerações Finais

 

Demonstramos com este artigo a aplicação do polimorfismo na orientação a objetos, dando uma visão superficial dos benefícios de se modelar um sistema OO usando herança e polimorfismo. Como dissemos, o polimorfismo é capaz de fazer coisas fantásticas, retirando muitas das responsabilidades de implementação dos programadores e trazendo-as para as classes. Se não existisse o polimorfismo, nossas classes ficariam, no mínimo, extensas e cheias de testes condicionais, o que tornaria o código, na melhor das hipóteses, de difícil compreensão.

 

Apenas com herança e polimorfismo já imaginamos que há todo um universo a ser aprendido e transformado pela cabeça ortodoxas dos programadores que não conheciam OO. Mas sem dúvida, trata-se de uma excelente maneira de programar, tanto a título de facilidade e agilidade de codificação, quanto de manutenibilidade do código e eficiência. Enfim, não parece haver motivos conhecidos para não se aplicar OO, especialmente com os benefícios trazidos pela herança e polimorfismo.

 

Na próxima publicação deveremos falar sobre encapsulamento, que é o (nosso) terceiro pilar dos conceitos da OO, abordando a visibilidade, e aplicando-os na prática.

 

Mais

***

Parte 4