• Nenhum resultado encontrado

Figura 3.1 Modelo da programação estruturada

N/A
N/A
Protected

Academic year: 2021

Share "Figura 3.1 Modelo da programação estruturada"

Copied!
11
0
0

Texto

(1)

3

3

-

-

C

C

on

o

n

c

c

e

e

i

i

t

t

o

o

s

s

d

d

e

e

O

O

r

r

i

i

e

e

n

n

t

t

a

a

ç

ç

ã

ã

o

o

a

a

o

o

s

s

O

O

b

b

j

j

e

e

c

c

t

t

o

o

s

s

Hoje em dia, praticamente todo o software está a ser escrito numa ou noutra linguagem orientada aos objectos. As linguagens estruturadas como o C e o Pascal tiveram muito sucesso nos anos 70 e 80. Desde os anos 90 que a programação orientada aos objectos – OOP (Object-Oriented Programming) ganhou especial relevância, dominando a indústria informática. Os exemplos mais familiares de linguagens orientadas aos objectos são o C++, o Java, o Delphi e o SmallTalk.

Porquê esta mudança? As linguagens estruturadas têm um modelo de programação simples. Tipicamente, o programador pensa numa ou mais estruturas de dados que lhe modele o problema e, em seguida, desenvolve um conjunto de operações (funções) que actuam sobre essas estruturas de dados (figura 3.1).

Figura 3.1 – Modelo da programação estruturada

Embora este modelo de desenvolvimento funcione bem para pequenos programas, existem sérios problemas quando a dimensão dos sistemas começa a aumentar.

(2)

O problema é que uma vez que todas as operações têm acesso a todos os dados, uma pequena modificação num dos módulos pode ter implicações em todo o programa. À medida que os programas crescem, torna-se muito difícil manter o código. É simples de constatar: mudar o nome de uma variável numa estrutura de dados pode implicar mudar o seu nome em milhares de linhas de código onde esta é utilizada. Vejamos um outro exemplo: se uma função pode alterar o valor de uma variável global, sem que o resto das funções tenham “consciência” ou esperem essa alteração, isso poderá levar a graves erros de funcionamento. Estes problemas são muito difíceis de retirar do código. Costuma-se dizer que neste tipo de arquitectura existe um elevado acoplamento entre módulos.

A programação orientada aos objectos tenta aliviar alguns destes problemas. A ideia principal é diminuir o acoplamento entre os diversos módulos. Para isso, cada uma das estruturas de dados é encapsulada dentro de um objecto, que também possui funções que actuam sobre essa estrutura de dados. Os dados não são directamente visíveis para o exterior do objecto. Apenas a interface, isto é, as operações disponibilizadas no objecto, é visível. Em OOP o programador pensa em termos de objectos que modelam o seu problema e nas relações entre eles. As estruturas de dados específicas e a implementação das operações sobre as mesmas devem ser apenas “detalhes” de implementação. A figura 3.2 ilustra esta ideia.

Figura 3.2 – Modelo da programação orientada aos objectos

Uma questão bastante complexa é a forma como se consegue chegar a um conjunto de objectos e relações que modelem o problema correctamente. O objectivo deste livro não é ensinar o leitor todo este processo. O tema é demasiado extenso e complexo para o discutirmos em profundidade aqui. Caso o leitor não tenha experiência em design orientado ao objecto, recomendamos o livro de Grady Booch – Object-Oriented Analysis

and Design with Applications. Não é realmente possível aprender a programar bem no

paradigma de orientação aos objectos, lendo simplesmente um livro que ensina a sintaxe usada numa linguagem. Embora isso seja mais ou menos possível numa linguagem estruturada, os conceitos envolvidos em orientação aos objectos são muito mais elaborados. É usual dizer-se que para aprender OOP começa-se por ler um livro que

(3)

ensine a sintaxe de uma linguagem OOP, em seguida lê-se um livro sobre design OOP e finalmente, só a experiência pode ajudar o programador.

No entanto, antes de começarmos a discutir a sintaxe do C# em detalhe, vamos discutir um pouco dos principais conceitos associados à OOP.

3.1 Conceitos básicos

A programação orientada aos objectos assenta em três conceitos básicos fundamentais: encapsulamento de informação, composição/herança e polimorfismo. Iremos examinar cada um deles. No entanto, caso o leitor não consiga perceber todos os conceitos, não se preocupe. Mais à frente iremos discutir em detalhe a sintaxe utilizada. Nesta altura o que importa é ficar com as noções básicas sobre estes três pilares fundamentais. Todos os conceitos aqui abordados serão largamente examinados em capítulos posteriores.

