Em nosso banco os clientes podem ter dois tipos de conta até o momento: conta corrente ou conta poupança. Para instanciar estes tipos de conta, poderíamos usar o seguinte código:
//Instanciar uma nova conta corrente
ContaCorrente contaCorrente = new ContaCorrente();
//Instancia uma nova conta poupança
ContaPoupanca contaPoupanca = new ContaPoupanca();
Nos capítulos anteriores, aprendemos que essas duas classes têm muito em comum, ambas são contas. Não apenas têm atributos em comum, mas também comportamentos. Para evitar a repetição do código em ambas as classes, vimos como isolar este código repetido em uma classe Conta
, e fazer com que as classes ContaCorrente
e ContaPoupanca
herdem dessa classe mãe todo o código em comum.
Além da reutilização de código, também vimos a possibilidade de escrever métodos que podem receber argumentos tanto do tipo ContaCorrente
quanto do tipo ContaPoupanca
, utilizando polimorfismo. Basta fazermos os métodos referenciarem o tipo mais genérico, no caso, Conta
:
public class TotalizadorDeContas
{
// ...
public void Soma(Conta conta)
{
// ...
}
}
Mas o que acontece quando executamos a seguinte linha:
Conta c = new Conta();
Criamos um novo objeto do tipo Conta
. Mas esse objeto faz algum sentido para nossas regras de negócio? É uma conta genérica, não sendo nem conta corrente e nem poupança.
Neste caso, não deveríamos permitir a instanciação de objetos Conta
.
Conta
é apenas uma ideia em nosso domínio, uma forma genérica de referenciarmos os dois tipos de conta que realmente existem em nosso sistema, ContaCorrente
e ContaPoupanca
. Podemos evitar a criação de objetos do tipo Conta
definindo a classe como abstrata:
public abstract class Conta
{
// ...
}
Desta forma, não podemos mais criar objetos do tipo Conta
, mas podemos ainda usar variáveis do tipo conta, para referenciar objetos de outros tipos:
Conta conta = new Conta(); //não compila, não pode criar objetos abstratos
Conta cc = new ContaCorrente(); // pode, objeto é do tipo ContaCorrente
Conta cp = new ContaPoupanca(); // pode, objeto é do tipo ContaPoupanca
Repare que o calculo necessário para realizar um saque é diferente em cada um dos tipos de Conta
. Sabemos que uma conta deve ter um método Saca
, mas a implementação deste método depende de regras específicas de cada tipo diferente de conta em nosso sistema. Uma solução possível seria implementá-lo sem fazer nada, mas dizendo que ele pode ser sobrescrito (virtual
):
public abstract class Conta
{
public virtual void Saca(double valor){
//não faz nada
}
// ...
}
E manter o código Saca
original nas classes filhas, dizendo que eles sobrescrevem (override
) o método na classe pai:
public class ContaCorrente : Conta
{
public override void Saca(double valor)
{
this.Saldo -= (valor + 0.10);
}
// ...
}
public class ContaPoupanca : Conta
{
public override void Saca(double valor)
{
this.Saldo -= valor;
}
// ...
}
Desejamos que toda classe filha implemente sua própria versão do método, com o comportamento referente ao tipo da conta. Mas se esquecermos de sobrescrever o método Saca
em uma subclasse, o método herdado da classe Conta
será executado, que não faz nada. Não queremos isso! Queremos obrigar as classes filha a implementar o método Saca
.
Podemos obrigar todas as classes filhas a sobrescreverem um método na classe mãe, basta declarar esse método com o modificador abstract
ao invés de virtual
. Toda vez que marcamos um método com o modificador abstract
, ele obrigatoriamente não pode ter uma implementação padrão:
public abstract class Conta // marcando que a classe está incompleta
{
public abstract void Saca(double valor); // marcando que o método está incompleto
}
Com essa modificação, o método Saca
passa a representar apenas uma ideia, que precisa de uma implementação concreta nas classes filhas. Caso não implementemos esse método na classe filha, o compilador emitirá um erro, avisando da obrigatoriedade de sobrescrever este método. Então se implementássemos, por exemplo, a classe ContaPoupanca
sem definir a implementação do Saca
, o código da classe não compilará:
public class ContaPoupanca : Conta
{
// Essa classe não compila pois não colocamos a implementação para o Saca
}
Podemos ter uma classe abstrata sem nenhum método abstrato, mas não o contrário. Se a classe tem pelo menos um método abstrato, ela deve ser abstrata também pois como o método está incompleto, a classe não está completa.
Exercícios
- Transforme a classe
Conta
em uma classe abstrata. Repare que agora teremos um erro de compilação em todos os pontos do código em que tentamos instanciar o tipoConta
. Por quê? Modifique o código da sua aplicação para que a conta não seja instanciada, assim corrigiremos os erros de compilação. Não se esqueça de sempre testar o seu código. - Repare que herdamos os métodos
Saca
eDeposita
da classeConta
, porém cada tipo deConta
sobrescreve esses métodos, logo eles são bons candidatos para métodos abstratos. Transforme os métodosSaca
eDeposita
em métodos abstratos, repare que com isso todas as classes filhas são obrigadas a dar uma implementação para esses métodos.