DEV Community

Yuri Peixinho
Yuri Peixinho

Posted on

POO: Polimorfismo

Introdução

Imagina que você tá fazendo um jogo com vários tipos de monstro, mas ainda sem polimorfismo. Cada monstro é só um "tipo" e uma função central decide o que cada um faz:

void Atacar(string tipoMonstro)
{
    if (tipoMonstro == "Dragao")
        Console.WriteLine("🔥 Cospe uma bola de fogo!");
    else if (tipoMonstro == "Fantasma")
        Console.WriteLine("👻 Atravessa a parede e assusta!");
    else if (tipoMonstro == "Slime")
        Console.WriteLine("🟢 Pula e gruda em você!");
    // toda vez que você inventa um monstro novo, tem que vir AQUI e adicionar mais um "else if"
}
Enter fullscreen mode Exit fullscreen mode

Isso funciona, mas é uma bomba-relógio: essa função vai crescer pra sempre, e todo mundo que mexe no jogo precisa saber que ela existe e lembrar de atualizá-la. É frágil e cansativo.

A ideia central do polimorfismo

Em vez de uma função central que pergunta "que tipo é você?", cada monstro já sabe se comportar sozinho. Você não pergunta, você só manda:

**abstract** class Monstro
{
    public string Nome { get; }
    public int Vida { get; protected set; } = 100;

    protected Monstro(string nome) => Nome = nome;

    public abstract void Atacar();

    public virtual void TomarDano(int dano)
    {
        Vida -= dano;
        Console.WriteLine($"{Nome} perdeu {dano} de vida (restam {Vida}).");
    }
}
Enter fullscreen mode Exit fullscreen mode

E cada monstro implementa sua própria versão:

class Dragao : Monstro
{
    public Dragao() : base("Dragão") { }
    public override void Atacar() => Console.WriteLine($"{Nome} 🔥 cospe uma bola de fogo!");
}

class Fantasma : Monstro
{
    public Fantasma() : base("Fantasma") { }
    public override void Atacar() => Console.WriteLine($"{Nome} 👻 atravessa a parede e assusta!");

    public override void TomarDano(int dano)
    {
        int danoReal = dano / 2; // é intangível, sofre metade do dano
        Console.WriteLine($"{Nome} é intangível — dano reduzido pela metade!");
        base.TomarDano(danoReal);
    }
}

class Slime : Monstro
{
    public Slime() : base("Slime") { }
    public override void Atacar() => Console.WriteLine($"{Nome} 🟢 pula e gruda em você!");
}

class Golem : Monstro
{
    public Golem() : base("Golem") { Vida = 200; } // é um tanque, vida maior
    public override void Atacar() => Console.WriteLine($"{Nome} 🪨 esmaga o chão com o punho!");
}
Enter fullscreen mode Exit fullscreen mode

Repara que Fantasma não só ataca diferente — ele reage a dano diferente também, porque sobrescreveu TomarDano. Cada classe carrega sua própria lógica, não só um texto diferente.

Colocando pra rodar: a batalha completa

List<Monstro> inimigos = new() { new Dragao(), new Fantasma(), new Slime(), new Golem() };

foreach (var inimigo in inimigos)
{
    inimigo.Atacar();
    inimigo.TomarDano(30);
    Console.WriteLine();
}
Enter fullscreen mode Exit fullscreen mode
Saída:

Dragão 🔥 cospe uma bola de fogo!
Dragão perdeu 30 de vida (restam 70).

Fantasma 👻 atravessa a parede e assusta!
Fantasma é intangível  dano reduzido pela metade!
Fantasma perdeu 15 de vida (restam 85).

Slime 🟢 pula e gruda em você!
Slime perdeu 30 de vida (restam 70).

Golem 🪨 esmaga o chão com o punho!
Golem perdeu 30 de vida (restam 170).
Enter fullscreen mode Exit fullscreen mode

Mesmo código (inimigo.Atacar(), inimigo.TomarDano(30)) rodando pra quatro tipos diferentes, cada um reagindo do seu próprio jeito. Zero if, zero switch.

O verdadeiro poder: extensibilidade

Quer adicionar um Vampiro que rouba vida ao atacar? Você cria a classe...

class Vampiro : Monstro
{
    public Vampiro() : base("Vampiro") { }
    public override void Atacar() => Console.WriteLine($"{Nome} 🧛 morde e rouba sua vida!");
}
Enter fullscreen mode Exit fullscreen mode

...e pronto. Não precisa tocar em nenhuma linha da lista, do foreach, ou de qualquer código que já existia. Isso é o Princípio Aberto/Fechado (aquele "O" do SOLID) na prática: o sistema está aberto pra crescer, mas fechado pra você precisar reabrir e editar o que já funciona.

Os dois tipos de polimorfismo (pra fechar com chave de ouro)

Tudo que fizemos até aqui é polimorfismo em tempo de execução (via override) — o mais importante e o que todo mundo quer dizer quando fala "polimorfismo" em POO. Mas existe um segundo tipo, mais simples, decidido em tempo de compilação: sobrecarga de métodos.

class Curandeiro
{
    public void Curar(Monstro alvo) => alvo.TomarDano(-20); // cura um
    public void Curar(List<Monstro> aliados) 
    {
        foreach (var a in aliados) a.TomarDano(-20); // cura vários
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui o compilador escolhe qual Curar chamar baseado nos argumentos que você passa — antes mesmo do programa rodar. Não tem nada de dinâmico, é só "mesmo nome, assinaturas diferentes".

Como o C# sabe qual versão rodar

A parte que parece mágica: repare que a lista é List<Monstro>, não List<Dragao> ou qualquer coisa específica.

Monstro m = new Fantasma();
m.Atacar(); // roda a versão do Fantasma, mesmo "m" sendo do tipo Monstro
Enter fullscreen mode Exit fullscreen mode

O C# decide qual Atacar() rodar olhando pro tipo real do objeto guardado na memória — não pro tipo da variável que você usou pra referenciar ele. É por isso que o foreach funciona perfeitamente numa lista cheia de tipos misturados: em cada volta, o objeto "lembra" quem ele realmente é.

Top comments (0)