3.2 Encapsulamento de informação

Tal como foi dito antes, um dos pontos fulcrais da OOP é o esconder as estruturas de dados dentro de certas entidades (objectos), aos quais são associadas funções (métodos) que manipulam essas estruturas de dados. As estruturas de dados não devem ser visíveis para outros objectos, apenas a sua interface (isto é, os seus métodos ou funções).

Como é que se começa este processo? O programador começa por definir “classes”. Uma classe representa um tipo abstracto de dados – famílias de entidades. Por exemplo, um empregado de uma empresa poderá corresponder a uma classe Empregado:

class Empregado {

private string Nome; // Nome da pessoa private int Idade; // Idade da pessoa

// Construtor: inicializa os elementos internos de um objecto public Empregado(string nomeDaPessoa, int idadeDaPessoa) {

Nome = nomeDaPessoa; Idade = idadeDaPessoa; }

// Mostra a informação sobre a pessoa public void MostraInformacao()

{

Console.WriteLine("{0} tem {1} anos", Nome, Idade); }

}

Um Empregado tem internamente armazenado um nome e uma idade. É de notar que

antes da declaração das variáveis Nome e Idade encontra-se a palavra-chave private. O

que isto quer dizer é que apenas esta classe pode utilizar estas variáveis. Nenhuma outra classe pode aceder às variáveis. A informação é “escondida” dentro da sua classe.

(4)

Esta classe possui também um construtor Empregado(string nomeDaPessoa, int idadeDaPessoa) e um método MostraInformacao(). Ambos são public. Sempre que

uma entidade é declarada como public, qualquer outra lhe pode aceder. Sempre que

uma entidade é declarada como private, apenas os elementos pertencentes à mesma

classe lhe têm acesso.

Para que serve o construtor? O construtor permite criar uma nova instância da classe. Isto é, permite criar um novo objecto dessa classe. Por exemplo:

Empregado chefeProjecto = new Empregado("Luís Silva", 34); chefeProjecto.MostraInformacao();

faz com que seja criada uma nova instância da classe (um objecto), que irá guardar o nome e a idade do empregado no seu interior. Ao chamar

chefeProjecto.MostraInformação(), o método é invocado naquele objecto em

particular – o empregado “Luís Silva”. Em qualquer altura podemos criar diversos objectos da mesma classe, que estes possuem identidades distintas:

Empregado engenheiro1 = new Empregado("Magda Dionísio", 25); Empregado engenheiro2 = new Empregado("Cecília Cardoso", 25); engenheiro1.MostraInformacao();

engenheiro2.MostraInformacao();

Ao executar este segmento de código, surgirá no ecrã:

Magda Dionísio tem 25 anos Cecília Cardoso tem 25 anos

É de notar que não é válido escrever expressões como:

Console.WriteLine("{0}", engenheiro1.Nome);

Uma vez que Nome é declarado como private, apenas elementos da sua própria classe

lhe conseguirão aceder. É certo que é possível declarar todos os elementos de uma classe como sendo públicos, mas aí estão a perder-se todas as vantagens de utilizar uma linguagem orientada aos objectos, pois está-se a aumentar o acoplamento total da aplicação.

Uma outra questão importante é o operador new. Este operador é utilizado sempre que se

está a criar uma instância de uma classe, isto é, um objecto. Este operador trata de encontrar e reservar a memória necessária para conter o objecto e de chamar o construtor do mesmo, finalmente retornando uma referência para o objecto criado.

3.3 Composição e herança

Quando um programador está a desenhar uma aplicação orientada aos objectos, começa por tentar encontrar classes. Cada classe tem uma determinada responsabilidade e representa uma entidade concreta do mundo real. Uma classe pode ter no seu interior objectos de outras classes ou relações para estes. Por exemplo, podemos ter na classe

(5)

vez, cada uma destas classes irá possuir os seus dados e métodos. A este tipo de relação chama-se composição, sendo a relação mais típica do design orientado aos objectos. A linha com um quadrado numa das pontas indica uma relação de composição, estando o losango do lado da classe que contém uma referência para a outra.

Figura 3.3 – Relação de composição

No entanto, existe um outro tipo de relação bastante comum e muito importante, é a relação de herança. Consideremos ainda o exemplo da classe Empregado. Imaginemos

agora que numa aplicação que utilize esta classe surge uma nova classe que representa o patrão da empresa. Isto é, existe uma classe Patrao. Tal como um empregado normal, o

patrão possui um nome e uma idade. No entanto, possui ainda uma característica que é ter um certo número de acções da empresa.

