Parte 2

<< Click to Display Table of Contents >>

Navigation:  Novatos > Orientação a objetos >

Parte 2

Previous pageReturn to chapter overviewNext page

Descrição

 

Este artigo tem como objetivo continuar uma outra publicação sobre Orientação a Objetos e Delphi já feita. Pretende estender o que foi abordado, permitindo àqueles que ainda não conhecem a OO – ou que, agora, já tem uma noção – se aprofundarem um pouco mais. Procura ainda tratar os conceitos de uma maneira mais prática e menos tediosa do que as comumente encontradas.

 

Pré-requisitos

 

Habilidade em programação com Delphi em qualquer versão, conhecimento de Object Pascal, cabeça aberta, e ter lido o primeiro dos artigos, publicado na home page do canal Delphi.

 

Introdução

 

Uma vez cumpridos os pré-requisitos dispostos logo acima, pressupõe-se agora que o leitor já saiba o que é um objeto, o que é uma classe e uma instância, o que são atributos, e o que são métodos. Pressupõe-se, ainda, que já saiba construir e destruir objetos. Logo, podemos dar continuidade à nossa superficial abordagem sobre OO.

 

Conforme já foi dito na publicação anterior, ainda existem muitos conceitos fundamentais a serem vistos sobre Orientação a Objetos – conceitos estes que, aplicados, tornam a programação bastante poderosa, quando comparado ao paradigma anterior. Muito ainda se há por falar sobre este estilo que virou a programação dos anos 60 de cabeça para baixo. Portanto se você, caro leitor, acha que porque leu o artigo anterior já sabe fazer programas orientados a objeto, saiba que está redondamente enganado.

 

Nesta seção abordaremos de maneira prática, um dos três princípios que tornam uma linguagem de fato Orientada a Objetos: herança, polimorfismo e encapsulamento. Falaremos da herança.

 

Generalização e Especialização

 

Voltemos a tratar de um objeto novamente como um elemento do mundo. Ele é definido por uma determinada classe, e uma vez construído, toma a sua forma concreta, isto é, passa a existir como instância desta classe. Logo, este objeto é um elemento pertencente a esta classe. No exemplo anterior, definimos uma classe de animais chamada TAnimal, contendo alguns atributos e métodos, e estabelecemos uma instância desta classe, a qual nos referimos por MeuAnimal.

 

Entretanto, surge que classes únicas e separadas entre si, como supomos antes, não são suficientes para definir todo um universo e as coisas que nele existem. Isto acontece principalmente porque alguns objetos podem pertencer a mais de uma classe ao mesmo tempo. Na verdade, quase todos eles provavelmente pertencem.

 

Podemos dizer que um determinado livro é uma instância da classe dos livros, enquanto o mesmo livro também pertence à classe dos materiais escolares. E por que não? Neste caso, surge uma questão bastante comum no nosso cotidiano, no que diz respeito à classificação das coisas. Eis que a questão é: algumas classes são subconjuntos de outras. A classe dos livros pode ser vista como um subconjunto especial da classe dos materiais escolares. Em palavras mais sérias, diríamos que os materiais escolares generalizam os livros. Os livros, por sua vez, seriam uma especialização dos materiais escolares, se olharmos pelo sentido contrário.

 

Se pararmos para observar, existem muitas outras situações no nosso cotidiano envolvendo este assunto. As pessoas, por exemplo, são uma classe especial de mamíferos, estes que por sua vez são uma subclasse de animais, enquanto estes últimos são generalizados por seres vivos. Se o leitor olhar ao próprio redor, irá encontrar muitos outros casos bem parecidos.

 

E como resolver a esta questão? Eis que surge a herança, que é um recurso voltado a resolver este tipo de problema, trazendo ainda consigo uma série de outras vantagens que vêm a otimizar o desenvolvimento de soluções em código executável. Falaremos sobre ela a seguir.

 

Herança

 

Nada de muito extraordinário ao nosso entendimento será tratado aqui, uma vez que já temos alguma noção de generalização e especialização. Extraordinário com certeza é o poder que este recurso tem. A herança vem a ser a forma usada pelas linguagens OO para implementar a especialização. Uma determinada classe que especialize outra, diremos que ela herda desta. Assim dizemos que a classe herdada é a ancestral, e a herdeira é a descendente. Vejamos o exemplo abaixo, adaptado do nosso exemplo do artigo anterior:

 

