CodeGym /Cursos /C# SELF /Problemas com polimorfismo e abstração

Problemas com polimorfismo e abstração

C# SELF
Nível 25 , Lição 3
Disponível

1. Polimorfismo nem sempre é mágica

Se você olhar só pros exemplos básicos, polimorfismo em C# parece a solução perfeita: herda, sobrescreve, chama tudo pelo tipo base — e pronto, tudo funciona. Mas na prática aparecem uns detalhes. Bora olhar mais de perto.

Escondendo métodos e a palavra-chave new

Imagina que a gente tem uma classe base e uma derivada, cada uma com um método de mesmo nome, mas sem a palavra-chave override. Se na classe derivada esse método for definido de novo, mas sem override, ele só esconde a implementação do método base, não sobrescreve. O compilador, tipo um pai cuidadoso, já vai reclamar com um aviso e sugerir pra você colocar new explicitamente:

class Animal
{
    public void Speak()
    {
        Console.WriteLine("O animal faz um som.");
    }
}

class Cat : Animal
{
    public new void Speak()
    {
        Console.WriteLine("Miau!");
    }
}

// Uso
Animal animal = new Cat();
animal.Speak(); // Vai mostrar: "O animal faz um som."

Uau! Mesmo se a gente cria um objeto do tipo Cat e coloca ele numa variável do tipo Animal, vai chamar o método original da classe base. Por quê? Porque o método não foi declarado como virtual! Primeira armadilha: se quiser usar polimorfismo, não esquece das palavras-chave virtual e override. Use new só se você realmente quiser esconder, e não sobrescrever o método (isso, aliás, é bem raro e só por um bom motivo).

Chamadas de construtores e polimorfismo

Outra pegadinha: construtores não são virtuais. Se você declarar um construtor na classe base e outro na derivada, eles não vão ser polimórficos. Olha só:

class Animal
{
    public Animal()
    {
        Console.WriteLine("Construtor Animal");
    }
}

class Cat : Animal
{
    public Cat()
    {
        Console.WriteLine("Construtor Cat");
    }
}

// Uso
Animal animal = new Cat();
// Vai mostrar:
// Construtor Animal
// Construtor Cat

Mas se você chamar métodos do construtor da classe base que podem ser sobrescritos na derivada, o resultado pode ser estranho — o método virtual vai ser chamado antes da inicialização do filho! Por isso, não chame métodos virtuais/abstratos em construtores.

Problema de "quebrar" a encapsulação com override

Métodos virtuais são legais, mas se você na classe base pensou que um método ia se comportar de um jeito certinho, e depois esse método é sobrescrito na filha e quebra a lógica — podem aparecer bugs inesperados.

class Animal
{
    public virtual void Eat()
    {
        Console.WriteLine("O animal está comendo.");
    }

    public void Live()
    {
        Eat(); // Pode chamar qualquer versão sobrescrita!
    }
}

class Cat : Animal
{
    public override void Eat()
    {
        Console.WriteLine("O gato está comendo peixe.");
    }
}

Animal a = new Cat();
a.Live(); // Vai mostrar "O gato está comendo peixe."

Se na classe base Animal o método Eat() mostrava "o animal está comendo", e depois na derivada você colocou algo perigoso no Eat() sobrescrito, isso pode quebrar o funcionamento da classe toda. Esse problema é chamado de violação do princípio de substituição de Liskov (Liskov Substitution Principle, LSP). Sempre pense se o comportamento das classes filhas vai continuar fazendo sentido em relação à base.

Casting e problemas de conversão de tipos

Polimorfismo deixa você guardar vários objetos diferentes numa "caixa só": tipo uma lista do tipo base, onde pode ter cachorro, gato (os dois herdam de Animal). Mas se você quiser chamar algo específico:

List<Animal> pets = new List<Animal> { new Cat(), new Dog() };

foreach (var pet in pets)
{
    if (pet is Cat cat)
    {
        cat.Purr();
    }
}

Se esquecer de checar o tipo e fizer um cast sem cuidado, vai tomar um erro chato InvalidCastException. Às vezes isso vira um monte de checagens de tipo, deixa o código feio e mostra que talvez seu design precise de ajustes.

2. Problemas com abstração

Abstração é ótima pra facilitar a vida de quem usa o objeto e limitar o acesso ao que tá dentro. Mas também tem suas armadilhas!

Exagerando nos níveis de abstração (Over-Abstraction)

Alguns devs iniciantes (e até experientes) ficam tão empolgados com "OOP certo" que criam verdadeiros bolos de camadas com classes base, interfaces e níveis abstratos. No fim, nem o autor entende como funciona.

interface IAnimal
{
    void Speak();
}

abstract class Feline : IAnimal
{
    public abstract void Speak();
}