Uma possível solução para este problema seria criar uma nova classe que tivesse como campos o nome, a idade e o número de acções que o patrão possui. No entanto, iríamos também de ter de duplicar os métodos existentes, para além dos dados já presentes em

Empregado. Uma outra possível solução seria utilizar composição e colocar dentro da

classe Patrao uma instância de Empregado. No entanto, novamente aqui temos o

problema de ter de duplicar os métodos de Empregado na classe Patrao.

Quando surge este tipo de problemas, em que uma classe é uma especialização de uma outra (Patrao é um caso especial de Empregado: o patrão é um empregado da empresa),

estamos na presença de uma relação de herança. Isto é, existe uma classe que possui todos os elementos que outra possui, mas também possui mais alguns, sejam estes métodos ou dados. No nosso exemplo particular, dizemos que Patrao é uma classe derivada (ou

herdada) da classe Empregado (figura 3.4). A seta indica a relação de herança, indo da

(6)

Figura 3.4 – Relação de herança

Também é vulgar chamar à classe Empregado classe base, uma vez que está a ser

utilizada como base de uma outra classe que se está a definir. Vejamos como é representada esta relação:

class Patrao : Empregado {

private int NumeroAccoes; // Número de acções da empresa public Patrao(string nomeDoPatrao, int idadeDoPatrao, int nAccoes) : base(nomeDoPatrao, idadeDoPatrao)

{

NumeroAccoes = nAccoes; }

// Mostra o número de acções do patrão public void MostraAccoes()

{

Console.WriteLine("O número de acções é: {0}", NumeroAccoes); }

}

A primeira mudança aparente é na declaração da classe:

class Patrao : Empregado {

... }

o que isto quer dizer é que a classe Patrao deriva de Empregado, tendo todas as variáveis

e métodos presentes na classe base (Empregado). Note-se também a modificação no

construtor:

public Patrao(string nomeDoPatrao, int idadeDoPatrao, int nAccoes) : base(nomeDoPatrao, idadeDoPatrao)

{ ... }

Como a classe é diferente, o construtor também tem de ser diferente. Após a declaração do construtor, é indicado após os dois pontos a forma como a classe base tem de ser construída. Isto é, antes de um “patrão ser um patrão, tem de ser um empregado”. Assim, a palavra-chave base representa o construtor da classe acima (Empregado). Neste caso é

(7)

indicado que o objecto base Empregado deve ser inicializado com as variáveis nomeDoPatrao e idadeDoPatrao. Finalmente, no corpo do construtor propriamente dito,

é feita a inicialização da variável que faltava (NumeroAccoes).

Um ponto extremamente relevante é que um objecto Patrao também é um objecto Empregado, possuindo todos os métodos que este tem. Assim, o seguinte código é

perfeitamente válido:

Patrao donoDaEmpresa = new Patrao("Manuel Marques", 61, 1000000); donoDaEmpresa.MostraInformacao();

donoDaEmpresa.MostraAccoes();

É de salientar que na maioria das aplicações não existem muitas relações de herança. As relações de herança são extremamente úteis quanto se está a desenvolver bibliotecas para serem utilizadas (ou reutilizadas) por outros. Um erro muito comum das pessoas que se encontram a aprender OOP pela primeira vez é pensarem que a herança tem de, forçosamente, ser utilizada na solução de todos os problemas. Isso não é verdade. Só se deve utilizar herança em casos em que traga vantagens claras de reutilização ou caso a abstracção seja de facto algo que seja bem implementado em objectos concretos derivados.

Figura 3.5 – Exemplo de uma hierarquia de classes

Uma das regras básicas para determinar se uma relação é de composição ou de herança é perguntar se se deve dizer contém ou é um. Por exemplo: um automóvel contém um motor, logo, deve existir uma relação de composição entre automóvel e motor. Não se pode dizer que um automóvel é um motor. Da mesma forma, pode-se dizer que um automóvel é um veículo. Assim, existe uma relação de herança entre estas duas entidades. Não faz sentido dizer que um veículo contém um automóvel.

(8)

Sempre que o programador decidir ter uma classe base e várias classes derivadas, deverá mover o máximo de funcionalidade para a classe base. Por exemplo, se existirem variáveis com a mesma funcionalidade nas classes derivadas, estas deverão ser substituídas por uma variável comum na classe base. O mesmo acontece com métodos semelhantes. É típico existir uma classe base, da qual derivam várias outras classes. Ao conjunto de classes pertencentes à mesma árvore, chama-se hierarquia de classes. A figura 3.5 ilustra parte de uma hierarquia de classes retirada da documentação da plataforma .NET.

