Programação Orientada aos Objectos em Pascal

Introdução

Há quem pense no Pascal como uma linguagem fraca e antiquada que não acompanhou os novos tempos. Mas, na realidade, o Pascal evoluiu em vários dialectos, sendo o mais proeminente o Delphi.

Hoje em dia é possível programar nesta linguagem segundo o paradigma POO (Programação Orientada aos Objectos), programar DLLs (Dynamic Link Libraries), fazer programas não só em consola mas também com recurso a GUI. Permite igualmente a ligação a bases de dados, como o MySQL, bem como a criação de IDEs. O grande exemplo é o, infelizmente descontinuado, IDE Dev-Pascal, da Bloodshed, um IDE open-source, escrito totalmente em Delphi e recorrendo a uma velha versão do FPC (Free Pascal Compiler) e do GPC (GNU Pascal Compiler) como compiladores opcionais integrados.

Posta esta breve introdução, os objectivos para o presente artigo são:

  • Dar a conhecer noções teóricas básicas sobre o paradigma POO;
  • Apresentar o dialecto Object Pascal;
  • Utilizar ferramentas do Free Pascal e do Delphi para criar programas segundo o paradigma POO;
  • Criar uma classe útil para os leitores.

Doravante, a linguagem Object Pascal será designada tão-somente por Pascal por duas razões:

  1. Genericamente, qualquer dialecto derivado do Pascal (leia-se Standard Pascal) ser designado exacta e somente por Pascal;
  2. Para simplificar a leitura do presente artigo.

Os trechos de código aqui presentes foram todos testados recorrendo ao Free Pascal 2.4.4.

Em todos os códigos aqui presentes foi utilizado o modo de compatibilidade com Delphi do Free Pascal, pelo que estará presente nestes a directiva de compilação {$mode delphi}.

Preliminares em POO

O âmbito do presente no artigo não é uma explicação alongada deste paradigma. Contudo, vale uma explicação no âmbito do Pascal e daquilo que será utilizado adiante.

Serão então abordados os conceitos principais deste paradigma, sendo estes: classes, métodos, propriedades, instâncias, herança e visibilidade (ou protecção).

Classes, métodos, propriedades e instâncias

Uma classe é um conjunto de objectos com características iguais. Um método da classe não é mais do que uma capacidade da classe, ou seja, um procedimento ou função. Uma propriedade é um atributo, isto é, uma característica do objecto. Uma instância da classe é um objecto com todas as características dessa mesma classe.

De forma prática, vamos considerar este paradigma num caso da vida real.

Considere-se a classe Ser Humano:

  • Propriedades – altura, peso, idade, sexo, cor do cabelo.
  • Métodos – andar, falar, sentar, dormir, cozinhar, trabalhar.

Igor Nunes é uma instância desta classe, por exemplo. Sendo uma instância, Igor Nunes terá exactamente as propriedades altura, peso, idade, etc., bem como os métodos andar, falar, dormir, etc. Já a instância João terá exactamente as mesmas propriedades e métodos. O próprio leitor é uma instância desta classe! Contudo, é de ter em atenção que cada instância tem valores distintos para cada propriedade altura, peso, idade…

Herança

Uma das capacidades mais impressionantes e úteis do paradigma POO é a possibilidade de uma classe herdar métodos e propriedades de outra. Neste âmbito, temos então duas classes, uma que deriva de outra:

  • Classe principal (ou Main Class) – classe que tem os métodos e propriedades gerais;
  • Classe derivada (ou Inherited Class) – classe que herda os métodos e propriedades da anterior – deriva da classe principal.

Voltando à classe Ser Humano:

  1. Mamífero é uma classe principal de Ser Humano;
  2. Caucasiano é uma classe derivada de Ser Humano;
  3. Reino Animal é uma classe principal de Mamífero;

Isto significa então que, respectivamente:

  1. Ser Humano é um Mamífero;
  2. Caucasiano é um Ser Humano;
  3. Mamífero é do Reino Animal.

Visibilidade de métodos e propriedades

A classe pode ter métodos e propriedades com visibilidades diferentes:

  • Públicas – podem ser vistos por qualquer outra classe, programa ou unidade;
  • Privadas – só podem ser vistos pela própria classe, isto é, pelos restantes métodos e propriedades, independentemente da sua visibilidade. Só estão ocultos para fora da classe, e não para dentro da classe. São igualmente ocultos para classes derivadas.
  • Protegidas – são visíveis tão-somente pela classe e por qualquer outra que derive desta.

Mais visibilidades existem, e algumas variam de linguagem para linguagem. Contudo, ficam estas três, as mais importantes.

Considerando a classe Ser Humano, temos que, por exemplo:

  • O método falar é protegido, já que não é visível a outras classes como Leão, Pinguim ou Urso, mas é visível às classes que lhe têm herança, como por exemplo Etnias e Raças;
  • A propriedade altura é pública, já que qualquer um pode ver e saber quanto mede um ser humano em altura;

De referir que, se nenhum bloco de visibilidade for criado, então tudo o que for declarado na classe é considerado público por defeito.

Colocada a teoria essencial do paradigma POO, segue-se a exemplificação da sua implementação em Pascal, não antes de dar a conhecer o dialecto do Pascal que suporta este mesmo paradigma.

O Object Pascal

Apresentado em 1986, o Object Pascal foi uma linguagem de programação com a mesma sintaxe do Standard Pascal, de 1971, e já com as modificações introduzidas pelo Extended Pascal, e foi concebido por uma equipa da Apple Computer (hoje Apple Inc.), liderada por Larry Tesler, em conjunto com o criador do Pascal, Niklaus Wirth.

O seu nome original teria sido Clascal, e foi concebido devido ao facto de este ser necessário para uma Framework da época, a MacApp.

Deu-se assim o suporte do Pascal ao paradigma POO.

Com a introdução do POO no Pascal, esta linguagem tornou-se muito mais poderosa e moderna, sendo cada vez mais um exemplo da estruturação de um programa, módulo ou biblioteca.

A sintaxe do Pascal variou consoante os dialectos que foram surgindo. Contudo, podemos ter dois padrões de referência, sendo o utilizado neste artigo o segundo:

  • Free Pascal;
  • Delphi.

Passemos então à implementação em código.

Classes

Um conjunto novo de palavras reservadas surgiu, e entre elas estão as palavras reservadas Object e Class. É de referir que um Object pode ser qualquer coisa, sendo, portanto, a mãe de todos os tipos de dados, incluindo procedimentos e funções.

Para agora, a palavra que nos interessa é a Class. Com esta palavra reservada criamos uma classe, tal como o seu nome sugere.

Em Pascal, uma classe deverá ser sempre declarada num tipo (type) já que as instâncias da classe serão criadas como variáveis (var).

Este tipo não começa com a palavra reservada begin mas sim com a própria palavra class, mas termina obrigatoriamente com end. Dentro deste bloco que se cria teremos a declaração, mas não programação, dos métodos da classe. Igualmente, aqui estarão as propriedades.

Para definir as visibilidades, podemos criar três blocos. Nenhum deles necessita de estar num bloco beginend já que se considera que o bloco termina quando é encontrada a declaração de um outro bloco, ou o fim da classe.

Na prática, a estrutura de uma classe será a seguinte:

type Nome_Classe = class
        private
            // parte privada
        protected
            // parte protegida
        public
            // parte pública
    end;

Métodos

A parte principal de uma classe é o conjunto dos seus métodos – as “habilidades” da classe, os processos que esta é capaz de realizar. E, em Pascal, os métodos não são mais do que procedimentos e funções.

Na classe, estes apenas são declarados. Nunca são implementados nesta parte:

type Nome_Classe = class
         private
             function funcao(const texto : tipo_dado) : tipo_output;
         public
             procedure procedimento(const argumentos : tipo_dado);
 end;

Neste caso não temos métodos protegidos. Como podemos verificar, os métodos da classe são declarados dentro dos seus blocos.

Então, onde são implementados? A resposta é: tal como outro procedimento e função quaisquer: fora do bloco principal do programa.

