[Pesquisar este blog]

quinta-feira, 15 de março de 2018

POO::Plena-20-Herança simples

POO-P-19-Especificadores e Modificadores POO-P-21-Sobreposição
Os objetos e entidades reais possuem relações entre si que, quando reconhecidas, facilitam o entendimento do conjunto de objetos envolvidos e, ao mesmo tempo, de suas partes. É bastante comum classificar as coisas, criando hierarquias de elementos, onde os grupos superiores contém características comuns, depois separadas em subgrupos, nos quais se incluem características mais específicas, possibilitando definir muitos níveis e grupos. A herança é o mecanismo da orientação a objetos que permite estabelecer este tipo de relação, possibilitando o compartilhamento de características comuns, criando famílias de classes.

A Biologia, por exemplo, organiza os seres vivos numa árvore, agrupando suas características comuns, separando-os em filos, ordens, gêneros e espécies a medida em que são diferenciados. Enquanto os mamíferos contém características comuns encontradas em todos os animais deste tipo, a ordem dos canídeos acrescenta características encontradas apenas nos cães e lobos; assim como a ordem dos felinos inclui características presentes em gatos e tigres. Nesta situação, podemos dizer que os canídeos e os felinos herdam características dos mamíferos. Da mesma forma, uma espécie particular de cão, herda as características dos canídeos, que por sua vez incluem as dos mamíferos. Assim, vemos que as características dos grupos superiores são propagadas, por meio da herança, para os grupos descendentes.

Herança

A herança (inheritance) é um dos mecanismos mais importantes da orientação a objeto s [1][2][4][5][6][7]. De fato, é uma técnica que possibilita a uma classe utilizar atributos e operações definidas em outra classe. Neste sentido a herança representa o compartilhamento de atributos e de operações entre classes, sendo uma característica que só existe na orientação a objetos.

Ao realizar tal compartilhamento, cria-se uma relação hierárquica, do tipo pai e filha, ou seja, a classe pai, tomada como ponto de partida, contém definições que, a critério do programador, poderão ser utilizadas nas classes definidas como filhas. Sob certos aspectos, é como se as características comuns de diversas classes fossem fatoradas em um tipo tomado como classe pai.

É comum que a classe pai seja chamada de classe base (base class) ou superclasse (superclass), enquanto a classe filha é denominada de classe derivada (derived class) ou subclasse (subclass). Nas linguagens de programação Java e C# é mais comum o uso dos termos superclasse e subclasse.

A figura que segue ilustra, por meio de um diagrama de classes UML, a relação existente entre uma superclasse e uma subclasse. Nele se observam as duas classes relacionadas, representadas por retângulos, unidas por uma reta finalizada por um triângulo não preenchido, que aponta para a classe de maior nível hierárquico, ou seja, a classe base ou superclasse. Uma superclasse pode dar origem a várias subclasses, sendo desnecessário dizer que tais subclasses pode ser empregada como novas superclasses, permitindo criar hierarquias com múltiplos níveis.

Além de compartilhar elementos existentes na superclasse, as subclasses podem adicionar novos atributos e operações próprios, ou seja, cada subclasse se torna particular e mais especial do que a superclasse. Assim as subclasses podem ser entendidas como extensões ou especializações de suas superclasses. Como também é possível substituir atributos e operações existentes, as características herdadas podem ser restritas ou modificadas, aumentando as possibilidades da herança. Quando características de uma classe são eliminadas, diz-se que ocorreu uma contração, pois subclasse torna-se mais simples que a superclasse.

No sentido mais amplo e também em sua forma de uso mais comum, a herança é um mecanismo para expressar similaridade entre classes, permitindo representar elementos comuns explicitamente em uma hierarquia de classes [2][4], possibilitando a adição de novos membros na criação de novos tipos derivados e especializados.

A existência de uma relação de herança entre dois tipos pode ser identificada quando uma das perguntas 'é um(a)?' ou 'é do tipo?' é respondida afirmativamente [4][6][7]. Ao considerar as classes Funcionario, Caixa e Cliente, é fácil perceber que Caixa é um (tipo de) Funcionario, mas não o contrário. Assim, Funcionario pode constituir a superclasse de uma hierarquia onde Caixa é uma de suas subclasses. Toda vez que a pergunta 'é um(a)?' determinar o relacionamento entre conceitos, deve ser considerado o emprego da herança na modelagem das classes envolvidas.

Ao mesmo tempo, Cliente não é um (tipo de) Caixa ou Funcionario, portanto constitui uma classe que não faz parte da hierarquia originada em Funcionario, apesar de constituir um dos tipos presentes no cenário considerado.

Quando uma aplicação é construída por meio de um conjunto de classes organizadas hierarquicamente, a ampliação de suas funcionalidades pode, muitas vezes, ser facilitada com a criação de novas classes baseadas nas classes originais. Assim, a herança constitui um mecanismo onde novos tipos de objetos podem ser rapidamente definidos em termos de outros existentes [12]. Outros tipos de funcionário, tais como Motorista, Segurança ou Gerente, poderiam ser facilmente adicionados atendendo os novos requisitos da aplicação. A figura que segue ilustra esta hierarquia possível.

Também deve ser destacado que este mecanismo permite que o conceito de reusabilidade seja verdadeiramente empregado, pois classes tomadas como base podem ser reutilizadas na definição de outras sem que seu código tenha que ser reproduzido ou modificado [6][7]. O emprego da herança evita a cópia direta de código, o que reduz substancialmente a propagação de erros quando os trechos de programação copiados se mostram inapropriados ou incorretos.

Existem duas formas de herança: a herança simples, onde uma única classe é tomada como base na criação de uma subclasse; e a herança múltipla, na qual duas ou mais classes são, simultaneamente, tomadas como base para definição de novas classes derivadas.

A herança simples é tratada neste post, enquanto a herança múltipla será discutida num próximo.

Herança simples

A herança simples é aquela onde a subclasse (classe filha ou classe derivada) é criada a partir de uma única superclasse (classe pai ou classe base), a qual compartilhará seus atributos públicos e protegidos. Esta forma de herança também pode ser conhecida como extensão simples.

A linguagem de programação Java oferece apenas a herança simples, cuja sintaxe requerida para expressar a criação deste tipo de relação é:

public NomeSubclasse extends NomeSuperclasse {
  // corpo da subclasse
}

A palavra reservada extends indica no nome da classe tomada como superclasse na declaração de uma nova subclasse.

Já C# oferece tanto a herança simples, como a múltipla. A sintaxe envolvida na criação de uma nova classe através do mecanismo da herança simples em C# é :

public class <NomeClasseDerivada> : <NomeSuperclasse> {
  // corpo da subclasse
}

Aqui o símbolo de pontuação ':' permite determinar o nome da classe tomada como superclasse na declaração de uma nova subclasse.

Tanto em Java como em C#, ao usar a sintaxe indicada acima, a subclasse passa a compartilhar todos os membros públicos e protegidos presentes na superclasse. Assim, os atributos e métodos presentes na superclasse podem ser livremente utilizados na programação da subclasse e, também das subclasses desta. Mas as instâncias da superclasse e da subclasse tem acesso apenas aos elementos públicos da superclasse.

Isto significa que um membro declarado público em uma classe sempre será acessível para todas as instâncias desta classe, para todas as subclasses e todas as instâncias de sua subclasses.

Os membros protegidos de uma classe poderão ser sempre utilizados na programação de subclasse e das subclasses das subclasses, mas nunca serão acessíveis para os objetos da classe, nem os objetos de quaisquer subclasses.

Finalmente, os elementos privados presentes na superclasse são completamente inacessíveis, o que evidencia a utilidade do níveis de acesso existentes.

Apesar das subclasses herdarem todos os membros públicos e protegidos de suas superclasses, os construtores, mesmo que públicos, não são herdados pelas subclasses, exigindo que novos construtores sejam supridos quando necessário. Sempre é importante destacar que, naturalmente, cada superclasse pode dar origem a um número qualquer de subclasses e, estas a outras subclasses.

A classe Java que segue, denominada Superclasse, declara três atributos a, b e positive; respectivamente dos tipos int, double e boolean; com acesso public, protected e private.

O atributo público a não possui métodos de acesso, pois não existem restrições para seu uso. Já o atributo protegido b, embora existam restrições para seus valores, quando tem conteúdo positivo, tal informação deve ser indicada pelo atributo privado positive como true, enquanto se b é negativo, positive deve ser false. Para garantir que estes dois atributos estejam corretamente relacionados, b é protegido e seu método de mutação setB(double) garante o valor adequado de positive para qualquer valor de b. O atributo positive, portanto, não pode sofrer ajustes diretos, possuindo apenas um método de acesso apropriado para seu tipo boolean, que é isPositive().

O único construtor existente garante que a inicialização do atributo b seja corretamente refletida pelo atributo positive. O método toString() facilita consultar o estado dos três atributos da classe.

public class Superclasse {
   public int a;
   protected double b;
   private boolean positive;

   public Superclasse() {
      setB(0.0);
   }

   public double getB() { return b; }
   public boolean isPositive() { return positive; }

   public void setB(double b) {
      if (b<0) positive = false;
      else positive = true;
      this.b = b;
   }

   public String toString() {
      return String.format("a = %d\nb = %f\npositive = %s\n",
         a, b, positive);
   }
}

O quadro que segue ilustra se os membros de Superclasse são acessíveis em seu código, por suas instâncias, no código de suas subclasses e por instâncias de suas subclasses. Os membros originais são aqueles declarados na própria classe.