3.4 Polimorfismo

Uma outra característica fundamental da programação orientada aos objectos é o polimorfismo. Por polimorfismo, entende-se a capacidade de objectos diferentes se comportarem de forma diferente quando lhes é chamado o mesmo método. Vejamos um caso concreto.

/*

* Programa que ilustra o conceito de polimorfismo. */

using System;

// Classe base, comum a todos os empregados class Empregado

{

private string Nome;

public Empregado(string nomeDaPessoa) {

Nome = nomeDaPessoa; }

public void MostraNome() {

Console.WriteLine("{0}", Nome); }

// Método preparado para ser alterado por classes derivadas public virtual void MostraFuncao()

{

Console.WriteLine("Empregado"); }

}

// Classe patrao, um caso especial de empregado class Patrao : Empregado

{

public Patrao(string nomeDoPatrao) : base(nomeDoPatrao)

{ }

// Nova implementação da funcionalidade "MostraFuncao" public override void MostraFuncao()

{

(9)

} }

// O programa principal class Exemplo3_1

{

static void Main() {

// Uma pequena tabela dos trabalhadores da empresa Empregado[] trabalhadores = new Empregado[]

{

new Empregado("Zé Maria"), new Empregado("António Carlos"), new Patrao("José António") };

// Mostra o nome e a função de todos os trabalhadores for (int i=0; i<trabalhadores.Length; i++)

{ trabalhadores[i].MostraNome(); trabalhadores[i].MostraFuncao(); Console.WriteLine(); } } }

Listagem 3.1 – Programa que ilustra o conceito de polimorfismo (ExemploCap3_1.cs)

No caso do programa da listagem 3.1, temos uma classe base Empregado e uma classe

derivada Patrao. Existe ainda um método chamado MostraFuncao() que, por omissão,

diz que a pessoa é um “Empregado”. No entanto, as classes derivadas devem poder modificar este método para que reflictam a função da pessoa em questão. Assim, enquanto no caso de um empregado simples o método mostra a palavra “Empregado”, no caso do patrão deverá mostrar “Patrão”.

Suponhamos que temos o seguinte código:

Patrao chefe = new Patrao("Manuel Marques"); chefe.MostraNome();

chefe.MostraFuncao(); Empregado emp = chefe; emp.MostraFuncao();

Qual deverá ser o resultado da execução? Obviamente que a linha

chefe.MostraFuncao(); levará a que seja escrito “Patrão”, no entanto, quando se cria

uma referência Empregado emp com o valor de chefe e se chama MostraFuncao() sobre

esta, o que acontecerá?

Em primeiro lugar, a conversão de Patrao em Empregado é possível. É sempre possível

converter (ou utilizar) uma classe base em vez da classe derivada. Isso deve-se ao facto de uma relação de herança ser uma relação é um. A classe Patrao possui todos os

(10)

Agora vem a parte mais interessante: ao escrever emp.MostraFuncao();, o CLR guarda

a verdadeira identidade dos objectos que estão associados a cada referência. Assim, embora estejamos a chamar o método MostraFuncao() através de uma referência Empregado, o sistema sabe que tem de chamar a verdadeira implementação desse método,

para o objecto em causa. Neste caso o resultado da execução seria então:

Manuel Marques Patrão

Patrão

A isto chama-se polimorfismo. O sistema descobre automaticamente a verdadeira classe de cada objecto e chama as implementações reais para os objectos em causa. Em C#, sempre que se quiser tirar partido desta funcionalidade, tem de se declarar o método base como sendo virtual (chama-se a isto métodos virtuais). Sempre que numa classe

derivada se altera a implementação de um destes métodos, como é o caso da classe

Patrao, tem de se marcar esse método com a palavra-chave override.

Voltemos agora ao exemplo da listagem 3.1. Na classe principal do programa é criada uma pequena tabela com os trabalhadores da empresa (trabalhadores), onde se

encontram dois empregados normais e um patrão. Em seguida, é executado o seguinte ciclo:

for (int i=0; i<trabalhadores.Length; i++) {

trabalhadores[i].MostraNome(); trabalhadores[i].MostraFuncao(); Console.WriteLine();

}

Aqui vemos o poder do polimorfismo em acção. Os empregados (quer sejam normais ou patrões) são tratados de forma uniforme, sendo colocados numa tabela. No entanto, ao chamar o método MostraFuncao(), no caso dos empregados “normais”, é mostrado

“Empregado”. No caso dos patrões é mostrado “Patrão”. O resultado da execução do programa é o seguinte: Zé Maria Empregado António Carlos Empregado José António Patrão

O polimorfismo permite que os objectos mantenham a sua identidade apesar de serem tratados usando classes mais genéricas (isto é, classes mais acima na hierarquia de derivação). Isso é extremamente poderoso. Por exemplo, neste exemplo seria trivial estendê-lo por forma a que houvesse um método de cálculo de salário que no caso dos empregados teria uma certa implementação e no caso dos patrões uma outra.

Convém alertar para o facto de que no caso de não se declararem os métodos como virtual

(11)

chamar os métodos da classe que está a ser utilizada como referência. Isto é, no pequeno exemplo de chefe, ao fazer emp.MostraFuncao();, caso MostraFuncao() não fosse um

método virtual ou a nova implementação não fizesse o override da antiga, a chamada

resultaria em “Empregado” em vez de “Patrão”. Isto é uma fonte comum de erros, pelo que o programador deve estar atento a esta situação.

Ao longo dos próximos capítulos, iremos analisar detalhadamente os pormenores associados à programação orientada aos objectos.

Conceitos básicos de OOP

־ A programação orientada aos objectos (OOP) baseia-se em três princípios básicos: encapsulamento, composição/herança e polimorfismo.

־ Encapsulamento refere-se ao facto de as estruturas de dados serem entidades privadas de cada classe, devendo ser apenas acedidas por elementos da própria classe.

־ Composição e herança são os tipos de relações que podem existir entre objectos (e classes). Composição corresponde a relações do tipo contém, herança corresponde a relações do tipo é um.

־ Polimorfismo refere-se à capacidade de diferentes objectos se comportarem de forma diferente quando o mesmo método é invocado neles. Isto apesar de ser utilizada uma referência para uma classe base para fazer a chamada.

־ Uma classe representa uma família de objectos, enquanto um objecto representa uma instância dessa família (ex: Empregadoé uma classe enquanto a entidade que representa “José António” é um objecto da classe Empregado).

־ Diferentes instâncias de uma classe têm variáveis diferentes. As instâncias são criadas com o operador new.

־ Ao criar um novo objecto, o construtor da classe é chamado. O construtor tem o mesmo nome que a classe e não tem valor de retorno.

־ Se um elemento de uma classe é declarado private, então é visível apenas para elementos dessa classe.

־ Se um elemento de uma classe é declarado public, então é visível para o exterior da classe.

־ Os dados da classe devem, regra geral, ser privados. Apenas o menor número possível de métodos da classe deve ser público.

־ Uma relação de composição surge quando uma classe tem de conter um objecto de uma outra classe (exemplo: “automóvel contém motor”).

־ Uma relação de herança surge quando um objecto também é uma instância de uma outra classe mais geral (exemplo: “automóvel é um veículo”).

־ Uma relação de herança indica-se por:

class ClasseDerivada : ClasseBase { ... }

־ É sempre possível utilizar uma referência de uma classe mais acima na hierarquia de derivação para um objecto de uma classe derivada dessa.

־ Para existir polimorfismo, os métodos na classe base têm de ser declarados virtual e nas classes derivadas têm de ser declarados override.

Referências

Documentos relacionados

Há somente um modo para se obter vitória sobre estes três tipos de morte (física, espiritual e eterna), a saber, morrendo para a sentença de condenação e maldição da

Inútil porque não o transformo em som, porque as palavras – estas palavras – são poucas e desajeitadas para dizê-lo.. E porque penso que, se calhar, nem sequer vais ler

Uma vez que o modal de transporte urbano esta baseado no transporte coletivo e considerando o alto preço do transporte coletivo para essa faixa de renda a quem se destinam esses

A &#34;Carta de Porto Alegre&#34;, fruto do IV Congresso Internacional de Turismo da Rede Mercocidades, vem no final do livro como forma de ilustrar o que

Ainda segundo Gil (2002), como a revisão bibliográfica esclarece os pressupostos teóricos que dão fundamentação à pesquisa e às contribuições oferecidas por

Todos os delinqüentes juvenis são indivíduos desajustados e alguns delín - qüentes juvenis são produtos de lares desfeitos; logo, alguns indivíduos desa - justados são produtos

Quando o agente de Mark Osborne lhe propõe criar um filme a partir do livro O Principezinho, a reação do realizador não se fez esperar: «É impossível de adaptar para o cinema,

O presente artigo tem como objetivos relatar e discutir a experiência docente do exercício da mentoria para uma turma de graduação em Medicina durante o período de seis anos (de