[Pesquisar este blog]

segunda-feira, 2 de abril de 2018

POO::Plena-24-Polimorfismo, upcasting e downcasting

POO-P-23-PolimorfismoPOO-A-25-Classes internas e anônimas
O polimorfismo é a característica mais importante da OO e consiste no mecanismo pelo qual vários tipos de objetos, ou seja, formas diferentes de implementação, podem ser tratadas como sendo de um tipo comum.
Todas as linguagens orientadas a objetos possuem uma classe que constitui a raiz de sua hierarquia de tipos. A partir desta classe são criadas outras por meio da herança, a qual pode ser usada de maneira implícita ou explícita. Na plataforma Java a raiz da hierarquia é a classe java.lang.Object; enquanto que no C# a classe-raiz é System.Object.

Desta maneira, qualquer que seja a hierarquia de classes considerada, todas as classes ali existentes são subclasses de Object, sendo assim, uma variável do tipo Object pode armazenar referências de qualquer tipo de objeto.

O polimorfismo permite que um objeto seja, transparentemente, tratado como qualquer outro tipo ascendente (superclasses da qual é derivado), e também como sendo do seu tipo verdadeiro, isto é, como foi instanciado de fato. Isso possibilita a generalização, ou seja, uma operação que faz o contrário da herança, que permite a especialização.

Como vimos, um objeto cuja classe é derivada de outra é uma composição de objetos em camadas. A camada mais externa corresponde ao tipo real do objeto (por exemplo Gerente) e nela se encontram as adições providas por esta classe (sua especialização em relação à camada anterior). A camada mais interna é sempre aquela que corresponde ao tipo Object, onde estão presentes seus elementos. As camadas intermediárias são aquelas fornecidas pelos descendentes de Object até a camada mais externa. A figura que segue ilustra esta estrutura em camadas de um objeto de uma possível classe Comissionado.

Conforme o tipo da referência usada para acessar um objeto, determina-se quais camadas poderão efetivamente ser usadas. Se utilizamos uma referência do tipo real (de instanciação), temos acesso a todas os membros públicos deste tipo e de todos os tipos ancestrais (ascendentes). Na figura acima, a referência do tipo Comissionado permite acessar todos os membros públicos do objeto presentes na camada mais externa (Comissionado), na camada intermediária (Funcionario) e também na camada mais interna (Object).

Mas se a referência é do tipo Funcionario, só é possível acessar os membros desta camada (Funcionario) e das mais internas (no caso apenas Object). Uma referência de tipo Funcionário não consegue acessar nada presente em camadas mais externas do que seu tipo, porque sua classe não prevê sua existência, pois foram adicionadas nas especializações (subclasses) posteriores.

Da mesma maneira, uma referência do tipo Object só consegue acessar a porção referente a este tipo presente em objetos de qualquer classe.

Por conta da construção dos objetos em múltiplas camadas e do comportamento dos diferentes tipos de referências em relação às camadas presentes nos objetos, temos duas importantes operações decorrentes do polimorfismo:
  • up type casting (ou apenas upcasting) e
  • down type casting (ou apenas downcasting).


Polimorfismo e upcasting

O uptype casting, upcasting ou coerção para supertipo é a operação disponível na orientação a objetos que permite que uma referência de superclasse utilize a porção correspondente nos objetos descendentes de sua hierarquia. Em outras palavras, uma referência de um certo tipo pode acessar sua camada correspondente em qualquer objeto de tipo derivado, isto é, em qualquer subclasse.

Observe novamente a hierarquia de objetos. Funcionario (que é uma classe derivada de Object) dá origem a várias classes, tais como Caixa, Gerente, Motorista ou Comissionado.

Uma referência do tipo Gerente permite acessar todas as camadas presentes nos objetos instanciados como deste tipo, assim como apenas uma referência do tipo Comissionado permite acessar todos os elementos deste tipo, como ilustrado na figura que segue.

Referências deste tipo podem ser obtidas como no fragmento abaixo:

Gerente g2 = new Gerente(2345);
Comissionado c2 = new Comissionado(3456);

No entanto, como um Gerente é um Funcionario (é subclasse deste tipo e possui uma camada interna correspondente), uma referência do tipo Funcionario pode ser usada para acessar tal camada (e as interiores) em qualquer objeto do tipo Gerente ou Comissionado, pois é garantido que tais objetos possuem a camada interior correspondente a Funcionario.

Por meio do polimorfismo, uma referência de tipo Funcionario pode acessar objetos de tipos diferentes, embora tal acesso seja limitado a camada correspondente à referência, portanto de objetos cujos tipos são subclasses do tipo da referência. Assim é válido fazer:

// Funcionario é superclasse de Gerente
// Gerente é subclasse de Funcionario
Funcionario f2 = g2;
Funcionario f3 = new Gerente(2346);

// Funcionario é superclasse de Comissionado
// Comissionado é subclasse de Funcionario
Funcionario f4 = c2;
Funcionario f5 = new Comissionado(3457);

