Herança Simples e M´ultipla: Conceitos e Aplicaç ˜oes em Java e C++

Propaganda
Herança Simples e Múltipla: Conceitos e Aplicações em Java e C++
Carlos Akcelrud Cony
Programa Interdisciplinar de Pós-Graduação em Computação Aplicada
Unisinos
[email protected]
Abstract
Atualmente os conceitos que estão envolvidos na
Orientação Objetos tem sido tema de diversos estudos e
análises. Uma das principais caracterı́sticas da OO é o
suporte ao conceito de Herança.
A herança tem apresentado diversos benefı́cios para o
desenvolvimento de softwares mais concisos, robustos e
confiáveis mas isto tem seu custo.
Já no que diz respeito as linguagens de programação que
dão suporte a tal conceito, existem algumas divergências
quanto o seu uso, estes temas serão abordados com mais
ênfase neste artigo.
1
Introdução
Neste artigo discutimos uma das tecnologias mais importantes que a orientação objetos suporta: Herança, que
além de ser uma forma satisfatória de aperfeiçoar e organizar o código fonte, é uma técnica eficaz na redução da
complexidade dos softwares. Além disso, será apresentado
um comparativo entre Herança Simples e Herança Múltipla.
Herança é uma técnica útil para ampliar a abstração de
dados, apesar desta ser uma técnica efetiva para se definir
um conceito único e claro, ela pode não ter o suporte devido
para a solução completa do problema.
Para [1], herança ”É o recebimento, por um componente
de programa, de propriedades ou caracterı́sticas de outro
componente de programa, de acordo com uma relação especificada entre os dois componentes”.
2
Herança
Herança é utilizada para se representar a similaridade entre diferentes classes, com isso se simplifica a definição de
Classes semelhantes a outras previamente definidas. Com
isso é possı́vel que membros comuns, métodos e atributos sejam definidos somente uma vez (encapsulamento), e
ainda especializar estes membros em determinados casos.
Além do encapsulamento de dados, herança traz outros benefı́cios, tais como: permite à reutilização de software em
que novas classes são criadas a partir de classes pré-existentes, absorvendo seus atributos e comportamentos adicionando novos recursos as classes antigas.
A reutilização de software apresenta algumas vantagens,
por exemplo, economiza e facilita no desenvolvimento de
novos programas, incentiva a reutilização de códigos de
alta qualidade já testados e depurados, reduzindo possı́veis
problemas depois da instalação software final.
A herança define uma relação entre classes do tipo ”é um”, onde uma classe compartilha a estrutura e o comportamento definidos em uma ou mais classes. A partir do reconhecimento da similaridade entre as classes definem uma
hierarquia de classes, subdividida em:
• Superclasses (classes bases): Representam abstrações
mais gerais e se subdividem em duas:
– Direta: A subclasse herda explicitamente da superclasse.
– Indireta: É herdada de dois ou mais nı́veis acima
na hierarquia de classes.
• Subclasses (classes derivadas) Representam abstrações
em que os atributos e os serviços especı́ficos são adicionados, modificados e/ou removidos.
Para interligar as classes usa-se estruturas como: generalização e especialização, fazendo com que os atributos e
serviços comuns fiquem claros em uma hierarquia de Classes.
Então quando se dispõe de mecanismos de herança, a
especificação de uma classe pode ser retirada de uma biblioteca existente e uma nova classe (Subclasse) pode ser criada a partir de uma classe pai (superclasse), através do processo de derivação. A classe filha pode especificar as suas
caracterı́sticas que diferem da classe pai.
Um problema apresentado pela herança é que uma subclasse pode herdar métodos (na maioria das vezes) que não
necessita ou que não deveria os possuir, então o projetista
deve assegurar que os recursos fornecidos por uma classe
são apropriados para futuras subclasses.
Contudo, então, a melhor idéia seria que um programador possuı́sse um conjunto de bibliotecas de classes (por
exemplo, Java API) com código já implementado, à sua
disposição, e então definiria quais destas seriam úteis para
a confecção do novo software, tornando a implementação
mais simples.
Um exemplo de herança pode ser observado como na
figura 1 onde se tem que um:
• Estudante é uma Pessoa
• Professor é uma Pessoa.
• Um objeto da classe Estudante, tem os atributos e
serviços de Estudante, e também os atributos herdados
da classe Pessoa.
Por exemplo, Carlos é um estudante e tem os atributos
e serviços de Pessoa, como: Nome, Endereço, Dirigir, Viajar. Além destes possui atributos e serviços que expressam
o comportamento especı́fico de estudante, como: Curso,
Nota, Estudar e Realizar Prova.
A classe Pessoa é reconhecida como uma generalização
e Estudante como uma classe especialização, a qual herda
os serviços e atributos de Pessoa. Além disso, uma classe
derivada (uma especialização) pode incluir novos serviços
e atributos ou redefinir os serviços herdados.
2.1
Este é o tipo mais trivial de construção de subclasses, e
tem como objetivo central a especialização da nova classe
tornando-a mais especı́fica que a classe pai, satisfazendo em
todos os aspectos relevantes as especificações do pai. Este
tipo de herança é a mais comum e mais utilizada. A seguir
um exemplo deste tipo de herança: Uma classe ”janela”,
dispõe de operações gerais de janela como mover, redimensionar,... Uma subclasse especializada ”janela de texto”
herda as operações de ”janela” e em adição fornece ainda
outras facilidades que permitem, por exemplo, fazer o display de texto, editar texto,..., mas mantêm as propriedades
de uma janela em geral. [7]
2.2
Dessa forma, Funcionário pode incluir atributos como
Salário e Profissão e serviços como CalcularSalário, que
podem não ser relevantes ou apropriados para Pessoa em
geral.
Subclasse por Especificação
Este método também é utilizado de maneira freqüente se
faz quando as classes pai e derivadas mantêm um interface
comum, isto é apresentam os mesmos métodos, ou seja, a
classe pai é uma combinação de operações implementadas
e operações que só serão implementadas nas classes filhos.
Muitas vezes não há alteração da interface entre a classe
pai e a classe filho, o filho que implementará o comportamento descrito, já que este não implementado no pai.
Nestes casos as subclasses não são refinamentos dos tipos
que existem, mas antes realizações de uma especificação
abstrata incompleta.
Subclasses por especificação podem então ser reconhecidas quando, a superclasse não implementa o comportamento atual, mas descreve-o simplesmente e é implementado nas subclasses.
2.3
Figure 1. Classe Pessoa e Seus Herdeiros
Subclasse por Especialização
Subclasse por Generalização
A construção de subclasses por generalização é de certa
forma o contrário de herança por subclasses por especialização.
Neste tipo, uma subclasse estende o comportamento da
classe pai para criar um objeto mais geral, é muitas vezes
aplicável quando se quer construir a partir de uma base existente que não se quer ou não se pode alterar, por exemplo,
um sistema de display de gráficos em que a classe ”janela”
foi definida para fazer o display com fundo branco e preto.
Quando se cria um subtipo, ”classe colorida”, que permita
que o fundo seja de outra cor que não branco e preto, juntase um campo adicional para guardar a cor e sobre-se ao
código da ”‘janela”’ herdada em que o fundo é desenhado
nessa cor.
Subclasses por generalização ocorrem mais freqüencia
quando o projeto é baseado em dados e coloca para 2o lugar
questões de comportamento.
2.4
Subclasses por extensão
Enquanto as subclasses por generalização modificam ou
expandem as funcionalidades de um objeto, subclasses por
extensão juntam funcionalidades completamente novas.
A diferença entre esta forma de herança da anterior é
o fato que esta sobrepõe algum método da classe pai enquanto em subclasses por extensão, são agregados novos
métodos aos do pai, já as funcionalidades são menos ligadas aos métodos do pai, por exemplo, pretende-se manter os objetos de certa classe numa lista, para isto, pode-se,
então, construir uma subclasse em que se unem os campos
necessários para a construção de uma lista encadeada. [7]
Com isso a funcionalidade do pai permanece intocável.
2.5
Subclasses por Limitação
Quando a classe filha é menor, ou contém mais
restrições que a classe pai, então herança de subclasse
por limitação ocorre. Semelhantemente a subclasse por
generalização, a subclasse por limitação aparece no momento da programação da nova classe a partir de classes
pré-existentes e percebe-se que não se quer alterar ou não
se pode, por exemplo, uma classe que traduzia uma fila de
espera dupla em que os elementos podiam ser juntos ou retirados de cada um dos lados da estrutura. O programador
quer representar uma classe pilha, reforçando a propriedade
de que os elementos só podem ser adicionados ou retirados
de um dos lados da pilha. [7]
Como resultado, surge uma subclasse que modificou ou
sobrepôs alguns métodos e eliminou outros que não interessavam mais.
2.6
Subclasses por Combinação
Método semelhante ao utilizado pela herança múltipla,
ou seja, uma combinação entre duas ou mais classes, um
professor Assistente tem caracterı́sticas representativas das
duas seguintes classes: Professor e Aluno. [7]
3
3.1
O nome da classe pai pode ser precedido pelas palavras
chave private ou public. Se as palavras chaves não existirem, a classe derivada será por padrão, privadamente derivada. (figura 2) Se a classe filho for declarar de maneira
publica, então o que é público aos que usam a classe base,
será público para os utilizadores da classe filha, a não ser
que um método seja sobrecarregado na filha. Já se o filho é
uma classe privada, então nada do pai pode ser público para
os usam a classe filha.
Herança Simples
Herança Simples em C++
A declaração em C++ de uma classe derivada pode
fazer-se do seguinte modo:
class¡nome da classe derivada¿:¡nome da classe pai¿
class¡nome da classe derivada¿:private¡nome da classe pai¿
class¡nome da classe derivada¿:public¡nome da classe pai¿
Figure 2.
A classe derivada não pode ascender aos dados privados
da classe pai, para que a proteção da classe não seja quebrada só pelo processo de derivação. Pode, no entanto, ser
especificamente autorizada para isso.
Assim, existem dois processos:
• Construção de funções friend (ou a classe inteira ou
alguns métodos).
• Membros declarados na parte protegida que são acedidos pelas classes derivadas.
A partir disto é criado uma maneira diferente para o acesso, protected. Um método ou atributo protegido comportase como um membro público para a classe filha, mas é considerado privado para o resto do programa. (Figura 3) Desta
maneira as funções membro do filho podem ascender, por
exemplo, a uma variável declarada na zona protegida da
classe pai, mas os utilizadores da classe pai ou da classe
filho não podem o fazer.
Uma melhor compreensão considere o exemplo a seguir:
uma classe base designada pelo pai, deriva-se duas outras subclasses, filho1, privadamente e filho2, publicamente.
Então, sempre que uma classe derivada redefine uma função
membro pública da classe base, se pretende fazer atuar sobre um objeto da classe derivada, o método da classe pai
3.2
Herança Simples em Java
Herança Simples em Java se vale dos mesmos conceitos
que na linguagem C++, então será mostrado um exemplo
para implementar um applet, que utiliza-se a seguinte notação:
public class HelloApplet extends JApplet { ...... }
A palavra chave extends indica que HelloApplet é uma
classe filha de JApplet. Isso significa que a classe HelloApplet herda por exemplo, os métodos init() e start() de
JApplet. Em um applet, pode-se definir novos atributos e
métodos, bem como sobrescrever os métodos herdados de
JApplet, como o init().
Um exemplo de código fonte de herança simples em
Java:
Figure 3.
que foi redefinido, tem-se que usar o operador de âmbito de
resolução.
public class Student extends Person {
private String grade;
private int classes;
...
public Student(String n, String g) {
No exemplo, abaixo, mostra-se como é feito o código de
herança simples em C++, onde a classe ”planos” herda os
métodos públicos de Vetores.
class Vetores {
private:
struct vetor v,p;
public:
super(n);
grade = g;
classes = 0;
}
public String toString() {
return super.toString() + ”: ” + grade;
}
}
void cria(float,float,float,float,float,float);
void cria(vetor,vetor);
void vetores (float, float, float);
float escalar (vetor,vetor);
vetor vetorial (vetor,vetor);
float projecao(vetor,vetor);
float modulo(vetor);
vetor produto (float,vetor);
vetor subtracao (vetor,vetor);
};
A classe acima ilustra a criação de uma subclasse Student que herda os atributos e métodos de Person. Uma subclasse não herda os construtores de sua superclasse, mas
pode invocá-los através do comando super [8]. A chamada
a essa função deve ser a primeira instrução no construtor
da subclasse, senão ocorrerá erro. Caso a chamada ao construtor base não tivesse sido feita explicitamente em Student(String n, String g), uma chamada ao construtor default
(sem argumentos) da classe base seria realizada.
class Plano: public Vetores {
private:
4
vetor normal;
void set pontos(float, float,float);
void set normal(vetor, vetor);
public:
void cria (vetor, vetor, vetor);
void cria (float,float,float,float,float);
vetor get normal();
};
Herança Múltipla
É a capacidade de uma classe herdar de duas ou mais
classes, por exemplo à classe rádio-relógio herdar da classe
rádio e da classe relógio. [9]
Vantagens:
• Reuso de classes
• Simula o pensamento humano, por exemplo, um estagiário é um estudante e um funcionário ao mesmo
tempo
Desvantagens:
• Perda da simplicidade conceitual
• Dificuldade em implementar
• Definição da regra de ativação de métodos herdados
• Ambigüidade ocorre quando as superclasses possuem
homônimos e a subclasse não redefine esses membros
em suas declarações, causando problemas para o compilador. (Figura 4)
Concluı́-se que, como regra geral, deve-se evitar o seu
uso, exceto para inclusão de diferentes interfaces independentes.
4.1
Herança Múltipla em C++
Herança Múltipla permite a inserção de conceitos diferentes, tais como, uma classe pode ser combinada com mais
de uma superclasse para formar uma nova classebase, por
exemplo a Figura 5. [2]
construtor da classe derivada aparecerá: [9]
D :: D (int x,int y, int z, int w, int t) : B1 (z) , B2 (w,t)
Caso uma classe filha declare um método igual a um
pré-existente na classe pai, então a função da classe filha
esconde a função da classe pai, mesmo que tenham argumentos de tipos diferentes (overriding).
Um exemplo retirado de [6] de código fonte de herança
múltipla em C++:
class B1 {int atr b1;};
class B2 {int atr b2;};
class D: public B1, public B2
4.1.1
Herança Múltipla Virtual em C++
O problema da duplicidade é quando existem dois slots de
memória que estão referenciando a mesma variável, ou dois
caminhos diferentes para a chamada de uma função, e com
herança múltipla esse tipo de problema pode surgir [5], para
resolver isto o C++ dá suporte a um recurso que se chama
superclasse virtual. (Figura 6)
Figure 4. Ambigüidade em Herança
O C++ suporta herança múltipla, permitindo que uma
classe seja derivada de várias classes base. [3]
Para tal, a definição da classe derivada, terá a seguinte
configuração:
class ¡nome da classe ¿ : [public] ¡nome da classe¿,...,
[public] ¡nome da classe ¿
Ao utilizar construtores, quando se faz a declaração do
construtor da classe derivada, listam-se os construtores das
classes base com os valores dos argumentos, por exemplo:
A classe derivada D terá como classes base a classe B1,
cujo construtor tem um argumento e B2 cujo construtor
tem, por exemplo, dois argumentos. O construtor da
classe D tem dois argumentos. Assim quando se define o
Figure 5. Conceito de Herança Múltipla
A declaração virtual soluciona o problema da ambigüidade da duplicidade de atributos, apenas eliminando
a criação de dois endereços de memória para atributos na
superclasse.
Um exemplo, retirado de [6], de código fonte de
Herança Múltipla Virtual: class B0 {int atrb 0; };
class B1: virtual public B0 {int atrb 1; };
class B2: virtual public B0 {int atrb 2; };
class D: virtual public B1, virtual public B2 {int atr dl};
Figure 6. Herança Múltipla Virtual
4.2
Herança Múltipla em JAVA
Da mesma maneira que C++, Java suporta herança de
objetos, mas não suporta herança múltipla. Em seu lugar Java admite uma nova construção chamada ”interface”.
As interfaces especificam o comportamento de um objeto sem definir a sua implementação. [4] Java suporta a
herança múltipla de interfaces com isso ganha-se muitos
dos benefı́cios da herança múltipla de classes sem seus inconvenientes.
Interface é uma coleção de definições de métodos (sem
implementação) e constantes. Na declaração de uma classe
pode constar que ela implementa uma ou mais interfaces.
Algumas caracterı́sticas das interfaces são:
• Não é possı́vel herdar variáveis;
• Não é possı́vel herdar implementação de métodos;
• A hierarquia de Interfaces é independente da hierarquia de Classes. Duas classes que implementam a
mesma Interface não são necessariamente relacionadas
na hierarquia de Classes.
Vejamos uma utilidade das interfaces. Suponhamos que
queremos fazer referência a um certo conjunto de classes
que possuem o método func(), mesmo que não tenham uma
superclasse comum a não ser a classe base Object. Para isso,
primeiro declaramos uma interface que especifica o método
func(). Em seguida criamos subclasses dessas classes, que
implementam essa interface.
Agora, para fazer referência às classes, basta fazer
referência à interface comum Uma interface é utilizada
quando não existe a necessidade das classes derivadas herdarem métodos já implementados, e são um conjunto de
métodos e constantes (não contém atributos).
Então, Java por motivos de simplicidade, abandona a
idéia de herança múltipla, cedendo lugar ao uso de interfaces. Os métodos definidos na interface são ”ocos” ou desprovidos de implementação.
Classes podem dizer que implementam uma interface,
estabelecendo um compromisso, uma espécie de contrato,
com seus clientes no que se refere a prover uma implementação para cada método da referida interface. Ao cliente,
pode ser dada a definição da interface, porém, ele acaba não
sabendo o que a classe é, mas sabe o que faz.
Um exemplo de herança ”múltipla” em Java:
public class Analyse extends
Runnable{
private Words[] arrayWord;
private POS[] arrayPOS;
private Chunks[] arrayChunks;
...
}
5
Thread
implements
Java versus C++
Neste tópico serão mostradas algumas das principais
diferenças entres as linguagens de programação Java e C++,
para isso, observe a tabela 1:
6
Conclusões
• Reusabilidade de código. Quando comportamento é
herdado de uma outra classe, o código que o fornece
não precisa ser reescrito. Isto é importante, já que
os programadores perdem seu do tempo reescrevendo
código. Por exemplo, para encontrar um padrão em
uma lista ou inserir um novo elemento em uma tabela.
Com técnicas de OO, estas funções podem ser escritas
apenas uma vez e reutilizadas.
• Compartilhamento de código. Ocorre em muitos nı́veis com técnicas OO. Uma forma se dá quando diversos programadores ou softwares podem usar as mesmas classes. Uma outra ocorre quando duas ou mais
classes desenvolvidas por um programador como parte
de um projeto herda de uma única classe pai. Quando
isto acontece, dois ou mais tipos de objetos compartilharão o código que eles herdaram.
• Consistência de interface. Quando duas ou mais
classes herdam da mesma classe pai, podemos
estar certos de que o comportamento que elas
herdaram será o mesmo em todas as classes.
Logo, é garantido que as interfaces para objetos semelhantes sejam, de fato, semelhantes.
C ++
Definição da classe
e implementação em
ficheiros separados.
Usa funções e métodos.
Caso especial: função
main().
Construção de objetos
em modo dinâmico e não
dinâmico.
Especificador de acessibilidade: Os atributos
com omissão de especificador são privados.
Se uma classe não
declara explicitamente
o construtor por defeito
então o compilador
criará automaticamente
um, exceto se já existir
mais algum construtor.
Necessário implementar construtor cópia,
destrutor e fazer a sobrecarga do operador
de atribuição quando
a classe é composta
por atributos do tipo
apontador.
Arrays de objetos: Na
criação de um array de
objetos, é invocado automaticamente o construtor sem parâmetros para
inicializar cada um dos
objetos do array.
Passagem
de
parâmetros: por valor,
por apontador e por
variável referência.
Java
Definição da classe
e implementação no
mesmo ficheiro. (Estrutura de classe.)
Só métodos. O método
main() terá de estar incluı́do numa classe.
Construção de objetos sempre em modo
dinâmico.
Os
atributos
com
omissão de especificador são acessı́veis às
classes dentro do mesmo
package.
Idêntico ao C++
Não é preciso implementar qualquer um dos
métodos referidos. No
entanto, o uso do método
clone() em Java deve ser
usado sempre que se pretende cópia de um objeto.
Um array de objetos é
criado com a instrução
new e cada um dos
seus objetos necessita
também de ser criado de
modo explı́cito (new).
Por cópia da referência
do objeto (equivalente
à passagem por valor
de um apontador em
C++/C).
Tabela 1
• Componentes de software. Herança permite que programadores construam componentes de software reutilizáveis com o objetivo de desenvolver aplicações novas que requeiram pouca ou nenhuma codificação.
• Prototipação rápida. Quando um sistema de software
é construı́do com base em componentes reutilizáveis,
o tempo de desenvolvimento pode ser concentrado em
entender a parte nova do sistema. Logo, sistemas de
software podem ser gerados mais facilmente e mais
rapidamente.
• Ocultamento de informação. Um programador que reutiliza um componente de software necessita apenas
entender a natureza do componente e sua interface.
Logo, a interconexão entre sistemas de software é reduzida e a complexidade também é reduzida.
Embora os benefı́cios de herança em programação OO
sejam grandes, este mecanismo, não possui custo zero. Por
isso, devemos considerar o custo de técnicas OO e, em particular, o custo de herança:
• Velocidade de execução. Dificilmente, ferramentas
de software de propósito geral são tão rápidas quanto
aquelas desenvolvidas cuidadosamente para um propósito especı́fico. Logo, métodos herdados, que devem lidar com subclasses arbitrarias são frequentemente mais lentos do que código especializado.
• Tamanho do programa. O uso de qualquer biblioteca
de software em geral acarreta o aumento de tamanho
do programa, o que não acontece com sistemas construı́dos através de um projeto especı́fico.
• Overhead de envio de mensagens. Muito do que tem
sido feito do fato que envio de mensagens é por natureza uma operação mais custosa do que chamada de
procedimento.
• Complexidade do programa. Embora a programação
OO seja tida como uma solução para a complexidade
do software, o uso demasiado de herança pode simplesmente substituir uma forma de complexidade por
outra. Entender o fluxo de controle de um programa
que utiliza herança pode requerer várias varreduras no
grafo de herança.
Em relação a herança nas linguagens de programação
Java e C++, a discussão deve ser feita de maneira cautelosa.
Enquanto o Java tem por caracterı́sticas a simplicidade, robustez, segurança e portabilidade, o C++ apresenta conceitos mais complexos, a mais tempo no mercado e, ainda,
na maioria dos casos é mais rápida por não possuir uma
máquina virtual.
Portanto, o uso da linguagem de programação fica a
cargo do programador, e na análise de seu problema.
References
[1] PRATT, T. W.; ZELKOWITZ, M.V., 2001
[2] DEITEL, H.M.; DEITEL, P.J. C++: Como Programar.
Bookman.
[3] GRAHAM, Neill. Learning C++. McGraw-Hill, 1991
[4] DEITEL, H.M.; DEITEL, P.J. Java: Como Programar.
[5] MONTENEGRO, Fernando; PACHECO, Roberto.
Orientação Objetos em C++. Editora Ciência Moderna,
1994.
[6] BUENO, André. Apostila de Programação em C++,
2002.
[7] LEITÃO, Helena. http://www.dei.isep.ipp.pt/ hleitao/
[8] BITENCOURT, Tatyana. Grupo de Usuários Java http://www.guj.com.br
[9] CESTA,
André.
TUTORIAL:
”A
LINGUAGEM DE PROGRAMAÇÃO JAVA”, 1996,
http://www.dcc.unicamp.br/ aacesta
Download