Neste caso, queremos que a função privada conversor converta um carácter ASCII para o seu sucessor (“A” passará a ser “B”, por exemplo), e escrever será o método público que irá escrever o resultado da função privada conversor.

PROGRAM exemplo;
 
type CEx = class
        private
            function conversor(const texto : string) : string;
        public
            procedure escrever(const texto : string);
    end;
 
procedure CEx.escrever(const texto : string);
begin
    writeln(conversor(texto));
end;
 
function CEx.conversor(const texto : string) : string;
var i : integer;
    t : string;
begin
    t := '';
    for i:=1 to length(texto) do
        t := t + succ(texto[i]);
        result := t;
end;
 
BEGIN
    (* Bloco principal de execução *)
END.

Como podemos reparar, apesar de conversor ser uma função privada, escrever pode-lhe aceder já que ambos estes métodos pertencem à mesma classe. Em suma, o programa principal poderá aceder ao procedimento escrever mas nunca à função conversor.

Além disso, e muito importante de reter, é o facto de o método de cada classe ser programado identificando em primeiro lugar qual a classe a que pertence o método, e só depois dizendo o nome do método.

procedure/function classe.método({argumentos}) {: tipo_output};

Propriedades

Uma propriedade, por si só, não possui nenhum valor. Uma propriedade é um “atalho” para uma variável que, e essa sim, possui um valor específico. Por exemplo, o nosso peso é uma medida de massa que aparece na balança, mas, por detrás desse valor, está um mecanismo que mediu, registou e fez output desse mesmo valor. Neste exemplo, abordámos tudo o que há numa propriedade:

  • Input de um dado (com o seu devido registo);
  • Output desse mesmo dado (com a sua devida leitura).

Uma propriedade é, então, um atributo da classe que inclui, opcionalmente, um conjunto de dois métodos (um input e outro output) que atribuem e lêem o valor de uma variável da classe. Opcionalmente por uma razão: uma propriedade será ReadOnly (só de leitura) se só fornecer output e WriteOnly (só de escrita) se só poder ser atribuída mas não acedida para leitura.

Considerando o caso geral de uma propriedade que permite leitura e escrita:

property propriedade : tipo_dado read metodo_leitura write metodo_escrita;

Para melhor entender este conceito, vamos partir de um exemplo prático e que será já parte da nossa implementação prática:

type TAngle = class
    private
        VDEG : real; // Variável da qual depende a propriedade
        procedure DefinirValorDEG(const valor : real);
    public
        property ValorDEG : real read VDEG write DefinirValorDEG;
    end;
 
procedure TAngle.DefinirValorDEG(const valor : real);
(* Atribui valor à propriedade em graus *)
begin
    self.VDEG := valor;
end;

Isto é o que acontece na prática e na grande maioria das propriedades. Como podemos ver, a propriedade, de seu nome ValorDEG, tem uma variável “por detrás” que possui o seu valor: a variável privada VDEG.

Aqui começamos já a ganhar noção da vantagem das visibilidades das propriedades: o que nos interessa é que se veja a propriedade da classe, e não a variável na qual está atribuída o valor dessa propriedade. Assim, mantemos essa variável privada.

Para fazer o output da propriedade, bastará ler o valor da sua variável VDEG, pelo que o nome do método de escrita é o próprio nome da variável. O método de escrita deverá ser sempre uma função, mas como a variável em si é uma função, este processo torna-se válido.

Já a escrita necessita de um procedimento. Neste caso, criámos o procedimento DefinirValorDEG que tem única e exclusivamente um argumento. Todo e qualquer método de escrita deverá ter um e um só argumento, sendo este do mesmo tipo que a variável da qual depende a propriedade. É este argumento que recebe o valor a ser atribuído à propriedade, leia-se à variável.

Neste caso, DefinirValorDEG recebe valor e atribuí-o à variável VDEG. Para não confundir com nenhuma variável global com o mesmo nome que pode ser criado para o programa, utilizamos a palavra reservada self, que indica ao compilador que a variável VDEG é a referente à classe cujo método está ali a ser programado: neste caso, a classe TAngle.

Construtores