class Cat : Feline
{
    public override void Speak()
    {
        Console.WriteLine("Miau!");
    }
}

Tipo, pra quê esse intermediário abstrato se ele não faz nada? Abstração só por abstração deixa tudo difícil de manter e confunde a arquitetura.

Hierarquia mal pensada

Pensa o que acontece se você coloca uma ação lá no topo da hierarquia, mas ela não serve pra todo mundo:

abstract class Animal
{
    public abstract void Fly();
}
class Cat : Animal
{
    public override void Fly()
    {
        throw new NotImplementedException("Gatos não voam!");
    }
}

Aí você tem que encher as classes filhas de métodos que só jogam exceção, ou aceitar que sua interface não reflete a realidade. Isso é o anti-padrão clássico de "hierarquia errada". Nesses casos, melhor jogar esses métodos em interfaces separadas (tipo IFlyable).

Dica: não tente criar uma abstração que sirva pra tudo.

Problemas com classes abstratas e mudanças no API

Assim que uma classe abstrata vai pra produção e começa a ser herdada, qualquer mudança vira um risco. Se você adicionar um novo método abstrato, todos os filhos vão ter que implementar — senão o código nem compila. Isso complica muito a manutenção de libs e APIs públicas.

É pra isso que existem interfaces com implementação padrão (Default Interface Methods — vê a aula 116): elas deixam você expandir interfaces sem ter que mudar todo o código já existente.

Quebrando encapsulamento na abstração

Quando você faz uma classe abstrata, geralmente precisa declarar membros como protected pra que as filhas possam acessar. Isso muitas vezes vaza lógica interna que era melhor esconder. No fim, as filhas têm acesso a dados e operações que podem quebrar a integridade da classe base.

3. Cenários práticos de erros

Pra você não achar que isso só acontece em exercício de casa, bora ver exemplos reais — até dev experiente tropeça nessas coisas.

Exemplo com métodos não declarados como virtual

Imagina que a gente tá melhorando nosso app de logging (vê o Dia 24). Temos um logger base:

class BaseLogger
{
    public void Log(string message)
    {
        Console.WriteLine(message);
    }
}

class FileLogger : BaseLogger
{
    public void Log(string message)
    {
        // Escreve no arquivo
        Console.WriteLine("No arquivo: " + message);
    }
}

// Uso:
BaseLogger logger = new FileLogger();
logger.Log("Hello!"); // Esperado: "No arquivo: Hello!", real: "Hello!"

O dev do FileLogger achou que sobrescreveu o método, mas esqueceu de colocar override e não fez o método base virtual. Resultado: chama a versão base.

Recomendação: sempre marque métodos que podem ser sobrescritos como virtual na base e override nas derivadas.

Exemplo de abstração errada: Animais "flexíveis"

Continuando com animais! Vamos criar uma interface IFlyable pra não obrigar todo animal a implementar Fly:

interface IFlyable
{
    void Fly();
}

class Bird : IFlyable
{
    public void Fly() => Console.WriteLine("O pássaro está voando!");
}

class Cat
{
    // O gato não implementa IFlyable
}

Agora dá pra fazer uma função que só lida com "voadores", sem mexer com gatos:

void MakeItFly(object creature)
{
    if (creature is IFlyable flyingThing)
    {
        flyingThing.Fly();
    }
    else
    {
        Console.WriteLine("Esse bicho não sabe voar.");
    }
}

Assim você não estraga a arquitetura com métodos abstratos inúteis.

Problemas com classes abstratas "duras" na extensibilidade

Imagina que você lançou uma lib com essa classe abstrata:

public abstract class Creature
{
    public abstract void DoAction();
}

A galera começou a criar classes herdando dela. Um ano depois, você quer expandir o API e adiciona:

public abstract class Creature
{
    public abstract void DoAction();
    public abstract void Sleep(); // Novo método!
}

Agora todas as classes dos usuários não compilam, porque têm que implementar o novo método abstrato. Por isso, cuidado ao desenhar abstrações e prefira interfaces com métodos padrão quando possível.

4. Dicas pra evitar as armadilhas clássicas

Deixe seus apps flexíveis como ginastas, mas não quebre as pernas!

  • Não exagere na herança: se dá pra usar composição (um objeto dentro do outro), prefira isso.
  • Só faça métodos virtuais se realmente precisar sobrescrever.
  • Não crie classes abstratas inúteis nem hierarquias "pra crescer depois".
  • Cheque se a lógica continua certa ao sobrescrever métodos: não quebre as regras da classe base.
  • Não adicione novos métodos abstratos em classes base e interfaces públicas depois que a lib já foi lançada.
  • Use interfaces pra deixar as partes do programa desacopladas.
  • Pra expandir APIs, use Default Interface Methods.
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION