Herança e métodos virtuais (Polimorfismo)

Supondo que quiséssemos promover novas implementações para métodos de uma classe base, na classe derivada, ou variações destes. Outra questão interessante oriunda do processo de herança é a compatibilidade de tipos de classe base de derivada. Algumas regras devem ser consideradas.

O tipo da classe base é compatível com o da classe derivada, mas somente podemos atribuir objetos do tipo derivada a um objeto do tipo base. O código abaixo exemplifica esse conceito:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
class Base
{
      ...
}
 
class Derivada: Base
{
      ...  
}
 
class MinhaClasse
{
      Static void Main()
      {    
            Base B = new Base();
            Derivada D = new Derivada()
            B = D; // Atribuição válida
            D = B; // Atribuição inválida
      }
}
...

Tendo isso claro, vejamos como se comportam métodos na atribuição de classe de tipos compatíveis. Um caso a ser resolvido é muito típico: é o de métodos com mesmo nome. Veja um exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;
 
public class Base
{
      public void EscreveNome()
      {
            Console.WriteLine("Base");
      }
}
 
public class Derivada1: Base
{
      public void EscreveNome()
      {
            Console.WriteLine("Derivada1");
      }
}
 
public class Derivada2: Base
{
      public void EscreveNome()
      {
            Console.WriteLine("Derivada2");
      }
}
public class MinhaClassePolimorfismo
{
      static void Main()
      {
            Derivada1 minhaDerivada1 = new Derivada1();
            Derivada2 minhaDerivada2 = new Derivada2();
            Base minhaBase;
            string valor;
            Console.WriteLine("Digite um valor");
            valor = Console.ReadLine();
 
            if (valor == "1")
                  minhaBase = minhaDerivada1;
            else
                  minhaBase = minhaDerivada2;
            minhaDerivada1.EscreveNome();
            minhaDerivada2.EscreveNome();
            minhaBase.EscreveNome();
      }
}

Compile e veja a saída no console:

O código compila, mas gera alguns avisos. Isso ocorre porque implementamos de maneira forçada método de mesmo nome da classe base na classe derivada. O jeito fácil de resolver o problema é preceder a declaração do método com o operados “new”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;
 
public class Base
{
      public void EscreveNome()
      {
            Console.WriteLine("Base");
      }
}
 
public class Derivada1: Base
{
      new public void EscreveNome()
      {
            Console.WriteLine("Derivada1");
      }
}
 
public class Derivada2: Base
{
      new public void EscreveNome()
      {
            Console.WriteLine("Derivada2");
      }
}
public class MinhaClassePolimorfismo
{
      static void Main()
      {
            Derivada1 minhaDerivada1 = new Derivada1();
            Derivada2 minhaDerivada2 = new Derivada2();
            Base minhaBase;
            string valor;
            Console.WriteLine("Digite um valor");
            valor = Console.ReadLine();
 
            if (valor == "1")
                  minhaBase = minhaDerivada1;
            else
                  minhaBase = minhaDerivada2;
            minhaDerivada1.EscreveNome();
            minhaDerivada2.EscreveNome();
            minhaBase.EscreveNome();
      }
}

Ao compilar os avisos não são emitidos:

Mas ainda temos um problema. Repare que tanto no primeiro resultado como neste último, apesar do valor informado ser 1, a última mensagem impressa se refere à classe base, apesar de termos atribuído à variável “minhaBase” a variável “minhaDerivada1”. Por que então não foi impressa a mensagem “Derivada1” ao invés de “Base”?

Tudo bem, classe base é base e ponto final, certo? Quase. A característica de polimorfismo nos permite “muitas formas” como já foi dito anteriormente. Através de um tipo ancestral, podemos obter diferentes comportamentos condicionalmente. Como fazer para que isso aconteça?

Para redefinir um método na classe derivada de forma que ele seja entendido por um objeto de classe base, estes métodos devem ser “virtuais”. A palavra chave “virtual” deve ser utilizada na declaração do método na classe base e a palavra chave “override” na declaração do método de mesmo nome da classe derivada. Veja a implementação de código a seguir:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
using System;
 
public class Base
{
      public virtual void EscreveNome()
      {
            Console.WriteLine("Base");
      }
}
 
public class Derivada1: Base
{
      public override void EscreveNome()
      {
            Console.WriteLine("Derivada1");
      }
}
 
public class Derivada2: Base
{
      <strong>public override void EscreveNome()</strong>
      {
            Console.WriteLine("Derivada2");
      }
}
public class MinhaClassePolimorfismo
{
      static void Main()
      {
            Derivada1 minhaDerivada1 = new Derivada1();
            Derivada2 minhaDerivada2 = new Derivada2();
            Base minhaBase;
            string valor;
            Console.WriteLine("Digite um valor");
            valor = Console.ReadLine();
 
            if (valor == "1")
                  minhaBase = minhaDerivada1;
            else
                  minhaBase = minhaDerivada2;
            minhaDerivada1.EscreveNome();
            minhaDerivada2.EscreveNome();
            minhaBase.EscreveNome();
      }
}

Veja a saída após compilado o programa:

Note que tornamos a saída condicional. Dependendo do valor digitado, a atribuição muda e o objeto de classe base entende a implementação do objeto de classe derivada. Sim, pois uma classe derivada também é uma classe base. Se fizermos uma analogia aos conceitos apresentados no início deste capítulo, poderemos chamar a classe base de “Animal” e as classes derivadas de “Cachorro” e “Gato”, por exemplo. Adaptando os conceitos aos exemplos, podemos dizer que o cachorro e gato são animais. Veja a o código implementado sob esta ótica:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System;
 
public class Animal
{
      public virtual void EscreveNome()
      {
            Console.WriteLine("Sou um Animal");
      }
}
 
public class Cachorro: Animal
{
      public override void EscreveNome()
      {
            Console.WriteLine("Sou um Cachorro");
      }
}
 
public class Gato: Animal
{
      public override void EscreveNome()
      {
            Console.WriteLine("Sou um Gato");
      }
}
 
public class minhaClassePolimorfismo
{
      static void Main()
      {
            Cachorro meuCachorro = new Cachorro();
            Gato meuGato = new Gato();
            Animal meuAnimal;
            string valor;
            Console.WriteLine("Digite um valor");
            valor = Console.ReadLine();
 
            if (valor == "1")
                  meuAnimal = meuCachorro;
            else
                  meuAnimal = meuGato;
            meuCachorro.EscreveNome();
            meuGato.EscreveNome();
            meuAnimal.EscreveNome();
      }
}

Veja a saída no console:

Este exemplo deixa mais claro o conceito de implementação, uso e entendimento de polimorfismo.

Veremos agora um uso mais prático de métodos virtuais. Uma das implementações sobrecarregadas no método Console.WriteLine recebe um object como parâmetro. Dessa forma, podemos passar qualquer objeto já que qualquer tipo herda de object e é automaticamente compatível com este. Quando se passa um object para este método, ele automaticamente faz uma chamada ao método “ToString” da classe object. O método “ToString” é declarado como “virtual” e portanto pode ser sobrescrito usando a palavra chave “override”. Veja um exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
 
class Polegadas
{
      decimal valorMetros;
 
      public Polegadas(decimal v)
      {
            valorMetros = v * 100/2.54M;
      }
 
      public void ConverteMetros(decimal v)
      {
            valorMetros = v * 100/2.54M;      
      }
 
      public override string ToString()
      {
            return "O valor em metros é: " + valorMetros.ToString();
      }
}
 
class MinhaClasse
{
      static void Main()
      {
            Polegadas p = new Polegadas(100M);
            Console.WriteLine(p.ToString());
      }
}

Veja a saída no console:

Se mesmo com uma nova implementação (“override”) quisermos ter acesso ao método da classe base, é possível faze-lo através da palavra reservada “base”, porém, seu uso é diferenciado dos construtores. Veja um exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
using System;
 
public class Animal
{
      public virtual void EscreveNome()
      {
            Console.WriteLine("Sou um Animal");
      }
}
 
public class Cachorro: Animal
{
      public override void EscreveNome()
      {
            Console.WriteLine("Sou um Cachorro");
      }
}
 
public class Gato: Animal
{
      // redefine o método EscreveNome
      public override void EscreveNome()
      {
            Console.WriteLine("Sou um Gato");
            //chama o método EscreveNome da classe base
            Base.EscreveNome();
      }
}

Herança com construtores

Como já dito, no processo de herança somente o construtor sem parâmetros é herdado. O código de exemplo mostra o processo de uso dos construtores:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using System;
 
public class Base
{
      public Base()
      {
            Console.WriteLine("Estou no construtor da classe base");
      }
}
 
//declara a classe derivada herdando a classe base
public class Derivada: Base
{
      public Derivada()
      {
            Console.WriteLine("Estou no construtor da classe derivada");
      }
}
 