unit Unit1;

 

type

  TSerVivo = class

  end;

 

  TAnimal = class(TSerVivo)

  end;

 

  TVegetal = class(TSerVivo)

  end;

 

 

A partir deste par de parênteses depois da declaração da classe TAnimal, fizemos esta classe herdar de TSerVivo. Em outras palavras, nosso sistema agora entende que todo animal também é um ser vivo, e o especializa. O mesmo se aplica a TVegetal. E é assim que o nosso programa enxergará estes objetos: as instâncias de TAnimal e de TVegetal agora serão, todas ao mesmo tempo, também pertencentes à classe TSerVivo, devendo ser tratadas como tal. Uma classe pode ter inúmeras classes descendentes diferentes.

 

E a classe TSerVivo, herda de quem? De ninguém? Excelente pergunta. E bem oportuna. Quando você não declara uma ancestral à sua classe, o delphi automaticamente o faz descendente da classe mais ancestral de todas, a classe TObject.

 

Agora vamos complicar um pouquinho mais o nosso modelo:

 

unit Unit1;

 

type

 

TSerVivo = class

  Alimento: string;

  Peso: Real;

end;

 

TAnimal = class(TSerVivo)

  Membros: Integer;

  Velocidade: Real;

end;

 

TVegetal = class(TSerVivo)

  Fruto: string;

end;

 

Agora além de uma relação de herança entre as classes TSerVivo e as descendentes TAnimal e TVegetal, demos atributos a cada uma delas. Todo TAnimal agora possui uma velocidade de locomoção, representado pelo atributo Velocidade, bem como um número de membros. Os vegetais por sua vez terão o nome do fruto que produzem. De igual forma, demos a todo TSerVivo o direito de escolha por um alimento preferido, representado pelo campo Alimento e uma informação de peso, pelo campo Peso.

 

Agora chegamos num ponto interessante. Dada as seguintes referências:

 

var

  MeuSerVivo: TSerVivo;

  MeuAnimal: TAnimal;

 

Poderemos facilmente atribuir um número qualquer de membros a MeuAnimal, desde que ele esteja instanciado. De igual maneira, podemos informar a MeuSerVivo que ele gosta de comer 'frutas tropicais', simplesmente atribuindo-lhe o valor ao respectivo atributo. Mas podemos atribuir um peso a MeuAnimal? A resposta é uma outra pergunta: Por que não?

 

Como poderia se imaginar, todo animal também é um ser vivo e, como ser vivo, ele também respeita às características e regras estabelecidas para todos os seres vivos. Em outras palavras, TAnimal literalmente herda os atributos de TSerVivo, podendo ser tão manipulável nisto enquanto TAnimal, como qualquer instancia de TSerVivo. E isto se aplica tanto aos atributos quanto aos métodos.

 

Não existe um limite máximo para quantos níveis de especialização se pode haver entre classes. Uma classe ancestral, tal como é TObject, pode ter milhares de descendentes em cascata, e isto é o torna os sistemas OO dotados de um amplo poder de classificação. Agora vamos incrementar nosso modelo um pouco mais:

 

unit Unit1;

 

type

 

TSerVivo = class

  Alimento: string;

  Peso: Real;

  procedure AlimentarSe(const Alimento: string);

end;

 

TAnimal = class(TSerVivo)

  Membros: Integer;

  Velocidade: Real;

  procedure LocomoverSe(const Direcao: string);

end;

 

TVegetal = class(TSerVivo)

  Fruto: string;

  procedure ProduzirOxigenio;

end;

 

var

  MeuSerVivo: TSerVivo;

  MeuAnimal: TAnimal;

  MeuVegetal: TVegetal;

 

implementation

 

procedure TSerVivo.AlimentarSe(const Alimento: string);

begin

  if Self.Alimento <> Alimento then

    ShowMessage('Estou comendo ' + Alimento + ', mas eu gostaria e estar' +

                'comendo ' + AlimentoPreferido)

  else

    ShowMessage('Estou comendo ' + Alimento + ', que é o que eu Gosto');

  Peso := Peso + 1;