Uma instância de uma classe, que será vista a seguir, não pode surgir do nada. A instância tem de ter um construtor de modo a que a variável que criarmos seja, de facto, uma instância. Caso não façamos a construção, a variável do tipo da classe será uma referência nula e, logo, surgirá uma excepção na execução do programa.

O tipo de dados geral TObject tem o seu construtor: o Create. Vamos invocar este construtor nas nossas classes para podermos construir o nosso. O processo de herança será feito pela palavra reservada inherited.

O construtor deverá ser público, e é declarado pela palavra reservada constructor. Poderá receber argumentos ou não, depende do processo de construção da classe.

Regra geral, com o construtor damos os valores por default às propriedades da classe. Estes valores poderão vir dos argumentos.

type Nome_Classe = class
        private
            // parte privada
        protected
            // parte protegida
        public
            constructor Create({argumentos do construtor});
    end;


constructor Classe.Create({argumentos do construtor});
begin
    inherited Create; // Invoca construtor da classe ascendente TObject.
    (* Código do construtor *)
end;

Instância

Por fim, agora que a classe está criada, vamos criar instâncias desta. Uma instância é criada por uma variável do tipo da classe. Em Pascal:

var Instância : Nome_Classe;

No bloco de execução, antes de utilizar qualquer método ou atribuir ou ler qualquer propriedade, é necessário criar a instância através do construtor:

Instância := Nome_Classe.Create({argumentos do construtor});

Note-se bem que, no construtor, é o nome da classe que se coloca e não o nome da instância!

E, daqui em diante, pode-se utilizar qualquer método da classe, atribuir e ler propriedades desta, mas, para cada instância, cada valor. Se tivermos dez instâncias, cada uma pode ter um valor diferente para uma mesma propriedade. Isto é perceptível no caso da classe Ser Humano:

  • O João pesa 75Kg;
  • O André pesa 64Kg;
  • A Joana pesa os seus leves 52Kg;
  • Já a Mariana chega aos 86Kg;

Destrutor

Já que neste artigo não vamos criar destrutores, fica a referência que, para qualquer classe, podemos utilizar o destrutor geral: o Free.

Antes de terminar um programa, as instâncias devem ser, então, “destruídas”:

Instância.Free;

Implementação prática

Terminada que está a introdução teórica à utilização de Programação Orientada aos Objectos em Pascal, a melhor forma de consolidar estes conhecimentos é com um exercício prático.

Criemos, então, uma classe, de seu nome TAngle, e que permita guardar o valor de um ângulo em graus e radianos e tenha as devidas funções de conversão. Para tal, teremos:

  • Métodos:
    • Conversor radianos –> graus;
    • Conversor graus –> radianos.
  • Propriedades:
    • Valor do ângulo em graus;
    • Valor do ângulo em radianos.

Para as propriedades, necessitaremos dos devidos e respectivos métodos de escrita:

  • Definir valor em graus;
  • Definir valor em radianos.

E, para a criação de uma instância da classe, necessitaremos de um construtor, construtor esse que terá dois argumentos:

  • Valor do ângulo;
  • Tipo de ângulo (graus ou radianos).

Para o tipo de ângulo, vamos criar especialmente o tipo de dados TMedidasAngulo, e que não deve ser confundido com a classe TAngle, e que assumirá os “valores” deg ou rad, respectivamente para graus e para radianos.

Além disso, o objectivo é que, quando se cria ou se modifica o ângulo, o valor mude automaticamente para os respectivos valores no outro tipo de ângulo. Ou seja, se atribuirmos em graus, o valor em radianos é modificado automaticamente para o valor correspondente.

Por fim, no programa, vamos testar rapidamente o construtor e os conversores.

Passando tudo isto para Pascal:

{$mode delphi} // Utiliza a nomenclatura do Delphi.
PROGRAM Teste_Classes;
uses crt, math; // Unidades necessárias
 
type TMedidasAngulo = (Deg, Rad); // Tipo de ângulo: Graus ou Radianos.
    TAngle = class
        private (* Só visível para a classe *)
            // Variáveis onde são guardados os valores das propriedades.
            VRAD : real; // Radianos
            VDEG : real; // Graus
 
            // Métodos que permitem atribuir valores às propriedades.
            procedure DefinirValorRAD(const valor : real);
            procedure DefinirValorDEG(const valor : real);
 
        public (* Visível para todo o programa *)
            // Propriedades
            property ValorRAD : real read VRAD write DefinirValorRAD;
            property ValorDEG : real read VDEG write DefinirValorDEG;
 
            // Conversores
            function Deg2Rad(const deg : real) : real;
            function Rad2Deg(const rad : real) : real;
 
            // Construtor da classe
            constructor Create(const valor : real; TipoAng : TMedidasAngulo);
    end;
 
 
constructor TAngle.Create(const valor : real; TipoAng : TMedidasAngulo);
(* Construtor *)
begin
    inherited Create; // Invoca construtor da classe ascendente TObject.
    case TipoAng of
 // Conversores, consoante o tipo de ângulo introduzido.
        Rad : begin
              self.ValorRAD := valor;
              self.ValorDEG := Rad2Deg(valor);
              end;
        Deg : begin
              self.ValorDEG := valor;
              self.ValorRAD := Deg2Rad(valor);
              end;
    end;
end;
 
procedure TAngle.DefinirValorRAD(const valor : real);
(* Atribui valor à propriedade em radianos *)
begin
    self.VRAD := valor;
    self.VDEG := self.Rad2Deg(valor);
end;
 
procedure TAngle.DefinirValorDEG(const valor : real);
(* Atribui valor à propriedade em graus *)
begin
    self.VDEG := valor;
    self.VRAD := self.Deg2Rad(valor);
end;
 
function TAngle.Deg2Rad(const deg : real) : real;
begin
    result := (pi * deg) / 180;
end;
 
function TAngle.Rad2Deg(const rad : real) : real;
begin
    result := (rad * 180) / pi;
end;
 
 
var Angulo : TAngle;
 
BEGIN
    Angulo := TAngle.Create(90, deg); // cria ângulo de 90º
    writeln('Em radianos: ', Angulo.Deg2Rad(Angulo.ValorDEG):0:3);
    // Converte em radianos
    Angulo.ValorRAD := pi; // atribui 180º em radianos
    writeln('pi(rad) em graus: ', Angulo.Rad2Deg(Angulo.ValorRAD):0:3);
    readln; // pausa
    Angulo.Free();
END.

Apesar de os valores convertidos serem facilmente obtidos pela propriedade correspondente ao tipo de ângulo pretendido, a forma como realizámos a conversão nos procedimentos writeln demonstram como se podem utilizar vários métodos de uma vez só.

Note-se que se pode utilizar o bloco with para se evitar escrever continuamente o nome da instância seguido do método ou propriedade que se pretende. Contudo é necessária precaução no caso de haver várias instâncias e classes cujos métodos e propriedades partilham os mesmos nomes. Em caso de dúvida sobre o que vai o compilador assumir, considere sempre escrever na forma completa: nome_classe.método().

Fica então assim uma classe útil que o leitor poderá utilizar livremente nas suas programações em Pascal. Sinta-se à vontade para a expandir com novos métodos e propriedades, já que a melhor forma de aprender é a tentar.

Conclusão

Após este artigo, percebemos que o Pascal, já há quase 30 anos atrás, suporta o paradigma da Programação Orientada aos Objectos sem que a sua sintaxe original tivesse de ser alterada: apenas houve ligeiras adaptações e a entrada de novas palavras reservadas.

A entrada deste paradigma reforçou ainda mais o objectivo desta linguagem de programação: a programação estruturada, com “cabeça, tronco e membros” como se diz na gíria popular.

Conclui-se, sendo assim, que, apesar de muitos lhe chamarem uma linguagem ultrapassada, o Pascal está longe de ser uma linguagem do passado. Pascal é uma linguagem do século XXI.

Links úteis

Uma lista de documentos úteis da Wiki P@P, relacionados com o presente artigo.

Fontes bibliográficas de apoio

Publicado na edição 33 (PDF) da Revista PROGRAMAR.