public class MinhaClasse
{
      static void Main()
      {
            // objeto da classe base executando o construtor
            Base minhaBase = new Base();
            // objeto da classe derivada executando o construtor
            Derivada minhaDerivada = new Derivada();
 
      }
}

Compile e veja a saída no console:

Note que o construtor sem parâmetros da classe base foi automaticamente herdado. O 1º texto se refere à criação do objeto “minhaBase” e os seguintes se referem à criação do objeto “minhaDerivada”. No momento da criação do objeto “minhaDerivada”, foram executados o construtor da classe base e o construtor da classe derivada. Se quisermos herdar os outros construtores, devemos criar um equivalente na classe derivada e evocar o construtor da classe base com a palavra chave “base” passando os parâmetros necessários. O código abaixo exemplifica a chamada de um construtor de classe base através da classe derivada:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
 
public class Base
{
      public Base()
      {
            Console.WriteLine("Estou no 1º construtor da classe base");
      }
      public Base(int valor)
      {
            Console.WriteLine("Estou no 2º construtor da classe base");
      }
}
 
public class Derivada: Base
{
      // declara o construtor da classe derivada herdando o construtor da classe base
      public Derivada(): base()
      {
            Console.WriteLine("Estou no 1º construtor da classe derivada");
      }
     // declara o construtor da classe derivada herdando o construtor da classe base. Como o construtor da classe base exige um parâmetro, este é passado no pelo lista de parâmetros do próprio contrutor
      public Derivada(int valor): base(valor)
      {
            Console.WriteLine("Estou no 2º construtor da classe derivada");
      }
}
 
public class MinhaClasse
{
      static void Main()
      {
            // objeto da classe base executando o 1º construtor
            Base minhaBase1 = new Base();
            // objeto da classe base executando o 2º construtor
            Base minhaBase2 = new Base(1);
            // objeto da classe derivada executando o 1º construtor
            Derivada minhaDerivada1 = new Derivada();
            // objeto da classe derivada executando o 2º construtor
            Derivada minhaDerivada2 = new Derivada(1);
      }
}

Compile e veja a saída no console:

As duas primeiras saídas se referem à criação do objeto “minhaBase1” e “minhaBase2”, as duas seguintes se referem à criação do objeto “minhaDerivada1”, e as duas últimas, ao objeto “minhaDerivada2”.

Herança em C#

O mecanismo de herança do .NET obedece aos critérios definidos nos conceitos de herança já citados e não prevê herança múltipla de classes, processo contornado pelas interfaces. Para herdar classes em C#, deve-se utilizar o símbolo ”:” após a declaração da classe, por exemplo:

1
2
3
4
5
6
7
8
9
10
11
12
...
class Base
{
      // ...
}
 
// Declara e herda a classe Base
class Derivada: Base
{
      // ...
}
...

Com esse recurso, praticamente todos os membros da classe base passarão a fazer parte da classe derivada. Existem algumas ressalvas no processo de herança que devem ser consideradas:

  • membros private não são herdados;
  • somente o construtor sem parâmetros (se houver) é herdado;
  • todas as classes derivam de object, portanto, a classe herdada e a derivada conterão os membros de object;
  • não é permitido eliminar uma funcionalidade já existente, mas é possível redefini-la;
  • para redefinir métodos de classes bases, estes devem ser virtuais;

Polimorfismo

Este é um dos conceitos mais complexos da POO.  Ele é conseqüência do processo de herança que teve que resolver vários problemas dado seu nível de abrangência.

Polimorfismo significa “várias formas” ou “multi-forma”. Pode-se entender este processo como a habilidade de uma classe adquirir várias formas em vários objetos, ou, de se comportar de maneira diferente dependendo de em que objeto ela atua.

A princípio é um pouco difícil de entender, mas vamos tentar interpretar de maneira simples. Imagine a classe veículo comentada no início deste capítulo. Todo veículo deve ter o método “movimentar” ou andar só pelo fato de ser um veículo. Também deve implementar um método para possibilitar a sua parada, que neste caso, poderia ser chamado de “parar”.

Até aqui estes métodos aplicados à classe veículo estão claros e concisos à aplicação da classe. Porém, quando quisermos construir uma classe carro que herdará a classe veículo, esses métodos se aplicarão, mas “movimentar” não faz muito sentido para um objeto carro. O melhor neste caso seria acelerar, que junto a outros métodos provocariam o “movimento” do carro. A mesma idéia poderia ser aplicada ao método parar, que para o carro, seria melhor “brecar” ou “frear”.

Mas não é só de nomes que estamos falando. Mesmo que na classe carro este método se chamasse “movimentar”, ele teria uma conotação bem diferente do que o “movimentar” da classe veículo, que deve ser genérico para se aplicar a todas as subclasses que a herdarão.

Ainda pode parecer confuso, mas tente agora imaginar uma outra classe que poderia herdar a classe veículo, como por exemplo, a classe bicicleta. Uma bicicleta é um veículo, e como tal, deve possuir as características deste, bem como seus métodos como o “movimentar” e o “parar”. Assim como para a classe carro, o método movimentar na classe bicicleta deverá sofrer algumas adaptações para que funcione como se fosse um método “pedalar”, e este sim provocaria movimento no veículo bicicleta. Ainda assim, o problema não são os nomes, mas sim suas implementações. Vamos confrontar as classes agora:

Polimorfismo

Os membros “Movimentar” e “Parar” seriam perfeitamente herdados, mas para seu correto funcionamento, algumas adaptações deverão ser feitas. O resultado seria algo como:

Polimorfismo

Ambas as classes possuem suas definições corretas para os membros da classe base. Esta então deve prever uma implementação genérica para que as classes derivadas sejam obrigadas a definir, adaptar ou utilizar esses métodos como se fosse um preço que se paga por querer tirar proveito de suas características. Sabemos que os membros “Pedalar” e “Acelerar” são incompatíveis entre si, tanto pelo motivo de pertencer a classes distintas, como na sua estrutura, mas são compatíveis com o membro “Movimentar” da classe base, que por sua vez serviu como base para sua implementação.

Podemos generalizar que, mesmo sendo classes distintas, bicicleta e carro podem ser considerados e “referenciados” como sendo objeto do tipo “Veículo”.

Mais adiante isso ficará claro com a utilização de código.

Herança Múltipla

Herança múltipla é outro importante conceito a ser abordado na POO. Ele consiste em que uma classe pode ter não apenas uma classe base, mas duas ou mais. Nesse processo, todas as características de todas as classes bases são passadas para a classe derivada. O processo é ilustrado na figura a seguir:

Classes Utilizando Herança Múltipla

Uma pergunta que deve ter surgido pela interpretação da figura acima, é:

Os membros “Nome do cliente”, “nº da conta” e “Saldo” estão nas duas classes bases. Então, de qual delas esses membros são herdados?

É uma pergunta muito pertinente. Nas linguagens que implementam herança múltipla como o C++, existe um processo chamado “Ambigüidade em herança múltipla”, que trata desse tipo de problema.

Para os propósitos deste curso, não será abordado este processo, já que as linguagens a serem estudadas não suportam herança múltipla face aos problemas por elas causados. É claro que a herança múltipla nos oferece uma série de recursos, mas nas novas linguagens, isso é contornado de maneira simples e inteligente.

É aconselhável que os programadores ao menos saibam da existência deste conceito, pois se trata de um dos pilares a POO.

A dupla Encapsulamento X Herança

Após ter entendido o processo de herança de classes, será mais fácil entender algumas vantagens propostas pelo Encapsulamento, recurso explicado anteriormente. Quando se herda uma classe, espera-se que esta esteja em seu mais perfeito funcionamento. Portanto, se por algum motivo houver algum erro durante a criação ou utilização deste novo objeto ou subclasse, muito provavelmente este erro está contido na nova ou nas novas implementações. Isso geralmente é esperado. Mas, e quando isso não acontece? E quando o erro não estiver na nova implementação?

A resposta está em como o conteúdo da classe base está encapsulado, ou seja, não temos acesso a ele, o erro está em seu processo ou concepção. O mais provável é que alguma situação não tenha sido prevista, por exemplo, um envio de mensagens mal feito, ou o não término de um determinado processo, bem como a falta tratamento de erro não previsto. Como uma classe tem que ser o mais auto-sustentável possível, as correções deverão ser feitas na classe base e não nas classes derivadas.

O que está acontecendo, na verdade, é que além de encapsularmos o conteúdo restrito da classe base, estamos também encapsulando todos os possíveis problemas que esta possa causar, fazendo com que as correções sejam mais fáceis de identificar e tratar.

O processo de Herança cuidará para que as correções sejam implementadas nas demais classes derivadas desta.

Criação das classes utilizando herança

