Artigo Clube Delphi 110 - Introdução à POO – Parte 3

Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Para efetuar o download você precisa estar logado. Clique aqui para efetuar o login
Confirmar voto
0
 (3)  (0)

Este artigo aborda a orientação a objetos com o Delphi, usando uma metodologia simples, didática, de fácil aprendizado.

Atenção: esse artigo tem uma palestra complementar. Clique e assista!

[links]Introdução à Poo - Parte 1
Introdução à Poo - Parte 2
Introdução à Poo - Parte 4
Introdução à Poo - Parte 5
Introdução à Poo - Parte 6[/links][rotulo-curso/]

[lead]Do que trata o artigo

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.

Para que serve

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.

Em que situação o tema é útil

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.

Resumo do DevMan

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. [/lead]

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.

[subtitulo]Métodos abstratos[/subtitulo]

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.

Listagem 1. Comportamento básico para todos os meios de transporte

  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).

Listagem 2. Método abstrato

  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á.

[nota]Nota: 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. [/nota]

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).

Listagem 3. Sobrescrevendo um método virtual e abstrato: 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
      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. 

Listagem 4. Sobrescrevendo um método virtual e abstrato: 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
      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:

1 – 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;

2 – Mover de TCarro faz uma chamada à inherited o que invoca o método de mesmo nome na classe base, TMeioTransporte.Mover;

3 – TMeioTransporte.Mover chama Ligar para ligar o meio de transporte;

4 – Ligar é um método virtual / abstrato, logo será chamado TCarro.Ligar;

5 – 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).

[subtitulo]Construtores e Destrutores[/subtitulo]

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).

Listagem 5. Implementando um construtor: TCarro

  constructor TCarro.create();
  begin
    inherited; // chama o construtor da classe base
    Quilometragem := 0;
  end; 

Listagem 6. Implementando um construtor: TAviao

  constructor TAviao.create();
  begin
    inherited; // chama o construtor da classe base
    HorasVoo := 0;
  end; 

Listagem 7. Implementando um construtor: TMeioTransporte

  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.

Listagem 8. Implementando um destrutor

  destructor destroy; override;
  ...
  destructor TMeioTransporte.destroy;
  begin
    // seu código de limpeza aqui
    inherited;
  end; 

[nota]Nota: 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. [/nota]

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.

Listagem 9. Exemplo de construtor / destrutor na VCL

  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; 

[nota]Nota do DevMan

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;
  … 
[/nota]

[subtitulo]Especificadores de Visibilidade[/subtitulo]

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.

Listagem 10. Especificadores de visibilidade / modificadores

  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.

[nota]Nota do DevMan

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. [/nota]

[nota]Nota do DevMan

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. [/nota]

[nota]Nota do DevMan

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. [/nota]

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.

[nota]Nota: 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. [/nota]

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.

Listagem 11. TMeioTransporte, agora bem estruturada (porém sem propriedades)

  TMeioTransporte = class
  private
    FCapacidade : integer;
    FDescricao : string;
  protected
    procedure Ligar(); virtual; abstract;
  public
    constructor create();
    destructor destroy(); override;
    procedure Mover(); virtual;    
  end; 

[subtitulo]Propriedades[/subtitulo]

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.

Listagem 12. Propriedades

  ...
  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.

Listagem 13. Código final das classes, com especificadores de visibilidade e propriedades

  [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. 

[subtitulo]Métodos Get/Set[/subtitulo]

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.

[nota]Nota: Veremos mais sobre propriedades, rotinas get/set e especificadores de visibilidade nas próximas partes desta série, quando criarmos um componente. [/nota]

[subtitulo]Conclusão[/subtitulo]

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é a próxima edição!

 
Você precisa estar logado para dar um feedback. Clique aqui para efetuar o login
Receba nossas novidades
Ficou com alguma dúvida?