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:
- Genericamente, qualquer dialecto derivado do Pascal (leia-se Standard Pascal) ser designado exacta e somente por Pascal;
- 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:
- Mamífero é uma classe principal de Ser Humano;
- Caucasiano é uma classe derivada de Ser Humano;
- Reino Animal é uma classe principal de Mamífero;
Isto significa então que, respectivamente:
- Ser Humano é um Mamífero;
- Caucasiano é um Ser Humano;
- 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 begin
–end
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.
- Tutorial de Pascal (2011)
- Tutorial de Delphi – noções básicas
- Procedimentos e Funções – Passagem por Parâmetro / Passagem por Referência
- Indentação