Observe a imagem abaixo:

Classes Utilizando Herança

As classes “Conta Poupança” e “Conta Especial” possuem, automaticamente, todos os membros da classe “Conta Simples”, ou seja, “Nome do cliente”, “nº da conta” e “Saldo”, pois herdaram suas características. Não é necessário redefini-los nas novas classes, apenas criam-se os novos membros como “Taxa” e “Limite” nos casos acima.

Não só características podem ser herdadas, mas também os métodos. Nas classes do tipo “Conta”, algumas ações comuns seriam “sacar” e “depositar”. Estas seriam definidas na classe “Conta Simples” para serem herdadas para as demais classes. Em termos de familiarização, chamamos a classe que será herdada como classe-base ou superclasse, e a classe que herda como classe-derivada ou subclasse.

Classes derivadas também podem servir de base para outras classes. Não há limite para derivação de classes.

Existe um ditado que diz, quando herdamos uma classe, ela deve corresponder ao relacionamento “é um” ou “é uma”, ou seja, Conta Especial, é uma Conta Simples assim como um carro é um veículo.

Alguns pontos devem ser considerados quando utilizamos o processo de herança, como é o caso de encapsulamento. Certamente queremos proteger os membros que promovem o funcionamento de um objeto, mas é necessário herdá-los para que as subclasses funcionem corretamente. Estes temas serão abordados em exemplos práticos mais adiante.

Herança

Este pode ser considerado o conceito mais importante da POO, pois é ele quem possibilita obter a maior parte das vantagens oferecidas por esta.

Herança é o processo de reaproveitamento das características com base em um ancestral dos quais estas querem se aproveitar.

Pela palavra em si, já podemos admitir que este conceito nos leva a uma idéia de aproveitamento, e é isso mesmo que deve ser considerado. Podemos assimilar a situação em que o filho herda bens do pai, mas para termos de estudo da POO, vamos assumir uma herança mais “biológica”.

Imaginemos que o filho herda os traços do pai, ou seja, cor dos cabelos, da pele, dos olhos e outras não só estéticas, mas comportamentais como o caráter, temperamento e outras coisas. Devemos considerar também que nem sempre tudo é herdado. Além do mais, não é só do pai que a herança é feita, mas da mãe também. É possível também que algumas outras sejam herdadas dos avôs ou ancestrais mais distantes, dos quais os pais herdaram tais características.

Em resumo, este conceito nos leva ao reaproveitamento de características de nossas classes. Assumindo o caso acima, para criar um filho, precisamos de um pai, que precisa de outro pai, que precisa de outro, enfim. Obviamente não vamos chegar ao infinito, mas precisamos admitir que a construção de um filho, torna-se bem mais simples com a existência de um pai.

Voltando agora ao conceito de classes, tomemos o exemplo do carro. Se construíssemos o carro do zero, seria algo extremamente penoso, pois teríamos que conceituar todo o seu funcionamento, desde o motor, toda a parte elétrica e por aí vai. Mas obviamente não faríamos isso, mas sim aproveitaríamos muito do que já foi feito, por exemplo, o próprio motor, todo o câmbio, toda a parte elétrica, e mudaríamos apenas aquilo que nos fosse de interesse. Estamos então herdando vários conceitos de certa classe e reaproveitando-os para construir uma nova, implementando novas, e até alterando algumas de suas características. Isso significa uma economia de trabalho significativa, dependendo do que se quer construir ou herdar.

Vamos tentar exemplificar um caso real. Suponhamos que na construção de um sistema bancário tivéssemos que construir um cadastro de clientes e este cadastro fosse dos seguintes tipos:

  • Conta Simples
  • Conta Especial
  • Conta Poupança

Cada um destes tem as seguintes características:

  • Conta Simples
    • Nome do cliente
    • nº da conta
    • Saldo
  • Conta Especial
    • Nome do cliente
    • nº da conta
    • Saldo
    • Limite
  • Conta Poupança
    • Nome do cliente
    • nº da conta
    • Saldo
    • Taxa

Note que para os casos acima, todos têm os membros “Nome do cliente”, “nº da conta” e “Saldo” em comum. Não haveria motivo para tanta redundância. É aí que entra o conceito de herança. Poderíamos então tomar a classe Conta Simples que possui os três membros comuns a todas as outras, como sendo a classe Base, e, simplesmente “herdá-la”, implementando somente as modificações necessárias. O desenho abaixo demonstra as duas situações: