[Pesquisar este blog]

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.