Aqui foi usado o upcasting, ou seja, uma referência de um tipo ancestral (Funcionario) foi usada para acessar a camada correspondente em objetos de seus subtipos (Gerente e Comissionado). Assim, objetos de tipos diferentes são acionados por um mesmo conjunto de métodos, ou seja, recebem os mesmos tipos de mensagens. Esta situação é ilustrada na figura que segue.

Quando o tipo de referência usada não é compatível com a hierarquia do objeto, o compilador sinaliza o erro e impede que o código seja compilado, evitando que objetos sejam incorretamente manipulados por referências inadequadas.

Já referências de tipo Object, por meio do upcasting, podem acessar qualquer tipo de objeto, pois qualquer tipo é subclasse de Object e todo objeto possui como camada mais interior aquela que corresponde a Object, como mostra a figura que segue.

Assim temos que é possível fazer:

Object o1 = new Gerente(2367);
Object o2 = new Comissionado(3489);
Object o3 = f2;
Object o4 = f5;
Object o5 = new String("Upcasting");

Isto significa que, sob certos aspectos, uma operação de up type casting é como uma operação implícita de coerção (conversão de tipo) que possibilita o uso mais genérico de um objeto, mas só é válida quando utiliza subclasse do tipo real do objeto.

O upcasting é um mecanismo polimórfico de generalização, que permite o uso de um objeto por meio de referências de seus supertipos, embora as operações estejam limitadas as características da superclasse usada. Isto é conveniente para tratar famílias de objetos, pois uma referência de superclasse permite manipular qualquer um de seus subtipos, ao invés de requerer referências de tipos específicos.

Isto flexibiliza o armazenamento de objetos em arrays e outras estruturas de dados; possibilita que métodos retornem objetos de tipos diferentes (mas de uma mesma família; além de facilitar alterações no projeto de sistemas.

Por exemplo, no fragmento de código que segue, um array de tipo Funcionario pode conter objetos de qualquer tipo derivado de Funcionario:

// cria array de Funcionario
Funcionario folha[] = new Funcionario[3];

// instancia e ajusta objeto tipo funcionário
Funcionario f = new Funcionario(1234);
f.setSalarioBase(850);

// atribui objeto Funcionario ao array de mesmo tipo
folha[0] = f;

// instancia e ajusta objeto tipo Comissionado
Comissionado c = new Comissionado(2345);
f.setSalarioBase(650);

// atribui objeto Comissionado ao array
folha[1] = c;

// instancia objeto Gerente, atribuindo-o ao array, ajustando-o
folha[2] = new Gerente(3456);
folha[2].setSalarioBase(4500);

Nos elementos [1] e [2] do array, cujo tipo declarado é Funcionario, ocorre o upcasting, pois objetos de subclasses de Funcionario são ali armazenados.

O uso do array de Funcionario permite generalizar o tratamento de funcionários de tipos diferentes, como no outro fragmento que segue:

double folhaBaseTotal = 0;
for(int i=0; i<folha.length; i++) {
   folhaBaseTotal += folha[i].getSalarioBase();
}
System.out.println("Folha total = " + folhaBaseTotal);

No laço existente, a despeito do tipo real, todas as características do tipo Funcionario estão disponíveis por meio das referências existentes no array de tipo Funcionario. No entanto, ao acionar método getSalarioBase(), disponível na interface do tipo Funcionario, é de fato acionada a implementação existente na classe efetiva do objeto armazenado naquela posição, pois esta substitui (method overriding) a definida em Funcionario. Assim, este laço de tratamento generalizado acaba usado a operação definida nas subclasses de Funcionario, sem qualquer tratamento condicional.

O upcasting permite, assim, criar operações genéricas para lidar com múltiplos tipos de objetos, relacionados numa hierarquia de classes por meio de um ancestral comum, como segue:

double getFolhaBaseTotal(Funcionario[] folha ){
   double folhaBaseTotal = 0;
   for(int i=0; i<folha.length; i++) {
      folhaBaseTotal += folha[i].getSalarioBase();
   }
   return folhaBaseTotal;
}

O método getFolhaBaseTotal(Funcionario[]) recebe um array de tipo Funcionario. Como antes, a despeito do tipo real, todas as características do tipo Funcionario estão disponíveis por meio das referências existentes no array de tipo Funcionario. Mas, dada a sobreposição de métodos, serão usadas as implementações dos tipos específicos. Então, mesmo que surjam novos subtipos derivados de Funcionario, esta operação continuará a funcionar, sem necessidade de qualquer alteração.

Polimorfismo e downcasting

O polimorfismo também dispõe de uma operação que permite converter uma referência de superclasse em outra de subclasse, portanto mais especializada, que é chamada de down type casting, downcasting ou coerção para subtipo.


Como mostra a figura, o downcasting é uma operação inversa ao upcasting. Enquanto o upcasting ocorre quando se utiliza uma referência de supertipo (ancestral) para o tratamento de um objeto; no downcasting é usada uma referência de subtipo (descendente) para o tratamento de um objeto.

Aqui, a referência desejada como resultado do downcasting passa a utilizar uma camada mais externa do objeto, decorrente da especialização. Mas como o compilador não pode garantir que o objeto referenciado possua tais camadas adicionais, esta operação deve ser explicitamente indicada pelo programador, como uma espécie de autorização para sua realização.

Observe no fragmento de código que segue, como uma referência f3, de tipo Funcionario ou mesmo Object, é transformada em um objeto de tipo Comissionado (que é uma de subclasses); ou como uma referência de Object é transformada em outra do tipo String:

Comissionado c = (Comissionado) f3;
String str = (String) obj;

A transformação de tipo, down type casting ou coerção possibilita transformar uma referência do objeto em outra de seu tipo real ou de tipo mais próximo. Como nem sempre isso será possível, porque o objeto não foi, de fato, criado como algo daquele tipo, a compilação do código requer a explicitação da conversão por meio da indicação do tipo de destino entre parêntesis.

Se a referência não pode ser transformada na tipo indicado, o downcasting provoca o lançamento da exceção ClassCastException, como nestes exemplos inválidos:

// Upcasting OK
Funcionario f4 = new Gerente (6789);
// Downcasting INVALIDO
Comissionado c = (Comissionado) f4;

Para evitar esse tipo de exceção, existe o operador instanceof.

Operador instanceof

O operador especial instanceof, que retorna um resultado booleano, permite verificar, em tempo de execução, se um objeto é de um tipo específico ou de suas subclasses. A sua sintaxe é:

<objeto> instanceof <Tipo>

O uso deste operador retorna true, quando o objeto é uma instância direta do tipo indicado ou de alguma de suas subclasses; ou false, quando o objeto não é uma instancia do tipo indicado ou de qualquer uma de suas subclasses.

O uso do operador instanceof permite evitar erros em operações de downcasting, mas sua aplicação não pode ser generalizada, devendo empregar tipos específicos. Um exemplo:

Funcionario func = new Gerente(5678);
if (func instanceof Gerente) {
   Gerente g = (Gerente) func; // downcasting
   System.out.println("Gerente depto: " + g.getGerencia() );
}

Outro exemplo:

Integer n = new Integer (123456789);
if (n instanceof Number) {
   count= n.intValue();
}

Com o uso de instanceof é possível garantir a execução de operações de downcasting sem a ocorrência de exceções ClassCastException decorrentes de coerção inválida de objetos.

Considerações finais

O polimorfismo é o mecanismo principal da OO que pode se manifestar tanto em tempo de compilação, quanto em tempo de execução.

Em tempo de compilação o polimorfismo aparece como a sobrecarga de métodos (method overload), também como sobrecarga de operadores em algumas linguagens OO, também sendo chamado de ligação precoce (early binding). Já em tempo de execução, aparece como sobreposição de métodos (method overriding), que é decorrência do uso do mecanismo da herança onde a versão mais especializada de um método, presente em subclasses, é acionada, situação conhecida como ligação tardia (late binding).

Assim, com o polimorfismo é possível:
  • Que subclasses forneçam implementações diferentes de métodos com uma mesma assinatura (ou seja, a sobreposição de métodos ou method overriding), provendo então comportamentos distintos, modificados em cada classe derivada conforme a especialização desejada.
  • Que referências de superclasse acionem métodos de subclasse comuns à superclasse (sobrepostos) em tempo de execução, ou seja, invoquem métodos diferentes, mas com a mesma assinatura, possibilitando que tipos diferentes recebam as mesmas mensagens.

Finalmente, grande parte da flexibilidade e elegância das linguagens de programação OO decorrem do polimorfismo.

POO-P-23-Polimorfismo
POO-A-25-Classes internas e anônimas

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.

segunda-feira, 26 de março de 2018

POO::Plena-23-Polimorfismo

POO-P-22-Herança MúltiplaPOO-A-24-Polimorfismo, upcasting e downcasting
A palavra polimorfismo vem do grego poli morfos e significa muitas formas. É a característica chave da orientação a objetos que torna possível utilizar múltiplas formas de construção de objetos. Na OO o polimorfismo de manifesta na sobrecarga, na herança, na sobreposição e também na generalização.

Na verdade, o polimorfismo é um mecanismo único, semelhante a uma força. Em uma direção ele provê a especialização (de tipos), enquanto no sentido oposto oferece a generalização (de tipos). É por meio do polimorfismo que as linguagens OO proporcionam mecanismos de classificação e também de reuso.

Mecanismos do polimorfismo::sobrecarga

A sobrecarga de métodos (method overload) é uma propriedade da OO que permite a existência de dois ou mais métodos com o mesmo nome em uma mesma classe, desde que possuam assinaturas diferentes. Devemos recordar que a assinatura de um método é a combinação de seu nome com a lista dos tipos de seus parâmetros formais. 

Assim, o método estático pow da classe Math, que toma dois parâmetros do tipo double, tem assinatura pow(double, double); o método getClass() da classe Objeto não toma parâmetros, sendo sua assinatura getClass(void); enquanto o método get da classe ArrayList toma um inteiro como argumento e tem assinatura get(int).

É a assinatura diferente que possibilita a diferenciação do método que deve ser acionado no ponto de sua invocação (i.e., na chamada do método); por isso o tipo de retorno é desconsiderado e não faz parte da assinatura, como ilustra a figura que segue.


Desta maneira, métodos conceitualmente semelhantes podem possuir o mesmo nome, diferenciando-se pela lista requerida de argumentos, que possibilita que cada um realize sua operação de maneiras distintas. Por exemplo, a classe String oferece quatro métodos com nome indexOf:
  • indexOf(char) retorna a posição onde se localiza a primeira ocorrência do caractere fornecido como argumento;
  • indexOf(char, int) retorna a posição da primeira ocorrência do caractere suprido como argumento, a partir da posição indicada;
  • indexOf(String) retorna a posição onde se encontra a primeira ocorrência da substring indicada como argumento;
  • indexOf(String, int) retorna a posição da primeira ocorrência da substring dada como argumento, a partir da posição indicada.

A existência de métodos sobrecarregados proporciona flexibilidade para o uso da classe, pois várias formas das operações estão presentes, ao mesmo tempo que mantém a interface da classe simples, porque conceitualmente os métodos sobrecarregados fornecem uma mesma operação.

Construtores também podem ser sobrecarregados, aumentando as possibilidades de criação de objetos. A classe String possui 11 diferentes construtores, que permitem várias formas de instanciação de cadeias de caracteres.

Mecanismos do polimorfismo::herança

A herança (inheritance) é, talvez, a segunda característica mais importante da OO. É o mecanismo que permite a construção de novos tipos de dados (classes) baseada em outros tipos já existentes, possibilitando a criação de hierarquias de classes, como ilustrado a seguir.

Tipicamente, uma hierarquia de classes é como uma árvore cuja raiz é colocada na parte superior do diagrama. A classe-raiz, que dá origem às demais, é colocada no topo, assim, de modo que, para qualquer classe presente na árvore, seus tipos ascendentes ficam acima, enquanto seus descendentes ficam abaixo.

Na herança, uma classe, tomada como superclasse ou classe-pai, pode dar origem a uma ou mais novas classes denominadas de subclasses ou classes-filha. Esta relação pai-filha, identificada pela resposta afirmativa a questão "é um? | é do tipo?", conecta as classes no diagrama e permite, do ponto de vista de implementação, que:
  • as características ancestrais sejam compartilhadas pelos descendentes;
  • novas características possam ser adicionadas nos descendentes;
  • características presentes nos ascendentes sejam modificadas ou restringidas.

O compartilhamento da implementação da superclasse nas subclasse já é um ganho, pois atributos e operações públicas e protegidas tornam-se acessíveis para todos os descendentes da classe, evitando repetição de código e facilitando a manutenção. Isto é um mecanismo efetivo de simplificação de projeto e de reuso.

Além disso, ao adicionar novos atributos e operações, a subclasse é diferenciada de seu ancestral, o que corresponde a um mecanismo de extensão ou de especialização das classes.

Também é possível modificar o comportamento de operações existentes, utilizando outro mecanismo decorrente do polimorfismo denominado sobreposição, que será comentado um pouco mais a frente.

A adição de novos atributos e novas operações é a forma mais comum de aproveitar o mecanismo da herança. Nesta situação procura-se identificar os elementos comuns a um conjunto de classes, mantendo tais  elementos comuns em uma classe que funcionará como ponto de partida comum para um conjunto de outras classes, tal como na ilustração acima, onde a classe Funcionario concentra aspectos comuns de todos os tipos de funcionário de uma empresa, possibilitando que novas classes, especializadas em funções específicas, como Caixa, Motorista, Segurança ou Gerente, possam ser criadas.

Para auxiliar no tarefa de fatoração das características comuns e também na identificação daquelas que são específicas, a organização de uma tabela de classes/tipos e suas funcionalidades pode ser muito útil. Partindo da narrativa descritiva do problema, as classes que representam as entidades/atores do problema usualmente figuram como substantivos. Outros substantivos, adjetivos e complementos costumam indicar os atributos destas entidades; já os verbos apontam as funcionalidades que serão representadas por métodos das classes associadas.

É claro que o benefício da herança é permitir o compartilhamento da implementação dos aspectos comuns destes funcionários, sem necessidade de repetição deste código. Isto mantém a coesão necessária a cada classe, ao mesmo tempo que promove o reuso, facilita a manutenção do código e ainda permite que, no futuro, novas classes sejam adicionadas à hierarquia construída.

Mecanismos do polimorfismo::sobreposição

A sobreposição (ou substituição) de métodos, também conhecida como method overriding, consiste na implementação de um método na subclasse com a mesma assinatura de outro existente na superclasse.

Isto permite dotar a subclasse de uma implementação distinta daquela existente na superclasse, mas mantendo sua interface, o que facilita seu uso por meio do polimorfismo, como será visto mais à frente.

É necessário frisar que a sobreposição envolve a criação de um novo método, com a mesma assinatura, de uma operação existente nas superclasses daquela onde é codificado. Assim, isto constitui um mecanismo distinto da sobrecarga, que envolve a criação de métodos com mesmo nome, mas assinaturas diferentes, em uma mesma classe.

Em conjunto com a herança, a sobreposição permite que uma subclasse tenha comportamento diferente da superclasse, mantendo sua interface. Esta é uma outra forma de especialização da classe, sem ampliação de suas características.

Além disso, métodos considerados inadequados na subclasse, embora não possam ser ocultos (ou seja, deixarem de ser públicos), suas versões sobrepostas podem lançar exceções impedindo seu uso. Com isso, a sobreposição permite a contração de classes, ou seja, a redução de suas funcionalidades.

Mecanismo da sobrecarga::generalização

Sempre é adequado destacar que o polimorfismo é a característica mais importante da OO, pois representa o mecanismo pelo qual vários tipos de objetos, ou seja, várias formas de implementação, podem ser tratadas como sendo de um tipo comum.

Por isso é importante observar que:
  • Uma referência (de instância) é uma variável cujo tipo é uma classe.
  • Cada referência aponta para UM objeto de cada vez.
  • É possível trocar o valor de uma referência, por outra, para que ela aponte para OUTRO objeto.
  • O valor null indica que a referência não aponta qualquer objeto válido.
  • Várias referências diferentes podem apontar para um mesmo objeto.

No Java, em particular, as referências não são endereços de memória, mas identificadores de objetos definidos pela máquina virtual Java (JVM). Por meio desses identificadores, a JVM verifica quando os objetos estão sendo referenciados (em uso aparente). Se a JVM detecta que um objeto deixa de ser referenciado, isto é, quando não existem referências apontando para um objeto específico, ele é marcado pelo Automatic Garbage Collector para descarte (remoção da memória).

Também sabemos que na plataforma Java, qualquer classe é, direta ou indiretamente, uma subclasse de java.lang.Object, pois mesmo numa declaração de classe simples, implicitamente usamos o mecanismo da herança, tomando como superclasse o tipo Object, que constitui assim a raiz da hierarquia de classes Java. Com o C# ocorre o mesmo, mas com a classe System.Object sendo a raiz da hierarquia de classes.

Sendo assim, considerando qualquer hierarquia de classes, toda classe existente é uma subclasse de Object, de modo que uma variável do tipo Object pode armazenar referências de qualquer tipo de objeto:

Object o;
o = new String("Polimorfismo");
o = new Integer(123456);
o = new ArrayList();
o = new FileReader("arquivoTexto.txt");
o = new JFrame("GUI");
o = new Thread("fluxo de processamento");

O polimorfismo permite que um objeto seja, transparentemente, tratado como qualquer outro tipo ascendente (superclasses da qual é derivado), e também como sendo do seu tipo verdadeiro, isto é, como foi instanciado de fato.

A possibilidade de admitir múltiplos tratamentos para um mesmo objeto é a própria definição de polimorfismo. Mas existe uma condição envolvida nesta forma do polimorfismo: um objeto pode tratado como do seu próprio tipo, como sendo de qualquer um de seus ancestrais, até a raiz da hierarquia, ou seja, como do tipo Object.

Se observarmos novamente a figura da hierarquia de funcionários, podemos concluir que as classes Caixa, Motorista, Segurança e Gerente, derivadas diretamente da classe Funcionario, podem ter seus objetos tratados como sendo do tipo Funcionario, ou mesmo Object.

Isso possibilita a generalização, ou seja, uma operação que faz o contrário da herança, que permite a especialização.

Um objeto cuja classe é derivada de outra é como uma composição de objetos em camadas. A camada mais externa corresponde ao tipo real do objeto (por exemplo Gerente) e nela se encontram as adições providas por esta classe (sua especialização em relação à camada anterior). A camada mais interna é sempre aquela que corresponde ao tipo Object, onde estão presentes seus elementos. As camadas intermediárias são aquelas providas pelos descendentes de Object até a camada mais externa.

A figura que segue ilustra um objeto de uma possível classe Comissionado, a qual é derivada de (uma subclasse de) Funcionario, que por sua vez é derivada de (uma subclasse de) Object. Assim, o objeto possui três camadas, uma provida por cada classe de sua hierarquia, a mais interna correspondente a Object e a mais externa correspondente ao seu tipo real (ao tipo de sua criação -- instanciação).


Conforme o tipo da referência usada para acessar um objeto, determina-se quais camadas serão efetivamente acessadas. Se é utilizada uma referência do tipo real (de instanciação), temos acesso a todas os membros públicos deste tipo e de todos os tipos ancestrais (ascendentes). Na figura acima, a referência do tipo Comissionado pode acessar todos os membros públicos do objeto presentes na camada mais externa (Comissionado), na camada intermediária (Funcionario) e também na camada mais interna (Object).

Mas se a referência é do tipo Funcionario, só é possível acessar os membros desta camada (Funcionario) e das mais internas (no caso apenas Object). Uma referência de tipo Funcionário não consegue acessar nada presente em camadas mais externas do que seu tipo, porque sua classe não prevê sua existência, pois foram adicionadas em especializações (subclasses) posteriores.

Da mesma maneira, uma referência do tipo Object só consegue acessar a porção referente a este tipo presente em objetos de qualquer classe.

Por conta da construção dos objetos em múltiplas camadas e do comportamento dos diferentes tipos de referências em relação às camadas presentes nos objetos, temos duas importantes operações decorrentes do polimorfismo:
  • up type casting (ou apenas upcasting) e
  • down type casting (ou apenas downcasting).
Mas este é o assunto do próximo post!

POO-P-22-Herança MúltiplaPOO-A-24-Polimorfismo, upcasting e downcasting

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.

quinta-feira, 22 de março de 2018

POO::Plena-22-Herança múltipla

POO-P-21-Sobreposição POO-P-23-Polimorfismo
A herança (inheritance) é uma das formas mais importantes do polimorfismo que possibilita a especialização de classes. De fato, é uma técnica que possibilita compartilhar partes da implementação de uma classe com outra, além de permitir a adição de novos elementos, o que torna as novas classes em versões especializadas.

Com isto podem ser criadas famílias de classes, cujos participantes possuem características semelhantes, o que facilita seu desenvolvimento, sua manutenção e também seu reuso. Este mecanismo é único, pois só existe na orientação a objetos.

Considerando que existem muitos tipos de objetos e de entidades, reais e abstratas, estes podem ser classificados em hierarquias, nas quais os elementos superiores agrupam as características comuns dos seus derivados. Esta técnica é usada frequentemente nos diversos campos da conhecimento para criar classificações (ou taxonomias) de seres, componentes, máquinas ou funções.

Desta maneira, a herança é um mecanismo da orientação a objetos que permite representar as relações de semelhança, identificadas afirmativamente pela questão 'é um(a)?', existentes entre conceitos. Uma de suas contribuições diretas é evitar que atributos e operações comuns sejam definidos repetidamente em classes relacionadas. 

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

A linguagem de programação Java foi originalmente projetada para oferecer apenas a herança simples, assim como o C#, apesar de um pouco mais recente.


Herança múltipla

Em algumas situações, as relações existentes entre conceitos não podem ser representadas pela herança simples, que relaciona diretamente uma única superclasse (ou classe base) com sua subclasse (ou classe derivada). Nestes casos, torna-se necessário representar as subclasses relacionadas a partir de duas ou mais outras classes. Esta é a herança múltipla, que permite que uma nova classe compartilhe elementos de duas ou mais superclasses.

Como as linguagens de programação Java e C# não possuem herança múltipla, os exemplos desta seção utilizarão a sintaxe da linguagem C++.

A sintaxe para a construção de classes com herança múltipla em C# é a seguinte:

[acesso] class <NomeSubClasse> : [acesso] <NomeSuperClasse1>,
                                 [acesso] <NomeSuperClasse2>, 
                                 ... ,
                                 [acesso] <NomeSuperClasseN> {
   // corpo da subclasse 
};
// implementação de membros da subclasse

Pode ser observado que, após o nome da subclasse o sinal de dois pontos (':'), segue uma relação dos nomes das superclasses tomadas para herança, separados por vírgulas (',').

A herança múltipla possibilita que uma nova classe seja criada pela combinação de características de várias classes existentes, evitando a repetição destas características no código e permitindo tratamento polimórfico (como será visto no post sobre polimorfismo).

A classe Funcionario, que segue representa um funcionário qualquer de uma empresa que pode ser distinguido dos demais por meio do atributo privado long id que tem papel de um identificador.

class Funcionario {
// membros privados
    long id;
// membros publicos
  public:
    Funcionario(long nid) {
      cout << "Construtor Funcionario: " << nid << endl;
      id = nid;
    }
    ~Funcionario() { cout << "Destrutor Funcionario\n"; }
    long getId() { return id; }
};

Observe que o único construtor disponível em Funcionario exige um parâmetro de tipo long que indica o id do funcionário.

Já a classe Temporario representa os colaboradores contratados por período determinado, sendo seu principal atributo o privado de tipo int periodo.

class Temporario {
// membros privados
    int periodo;
// membros publicos
  public:
    Temporario(int p) {
      cout << "Construtor Temporario: " << p << endl;
      periodo = p;
    }
    ~Temporario() { cout << "Destrutor Temporario\n"; }
    int getPeriodo() { return periodo; }
};

Aqui também temos que o único construtor disponível em Temporario exige um parâmetro de tipo int que requer o número de períodos do contrato de trabalho temporário.

Uma empresa pode possuir engenheiros, que são um tipo particular de funcionário, ou seja, cuja classe Engenheiro pode ser uma subclasse de Funcionario, empregando herança simples. Observe a implementação da classe Engenheiro que segue.

class Engenheiro: public Funcionario {
// membros publicos
  public:
    Engenheiro(long nid): Funcionario(nid) {
      cout << "Construtor Engenheiro: " << nid << endl;
    }
    ~Engenheiro() { cout << "Destrutor Engenheiro\n"; }
};

Note que a classe Engenheiro não possui campos privados. Além disso, o construtor declarado aciona o construtor da superclasse Funcionario para prover o valor requerido do id.

Como a classe Engenheiro toma, por meio da herança simples e pública, a superclasse Funcionario, compartilha seus membros públicos, isto é, a operação getId().

Uma empresa também pode possuir estagiários, que são funcionários temporários, de modo que uma classe Estagiario, como segue, deve utilizar da herança múltipla para compartilhar aspectos das classes Funcionario (o id do funcionário) e também Temporario (o número de períodos de contrato de trabalho).

class Estagiario: public Funcionario, public Temporario {
  public:
    Estagiario(long nid, int p): Funcionario(nid), Temporario(p) {
      cout << "Construtor Estagiario: " << nid << "," << p << endl;
    }
    ~Estagiario() { cout << "Destrutor Estagiario\n"; }
};

Outra vez é possível observar que o construtor declarado aciona os construtores das superclasses Funcionario (para prover o valor requerido do id) e Temporario (para suprir o valor dos períodos).

A classe Estagiario toma, por meio da herança múltipla e pública, as superclasses Funcionario e Temporario; compartilhando seus membros públicos, isto é, as operações getId() (de Funcionario) e getPeriodo() (de Temporario).

A figura que segue ilustra um diagrama de classe UML simplificado que mostra as relações de herança existentes entre as classes Funcionario, Temporario, Engenheiro e Estagiario.


A herança múltipla é um recurso sofisticado que deve ser utilizado com bastante cautela principalmente por programadores menos experientes. Alternativas tais como a herança simples, a agregação ou composição (que foram tratadas em posts anteriores) não devem ser descartadas e podem ser mais simples de implementar e utilizar.

A herança múltipla é um aspecto complexo de C++, muito debatido. Para alguns é uma característica desnecessária, que pode ser substituída por outros mecanismos mais simples, evitando os diversos problemas de ambigüidade decorrentes. Para outros trata questões específicas e quando bem empregada conduz a resultados satisfatórios.

Por conta deste debate, tanto Java como C# não oferecem a herança múltipla, tendo seus projetistas optado por oferecer apenas a herança simples, complementada pelo mecanismo de implementação de interfaces, que será discutido num post mais a frente.

POO-P-21-Sobreposição POO-P-23-Polimorfismo

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.

segunda-feira, 19 de março de 2018

POO::Plena-21-Sobreposição

POO-P-20-Herança Simples POO-P-22-Herança Múltipla
A herança (inheritance) é um mecanismo da orientação a objetos que permite estabelecer relações de hierarquia entre classes e possibilita compartilhar membros selecionados entre elas. Esta é uma das características únicas e importantes da orientação a objetos [1][2][4][5][6][7].

A classe tomada como ponto de partida na herança torna-se a superclasse (ou classe-base ou classe-pai) de uma família, onde as subclasses (ou classes-derivadas ou classes-filha) criadas a partir desta primeira podem compartilhar de seus membros públicos e protegidos.

Além do compartilhamento de elementos existentes na superclasse, as subclasses podem adicionar novos atributos e operações próprios, de maneira a se tornarem extensões ou especializações de suas superclasses [4][5][6][7].

Mas também é possível substituir membros existentes, restringindo ou modificando as características herdadas. A substituição ou reescrita de métodos é o que se denomina sobreposição ou overriding.


A sobreposição de métodos

A substituição de métodos ou sobreposição de métodos é uma técnica conhecida também como method overriding ou apenas overriding. Constitui uma alternativa de implementação na qual uma subclasse recebe um método com a mesma assinatura de um método existente na superclasse, substituindo-o [6][7].

Esta técnica é tanto conveniente, como interessante, pois permite que a subclasse contenha uma implementação mais apropriada de um método definido na superclasse, o que mantém sua interface, facilita seu uso por meio do polimorfismo, ao mesmo tempo que torna possível alterar como tal operação é realizada [6][7].

Considere a classe Java que segue, denominada Motor.

public class Motor {
   public static final double potenciaMax = 100;

   public double getPotencia() {
      return potenciaMax;
   }
}

A classe Motor possui apenas um campo constante denominado potenciaMax, e um método de observação getPotencia().

Analise agora a classe MotorControlavel, que é uma subclasse de Motor.

public class MotorControlavel extends Motor {
   private double taxa = 0;

   public double getTaxa() { return taxa; }

   public void setTaxa(double taxa) {
      if (taxa<0 || taxa>1) {
         throw new RuntimeException("Taxa invalida: " + taxa);
      }
      this.taxa = taxa;
   }

   @Override
   public double getPotencia() {
      return taxa * potenciaMax;
   }
}

A classe MotorControlavel acrescenta um campo de tipo double denominado taxa e também os métodos de acesso getTaxa() e setTaxa(double) que permitem obter e ajustar o valor do campo taxa, garantindo que tenha um valor entre 0 e 1 (ou seja, 0 e 100%). O método getPotencia() da subclasse MotorControlavel é reescrito para fornecer uma implementação mais adequada, a qual substitui aquela existente na superclasse Motor, ou seja, efetua a sobreposição de método ou method override.

Objetos do tipo Motor e MotorControlavel podem ser instanciados e ter seus métodos getPotencia() acionados, o que provoca a execução da implementação efetivamente disponível em seu tipo, como segue:

Motor motor = new Motor();
MotorControlavel motorContr = new MotorControlavel();
motorContr.setTaxa(0.5);
// método getPotencia() acionado é da classe Motor
System.out.println("motor: " + motor.getPotencia());
// método getPotencia() acionado é da classe MotorControlavel
System.out.println("motorContr: " + motorContr.getPotencia());

Este pequeno exemplo ilustra como um método pode substituir outro existente em suas superclasses, provendo uma nova implementação que substitui o método para a classe onde ocorre a substituição e suas descendentes. Ao mesmo tempo, as várias classes da hierarquia oferecem a mesma interface, o que possibilita o uso polimórfico dos objetos destas classes, mas sempre acionando o método que corresponde ao tipo de origem (de instanciação) do objeto.

Polimorfismo na sobreposição

Considerando a classe Motor e sua subclasse MotorControlavel, sabemos que é possível referenciar um objeto do tipo MotorControlavel por meio de uma referência do tipo Motor, pois um MotorControlavel "é um" Motor:

Motor mc = new MotorControlavel();

Apesar disso, quando é acionado o método getPotencia() por meio da referência do tipo Motor, a versão efetivamente invocada é aquela que pertence a classe MotorControlavel (do tipo de nascença), pois o objeto referenciado pela variável mc é, de fato, do tipo MotorControlavel, não sendo possível "burlar" a substituição efetuada no código por meio de referências de suas superclasses.

Assim, quando ocorre a sobreposição de um método, as instâncias das subclasses ficam impedidas de acionar o método original da superclasse, pois este é o objetivo deste mecanismo.

No entanto, o uso de super permite que, exclusivamente na implementação da subclasse, seja acessada a versão de um método definida na superclasse. O método getPotencia() de MotorControlavel poderia ser sido escrito como segue, aproveitando o código do método sobreposto.

public class MotorControlavel extends Motor {
   private double taxa = 0;

   // demais membros não modificados foram omitidos
   :

   public final double getPotencia() {
      return taxa * super.getPotencia();
   }
}

Embora não seja obrigatório, essa é uma prática habitual, visto que o método reescrito geralmente adiciona funcionalidade à sua versão sobreposta. Não há qualquer problema em não utilizar o método sobreposto quando não for conveniente.

O modificador final

Nas situações onde se deseja impedir que um método seja sobrecarregado, pode ser empregado o modificador final, que indica que o método afetado não poderá ser substituído em qualquer subclasse.

Por exemplo, a classe MotorControlavel poderia sinalizar que o método getPotencia() não deve ser sobreposto em possíveis subclasses como segue:

public class MotorControlavel extends Motor {
   private double taxa = 0;

   // demais membros não modificados foram omitidos
   :

   @Override
   public final double getPotencia() {
      return taxa * potenciaMax;
   }
}

A anotação @Override

Existe uma anotação específica no Java, @Override, a qual indica que o método anotado (isto é, o método onde se aplica) sobrepõe outro existente em uma de suas superclasses. Quando a sobreposição é feita corretamente, esta anotação não produz qualquer efeito, servindo apenas como um lembrete no programa fonte. Mas caso o método anotado não substitua alguma operação, isto é, quando não ocorre a sobreposição, o compilador gera um erro indicando o problema.

Para aplicar a anotação, basta posicioná-la imediatamente antes da declaração do método anotado, com uma das formas que seguem:

@Override public int metodoAnotado() {
   // código do método
}

@Override
public int metodoAnotado() {
   // código do método
}

Embora seja opcional, seu emprego constitui uma boa prática de programação, como feito na class MotorControlavel, pois funciona como um comentário para o programador e garante que sobreposição pretendida, de fato, ocorra.

Regras para sobreposição de métodos

O uso da sobreposição de métodos (method overriding) pode ser vantajoso em muitas situações, mas existem limitações (ou regras) que devem ser observadas em sua aplicação.
  • Um método, em Java ou C#, só pode ser substituído numa subclasse, nunca na própria classe onde foi originalmente declarado.
  • A lista de argumentos deve ser exatamente a mesma (quantidade, tipo e sequência dos parâmetros) do método sobreposto, caso contrário teremos uma mera sobrecarga de método.
  • O tipo de retorno, embora não faça parte da assinatura, deve ser o mesmo daquele declarado no método original, sendo possível indicar uma subclasse do tipo de retorno.
  • Construtores não são herdados, portanto não podem ser sobrepostos.
  • Métodos declarados como final não podem ser sobrepostos.
  • O nível de acesso (visibilidade) não pode ser mais restritivo que o método sobreposto, ou seja, a visibilidade deve ser a mesma ou maior. A maior implicação disso é que métodos públicos não podem ser sobrepostos por versões protegidas ou privadas.
  • Métodos estáticos, de fato, não são sobrepostos, mas podem ser redeclarados.
  • Métodos privados, não são herdados e, assim, não são sobrepostos, mas podem ser redeclarados.
  • Os métodos sobrepostos podem lançar qualquer exceção não-monitoradas (unchecked exceptions), a despeito daquelas lançadas pelo método substituído.
  • Os métodos sobrepostos não podem lançar outras exceções monitoradas (checked exceptions), além daquelas lançadas pelo método substituído ou de suas subclasses. É possível que o método sobreposto lance menos exceções.
A sobreposição de métodos é um mecanismo útil que permite fornecer uma nova implementação para um método existente. Isto possibilita tanto modificar (e especializar) a realização de uma operação em uma subclasse, quanto suprir uma primeira implementação no caso de métodos abstratos, assunto dos próximos posts.

POO-P-20-Herança Simples POO-P-22-Herança Múltipla

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.