end;

 

procedure TAnimal.LocomoverSe(const Direcao: string);

var 

  i: Integer;

begin

  for i := 1 to Membros do

    ShowMessage('Estou movendo meu membro No.' + i + ' para ir a ' + Direcao);

 

  ShowMessage('Estou a ' + FloatToStr(Velocidade) + ' Km/h');

end;

 

procedure TVegetal.ProduzirOxigenio;

begin

  ShowMessage('Estou contribuindo para a atmosfera do nosso planeta.');

end;

 

end.

 

Ora, sabe-se que apenas os vegetais produzem oxigênio, e por isso declaramos o método ProduzirOxigenio apenas em TVegetal. Mas sabe-se que tanto os animais quanto os vegetais se alimentam, e por isso tivemos o cuidado de declarar o método AlimentarSe na classe imediatamente superior.

 

Você perceberá que poderá invocar o método AlimentarSe também de MeuAnimal, e de MeuVegetal uma vez que ele também é uma instância de TSerVivo. Entretanto, vale notar que os elementos membros de TAnimal não são acessíveis por instancias de TSerVivo, já que seres vivos não são necessariamente animais.

 

Mas observa-se que a herança possui um grande papel tanto na questão de modelar classes relacionadas quanto na economia de trabalho dos programadores. Se considerarmos uma classe B qualquer herdado de uma classe A, teremos que B trará automaticamente para si todas as características (atributos e métodos) de A, não precisando assim reescrevê-las.

 

Declarando X, Instanciando Y

 

Como vimos logo acima, um objeto pode ser instância de uma classe que é descendente de uma outra mais ancestral. E este objeto pode ser tratado como uma instância desta última classe mais genérica. Logo, o critério de generalização disposto pela herança nos permite uma boa flexibilidade quanto às referências. Vejamos:

 

var

  MeuSerVivo: TSerVivo;

 

implementation

 

procedure TForm1.Button6Click(Sender: TObject);

begin

  MeuSerVivo := TAnimal.Create;

end;

 

Pode-se ver, o exemplo acima declara uma referência a um TSerVivo, e a contraria, instanciando para ela um objeto do tipo TAnimal. Isto é permitido? Sim, é claro. Como já demonstramos, todo TAnimal também é um TSerVivo, herda todas as suas características, e não existe nenhuma razão lógica que proíba isso. Apenas a situação contrária não seria permitida, já que nem todo ser vivo se trata de um animal.

 

O único inconveniente que isso provoca é o fato de que, uma vez a referência sendo do tipo da classe superior, não há como saber de imediato que a instância apontada é da classe inferior. Logo, os atributos e métodos exclusivos de TAnimal não estarão disponíveis em MeuSerVivo em tempo de compilação. Verifique isso digitando MeuSerVivo. [control + espaço], e você perceberá que os únicos atributos e métodos disponíveis são os declarados até a classe TSerVivo.

 

Mas isso não quer dizer que você não tenha como acessá-los por causa disso. Uma vez que você tem certeza que a instância apontada pela referência MeuSerVivo é do tipo TAnimal, você pode informar ao compilador que se trata de uma instância de TAnimal, fazendo o chamado typecasting para esta classe, e assim resgatar novamente os atributos e métodos de TAnimal. Para isso, usa-se o operador as:

 

procedure TForm1.Button6Click(Sender: TObject);

begin

  MeuSerVivo := TAnimal.Create;

  (MeuSerVivo as TAnimal).LocomoverSe('Leste');

end;

 

Feito isso, o compilador passa a “entender” que o objeto apontado por MeuSerVivo é do tipo TAnimal, confiando na palavra de honra do programador, e liberando assim os seus atributos e métodos para uso. Nota: ao fazer typecasting, certifique-se de que realmente o objeto apontado seja da instância declarada com o as, ou isto poderá resultar numa exceção em tempo de execução.

 

Herança Múltipla

 

Bom, agora vamos fugir um pouco ao assunto para tratar deste outro um pouco mais controverso. Herança Múltipla é um conceito um pouco mais complicado de herança nas linguagens OO, e bastante questionado entre os estudiosos da área. Sabe-se que a herança múltipla é implementada em certos pontos no Ansi C++, não posso garantir em todos.

 

A herança múltipla visa permitir que uma classe possa herdar características de mais de uma classe ancestral ao mesmo tempo, fazendo, por exemplo, que um carro-anfíbio possa herdar das classes Carro e Barco ao mesmo tempo. Trata-se de um recurso bastante poderoso em termos de linguagem. Mas é igualmente perigoso, e possivelmente complicado no desenvolvimento do seu compilador.

 

O Delphi, como muitas das linguagens OO comerciais e amadurecidas, não implementa a herança múltipla. Por isso não entraremos em mais detalhes sobre esse assunto aqui. Mas o Delphi, bem como o Java, usam de recursos que podem simular a herança múltipla em determinados aspectos que são benéficos – especialmente os relacionados à tipagem (classificação) e à abstração – e ao mesmo tempo isolar outros que podem ser perigosos e complicados – como a herança de atributos. A este “recurso” usado pelo Delphi e pelo Java, damos o nome de interface. Ainda não veremos isto nesta publicação, ficando para as posteriores.

 

Classes Concretas e Abstratas

 

Bom, em meio às dezenas – às vezes centenas – de classes existentes num modelo, notaremos que algumas dessas classes poderão ser instanciadas, outras não. Isto se deve ao fato de que, muito embora dois objetos que pertençam a uma classe mais ancestral possuam características comuns, acontece de não haver como existir um elemento que represente, de forma física e real, aquela classe em questão.

 

Voltamos ao nosso exemplo de TAnimal e TSerVivo. Muito embora nada no Delphi nos impeça de instanciar um TSerVivo na sua pura essência, isso não faz o menor sentido. Você já viu algum ser vivo que não seja instância de uma classe mais inferior?

 

A casos como este, em que não existe nenhuma razão para a construção de objetos especificamente de uma classe, chamamos esta classe de classe abstrata. Ao caso contrário, isto é, de classes instanciáveis, chamamos de classes concretas. Classes abstratas geralmente são ancestrais de outras classes concretas. A recíproca é verdadeira.

 

Infelizmente no caso do nosso exemplo só tratamos de classes abstratas – é claro que vamos desconsiderar o construtor Create invocado no exemplo artigo anterior. Mas em termos conceituais, não faz sentido se instanciar um TAnimal. Se alguém lhe perguntasse “qual o seu bicho de estimação?”, e você respondesse “é um animal”, a pessoa voltaria a perguntar “Qual animal?”, no melhor dos casos. No pior, lhe chamaria de louco.

 

Então resolvamos o problema do seu bicho de estimação da seguinte maneira:

 

type

  TCachorro = class(TAnimal)

    Dono: string;

    procedure Latir;

  end;

 

Agora sim, poderemos citar uma classe concreta! Até porque existem cachorros de verdade no nosso mundo, sob a forma real de cachorro. Animais também existem, porém, sob forma de cachorros, gatos, etc. Nunca como animal apenas.

 

Por vias formais, definimos como uma classe abstrata, aquela que possui métodos abstratos. Métodos abstratos são aqueles que não possuem implementação física na classe. No Delphi eles são declarados por uma diretiva abstract depois da sua assinatura. Veremos isto mais adiante, numa próxima publicação. Ao contrário do Delphi, algumas linguagens como Java não permitem a instanciação de classes abstratas.

 

Se você instanciar uma classe abstrata no delphi, como TStrings por exemplo, ele permitirá, embora lhe dê um aviso em compilação “constructing instance of abstract class”. O erro só ocorrerá em tempo de execução, quando você invocar um método abstrato. Daí, para evitar o problema, sempre instancie classes concretas, nunca abstratas. Uma boa candidata a ser construída de maneira correta neste caso é a classe TStringList, descendente de TStrings.

 

Este tipo de permissividade do Delphi, de instanciar classes abstratas, leva ao questionamento de alguns pesquisadores, tanto em termos de consistência do projeto quanto da compreensão do modelo especificado. Java, nesse aspecto, parece ser mais fiel ao paradigma OO, por não permitir a compilação.

 

Construtores e Destruidores específicos

 

Suponha agora que aquele seu cachorro – aquele declarado na seção anterior – tenha um dono. Logo, nada nos custa dar um atributo Dono ao pobre animal. Suponha ainda que na regra do nosso projeto não possam existir cachorros sem dono, em nenhum momento, e nem se pode correr esse risco.

 

Se você considerar que usará o construtor padrão, derivado de TObject (aquele Create sem parâmetros), deverá admitir que o desenvolvedor que usar sua classe poderá esquecer de dar um dono ao cachorro. Isto, em alguns casos, poderá trazer conseqüências inesperadas ou até desagradáveis ao seu objeto.

 

Daí, em casos deste tipo utiliza-se um construtor específico à sua classe, declarando-o portador de parâmetros que sejam fundamentais à sua criação. Vejamos:

 

type

  TCachorro = class(TAnimal)

    Dono: string;

    constructor Create(const Dono: string);

    procedure Latir;

  end;

 

implementation

 

constructor TCachorro.Create(const Dono: string);

begin

  inherited Create;

  Self.Dono := Dono;

end;

 

A partir de agora, nenhum cachorro no modelo poderá ser instanciado sem que lhe haja o nome do dono. Nota-se que o nosso construtor faz uma chamada ao construtor da ancestral, através do qualificador inherited (que quer dizer herdado), que é quem de fato irá criar a classe na memória. Veremos as chamadas inherited em mais detalhes no próximo artigo. A variável Self, vale informar, é uma referência para a instância em questão, fazendo diferenciar neste caso o que é o atributo Dono do parâmetro Dono.

 

Logo aproveitamos para falar que uma boa prática de desenvolvimento de classes, é sempre procurar obrigar a informação dos dados importantes da criação no seu construtor. O uso de construtores específicos se faz importante também quando se deseja executar alguma ação (código) no momento da criação do objeto.

 

De igual maneira, podemos personalizar o destruidor da nossa classe, pela declaração abaixo:

 

type

  TCachorro = class(TAnimal)

    Dono: string;

    constructor Create(const Dono: string);

    destructor Destroy; override;

    procedure Latir;

  end;

 

implementation

 

constructor TCachorro.Create(const Dono: string);

begin

  inherited Create;

  Self.Dono := Dono;

end;

 

destructor TCachorro.Destroy;

begin

  ShowMessage('Eu precisava muito exibir esta mensagem antes de er a destruído!');

  Inherited;

end;

 

Esta declaração faz a nossa classe sobrescrever o destruidor padrão, chamando-o dentro do código. Observe que a chamada inherited, que é quem de fato irá destruir o objeto, vem por último. No próximo artigo, quando estivermos falando de polimorfismo, explicaremos melhor estes detalhes. A assinatura do destruidor, vale ressaltar, é padrão, não podendo ser acrescida de parâmetros como fizemos com o Create.

 

Dica: Recomendamos aos desenvolvedores sempre evitarem o uso explícito do Destroy. ele não checa se a referência é nula antes de destruí-lo, o que pode ser desagradável em caso de ser nula. Procure sempre usar o método Free derivado de TObject, que faz recorrência ao Destroy, porém sem perder a segurança.

 

Considerações Finais

 

Novamente voltamos a dizer que não concluímos tudo o que temos a dizer sobre orientação a objetos com esse artigo. Aqui, tratamos apenas um dos três pilares da programação OO.

 

A herança é um recurso extremamente poderoso, podendo ser em alguns momentos considerada por nós como o mais importante dos conceitos relacionados à orientação a objetos. A herança é, também, uma parte crítica da modelagem e do projeto de seu sistema. Um sistema bem modelado possui traços de herança bem definidos, quanto ao propósito, à classificação e às funcionalidades de cada classe. Um sistema mal modelado pode acabar ficando comprometido.

 

Recomendo aos leitores interessados a trazer OO à suas vidas de desenvolvedores, que pratiquem herança no seu dia a dia. Comecem por classificar as coisas do mundo que existem ao seu redor, passando pelas classes de negócio (isto é, aquelas classes que só deverão existir neste seu sistema, especificamente), até chegar em classes consideradas utilitárias (ou seja, classes que fazem funções gerais, e podem ser usadas em vários sistemas diferentes). Uma boa prática de herança leva à excelência de se desenvolver bons projetos, com boas estruturas e pouco re-trabalho.

 

Mais

Parte 3