O fragmento Java que segue mostra um possível uso da classe Superclasse.

Superclasse obj = new Superclasse();

System.out.println(obj);
obj.a = 13;
obj.setB(-1.5);
System.out.println(obj);

Sua execução produz a seguinte saída:

obj ==> a = 0
b = 0,000000
positive = true
obj ==> a = 13
b = -1,500000
positive = false

Uma classe Java derivada de Superclasse, denominada Subclasse, poderia ser como segue.

public class Subclasse extends Superclasse {
   public long l;
   protected double d;
   private double rate;

   public Subclasse() {
      super();
      // emprego explícito do construtor da superclasse
   }

   public double getD() { return d; }
   public double getRate() { return rate; }

   public void setD(double d) {
      if (d<0) {
         throw new RuntimeException("Valor inválido: " + d);
      }
      this.d = d;
      rate = b / d;
   }
}

Na subclasse denominada Subclasse, é possível ver que três novos atributos foram acrescentados. O atributo de tipo long l é público, pode ser usado irrestritamente e, por isso, não possui métodos de acesso ou mutação.

O atributo protegido de tipo long chamado d só aceita valores positivos, como garante o seu método de mutação associado setD(double). Além disso, valores válidos de d acarretam no armazenamento da razão b/d, onde b é um campo protegido da superclasse, tal como d é nesta classe. O valor da razão é obtido pelo método de acesso getRate().

O quadro que segue ilustra se os membros de Superclasse são acessíveis em seu código, por suas instâncias, no código de suas subclasses e por instâncias de suas subclasses. Os membros originais são aqueles declarados na própria classe, enquanto os membros herdados são aqueles oriundos de seus ancestrais (suas superclasses).


Também é importante notar que a classe Subclasse possui um construtor sem parâmetros, tal qual o default, que foi adicionado aqui apenas para mostrar o uso explícito de super.


super

A palavra reservada super indica a superclasse da classe onde onde é usado.

No caso de construtores, super só pode ser usado na primeira linha de código do construtor, explicitando o acionamento de um construtor (parametrizado ou não) da superclasse, que deve existir. Caso seja acionado o construtor default (sem parâmetros) da superclasse, a chamada a super() pode ser omitida.

Aqui é necessário destacar que quando a superclasse possui apenas construtores parametrizados, torna-se necessário

Em resumo, membros privados não são compartilhados, membros protegidos são acessíveis apenas no código de subclasses e membros públicos são acessíveis pelo código de subclasses e também por instâncias da classe e sua subclasses. Assim, fica a critério do projetista determinar quais os especificadores de acesso serão empregados em cada um dos membros das classes e dos membros adicionais das subclasses.

Usualmente os membros das classes devem ser privados, exceto nos casos onde não existam restrições para seu conteúdo. Métodos de acesso possibilitam usar membros privados indiretamente, garantindo a validade de seu conteúdo e a consistência com outros membros da classe, até mesmo quando não existem restrições. 

Os membros protegidos são destinados ao controle de aspectos internos da classe, mas que poderiam ser alvo de modificações em subclasses, desde que isto não comprometa a consistência de outros membros da classe. Quando o uso de um membro é muito complexo ou permite tornar o objeto inconsistente, é adequado declará-lo como privado, acrescentando métodos que permitam seu uso indireto e consistente.

A classe Object

Existe uma classe, tanto no Java como em C# que é muito importante. No Java, esta classe é Object, do pacote java.lang, que dá origem a toda hierarquia de classes do Java. No C# temos a mesma situação, a classe Object pertence ao namespace System. Todas as classes, de maneira direta ou indireta, possuem a classe Object como ancestral, ou seja, como superclasse. Isto ocorre porque toda vez que se declara uma nova classe, sem indicação explícita de uma classe base, é tomada implicitamente a classe Object como ancestral direto, o que cria uma raiz única para toda hierarquia de classes do Java ou do C#.

Observe que a declaração de um classe Horario, como abaixo, não indica, explicitamente uma superclasse:

public class Horario {
   private int hora;
   private int minuto;

   public Horario(int hora, int minuto) {
      setHora(hora);
      setMinuto(minuto);
   }

   public int getHora() { return hora; }
   public int getMinuto() { return minuto; }

   public void setHora(int hora) { 
      if (hora<0 || hora>23) {
         throw new RuntimeException("Hora invalida: " + hora);
      }
      this.hora = hora;
   }
   public void setMinuto(int minuto) {
      if (minuto<0 || minuto>59) {
         throw new RuntimeException("Minuto invalido: " + minuto);
      }
      this.minuto = minuto;
  }
}

Mas realmente esta declaração corresponde a:

// Java
public class Horario extends Object {
  :
}

// C#
public class Horario : Object {
  :
}

Assim, todos os objetos Java, incluindo qualquer tipo de array, compartilham os recursos disponíveis na classe Object, cuja API inclui elementos destinados à:
  • semântica de comparação (métodos equals(Object) e hashcode());
  • coleta de lixo (método finalize());
  • reflexão (método getClass());
  • representação textual (método toString()); e
  • sincronização de threads (métodos wait(), notify() e notifyAll()).
Em C# a situação é idêntica. Além disso, temos duas consequências importantes do fato de qualquer classe ser derivada de java.lang.Object ou de System.Object:
  • É a herança da classe Object que define uma infraestrutura comum a todos os objetos, possibilitando que acionem os métodos públicos lá definidos, como toString() e os demais.
  • Como toda classe é derivada de Object, todas as instâncias, quaisquer sejam suas classes específicas, podem ser tratadas como instâncias de Object, justificando porque muitas classes da API Java lidam com o tipo Object, pois isto permite operar com qualquer tipo de objeto.

Modificador final

O modificador final pode ser aplicado a uma declaração de atributo, método ou classe. Nestas três situações ele indica que a declaração onde se aplica é a definitiva, ou seja, que substituições ou modificações não serão aceitas.

Uma classe declarada como final não pode ser tomada como base de outra, ou seja, uma classe final não permite o uso da herança na construção de novas subclasses.

Assim, se a classe Horario fosse declarada como:

public final class Horario extends Object {
  :
}

Seria incorreto fazer:

public class HorarioDetalhado extends Horario {
  :
}

Se tal código for compilado, será produzido o erro:

HorarioDetalhado.java:1: error: cannot inherit from final Horario
public class HorarioDetalhado extends Horario {
                                      ^
1 error

Geralmente classes são feitas finais por motivos de segurança, quando não se deseja que terceiros modifiquem as classes existentes por meio de subclasses, garantindo a integridade do projeto.

Argumentos de um método que não devem ser modificados podem ser declarados como final na própria lista de parâmetros, mas como podem ser copiados e modificados indiretamente, o emprego desta declaração é meramente convencional, para comunicar tal intenção aos usuários do método.

O uso de final em atributos, quando associado ao modificador static, permite definir constantes, como mostrado no post Membros estáticos. Mas isoladamente impede que tal atributo seja modificado em subclasses. Da mesma forma, métodos declarados como final não poderão ser substituídos nas subclasses da classe onde foram declarados.

Estas duas últimas situações serão detalhadas no post sobre sobreposição.

Considerações finais

A herança simples é uma das características mais utilizadas da orientação a objeto na construção de sistemas, pois facilita o projeto ao permitir que classes contendo a infraestrutura de outras tenham seus membros compartilhados, seletivamente conforme determinado pelos seus especificadores de acesso, possibilitando a construção de outras, sem necessidade de copiar código, o que agiliza o desenvolvimento e simplifica as atividades de manutenção.


POO-P-19-Especificadores e Modificadores POO-P-21-Sobreposição

Referências Bibliográficas

[1] JAMSA, K.; KLANDER, L.. Programando em C/C++: a bíblia. São Paulo: Makron Books, 1999.
[2] PAGE_JONES, M.. Fundamentos do Desenho Orientado a Objeto com UML. São Paulo: Makron Books, 2001.
[3] SOMMERVILLE, I.. Software Engineering. 6th. Ed. Harlow: Pearson, 2001.
[4] DEITEL, H.M.; DEITEL, P.J.. Java: como programar. 6a. Ed. São Paulo: Pearson Prentice-Hall, 2005.
[5] SAVITCH, W.. C++ Absoluto. São Paulo: Pearson Addison-Wesley, 2004.
[6] JANDL JR., P. Introdução ao C++. São Paulo: Futura, 2003.
[7] JANDL JR., P.. Java - guia do programador. 3a. ed. São Paulo: Novatec, 2015.
[8] RUMBAUGH, J.; BLAHA, M.; PREMERLANI, W.; EDDY, F.; LORENSEN, W.. Object-oriented modeling and design. Englewoods Cliffs: Prentice-Hall, 1991.
[9] STROUSTRUP, B.. The C++ Programming Language. 3rd Ed. Reading: Addison-Wesley, 1997.
[10] LANGSAM, Y.; AUGENSTEIN, M. J.; TENENBAUM, A. M.. Data structures using C and C++. 2nd Ed. Upper Saddle River: Prentice-Hall, 1996.
[11] WATSON, K.; NAGEL, C.; PEDERSEN, J.H.; REID, J.D.; SKINNER, M.; WHITE, E.. Beginning Microsoft Visual C# 2008. Indianapolis: Wiley Publishing, 2008.
[12] GAMMA, E.; HELM, R.; JOHNSON, R.; VLISSIDES, J.. Design Patterns: Elements of Reusable Object-Oriented Software. Reading, MA: Addison-Wesley, 1995.

Nenhum comentário: