C# - Herança ou Composição


  Neste artigo vou apresentar os principais atributos Data Annotations que podem    simplificar o tratamento de dados na sua aplicação

A herança ocorre quando uma classe (conhecida como classe derivada ou subclasse) herda os membros (campos, propriedades e métodos) de outra classe (classe base ou superclasse) e permite que a classe derivada reutilize o código da classe base e adicione ou modifique o comportamento conforme necessário.

Já a composição, por outro lado, ocorre quando uma classe contém instâncias de outras classes como membros, e, vez de herdar o comportamento, a classe composta  utiliza os membros da classe contida para obter o comportamento desejado. Assim, a composição é uma forma de relacionamento "tem-um", onde a classe contendo tem uma ou mais instâncias da classe contida.

A seguir temos um exemplo de herança usando as classes Carro e Motor:

class Motor
{
  
public void Ligar()
   {
     Console.WriteLine(
"Motor ligado.");
   }
}

class Carro : Motor
{
  
public void Dirigir()
   {
      Console.WriteLine(
"O carro está em movimento.");
   }
}

Nesse exemplo, a classe Carro herda da classe Motor e assim o Carro pode acessar os membros (métodos) da classe Motor, como o método Ligar(), além de ter seus próprios membros, como o método Dirigir().

Agora vejamos um exemplo de composição usando as classes Animal e Gato:

class Gato
{
  
private Animal animal;
  
public Gato()
   {
      animal =
new Animal();
   }

   public void Miar()
   {
     Console.WriteLine(
"O gato está miando.");
   }

   public void Comer()
   {
       animal.Comer();
   }
}

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

Nesse exemplo, a classe Gato possui uma instância da classe Animal como um membro privado. O Gato utiliza a composição para acessar o comportamento do Animal e o Gato possui seus próprios métodos, como Miar(), mas delega o comportamento de Comer() para a instância de Animal.

A principal diferença entre herança e composição é o tipo de relacionamento que é estabelecido. A herança estabelece um relacionamento "é-um", onde a classe derivada é uma extensão da classe base. No exemplo do Carro e Motor, o Carro é um tipo específico de Motor. A herança é útil quando existe uma relação de especialização, quando a classe derivada é uma versão mais específica ou especializada da classe base.

Por outro lado, a composição estabelece um relacionamento "tem-um", onde a classe contendo tem uma ou mais instâncias da classe contida para fornecer o comportamento desejado. No exemplo do Gato e Animal, o Gato possui um Animal. A composição é útil quando existe uma relação de composição, onde a classe contendo precisa de outros objetos para funcionar corretamente.

Quando e qual abordagem usar

Quanto à melhor abordagem a ser usada, a escolha entre herança e composição depende do contexto e do problema em questão.

Aqui estão algumas considerações:

  • Herança é adequada quando há uma relação clara de especialização entre as classes e quando a hierarquia de classes é estável. A herança oferece a vantagem de reutilizar o código da classe base e permite a extensão de comportamento através de substituição de métodos.

    No exemplo do Carro e Motor, se houver diferentes tipos de carros com comportamentos específicos, como CarroEsportivo ou CarroSedan, a herança pode ser uma abordagem adequada. O CarroEsportivo pode herdar da classe Carro e adicionar métodos e propriedades específicos para carros esportivos.
     
  • Composição é adequada quando a relação entre as classes é de composição ou quando é necessário um alto nível de flexibilidade. Ela permite a criação de objetos complexos combinando diferentes componentes. A composição favorece um acoplamento mais fraco entre classes, tornando o código mais modular e mais fácil de manter e estender.

    No exemplo do Gato e Animal, se houver diferentes tipos de animais que podem ser associados ao Gato, como um Animal de Estimação, a composição pode ser mais adequada. O Gato pode ter uma referência para um Animal de Estimação, permitindo a troca do tipo de animal sem alterar a estrutura do Gato.

A escolha entre herança e composição depende das necessidades específicas do projeto. Aqui estão algumas considerações para ajudar na escolha:

  1. Relação de especialização: Se há uma relação clara de especialização entre as classes, onde a classe derivada é uma versão mais específica ou especializada da classe base, a herança pode ser apropriada.
     
  2. Reutilização de código: Se há um conjunto significativo de código a ser compartilhado entre as classes, a herança permite reutilizar esse código da classe base.
     
  3. Flexibilidade e manutenção: Se é necessário um alto nível de flexibilidade e modularidade, onde os objetos podem ser compostos e reconfigurados de forma dinâmica, a composição pode ser preferível. Isso torna o código mais flexível para alterações futuras e facilita a manutenção.
     
  4. Acoplamento: A composição favorece um acoplamento mais fraco entre classes, o que significa que as mudanças em uma classe têm menos impacto nas outras. Se o acoplamento reduzido é uma consideração importante, a composição pode ser mais adequada.

Exemplo prático : Comparando Herança e Composição

Vamos criar um exemplo completo e prático para ilustrar o uso da herança e como podemos melhorar usando a composição.

Considere o cenário de um jogo de RPG, onde temos personagens com diferentes classes, como Guerreiro e Mago, e, cada classe tem suas habilidades específicas.

Vamos começar com a implementação usando herança:

// Classe base Personagem
class Personagem
{
    public string Nome { get; set; }
    public int Nivel { get; set; }

    public virtual void Atacar()
    {
        Console.WriteLine("Ataque básico!");
    }
}

// Classe derivada Guerreiro
class Guerreiro : Personagem
{
    public override void Atacar()
    {
        Console.WriteLine("Golpe de espada!");
    }

    public void UsarEscudo()
    {
        Console.WriteLine("Usando escudo para se defender!");
    }
}

// Classe derivada Mago
class Mago : Personagem
{
    public override void Atacar()
    {
        Console.WriteLine("Bola de fogo!");
    }

    public void UsarMagia()
    {
        Console.WriteLine("Usando magia poderosa!");
    }
}

Nesse exemplo, temos uma classe base chamada Personagem, que define propriedades comuns a todos os personagens, como nome e nível, e um método Atacar() que representa o ataque básico.

Em seguida, temos as classes derivadas Guerreiro e Mago. Cada classe sobrescreve o método Atacar() para implementar suas habilidades de ataque específicas. Além disso, a classe Guerreiro tem um método adicional UsarEscudo() e a classe Mago tem um método UsarMagia().

Agora, vamos mostrar como podemos melhorar esse exemplo usando composição:

// Classe Personagem com composição
class Personagem
{
    private IAtacante atacante;

    public string? Nome { get; set; }
    public int Nivel { get; set; }

    public Personagem(IAtacante atacante)
    {
        this.atacante = atacante;
    }

    public void Atacar()
    {
        atacante.Atacar();
    }
}

// Interface para atacantes
interface IAtacante
{
    void Atacar();
}

// Implementação da interface para Guerreiro
class GolpeDeEspada : IAtacante
{
    public void Atacar()
    {
        Console.WriteLine("Golpe de espada!");
    }
}

// Implementação da interface para Mago
class BolaDeFogo : IAtacante
{
    public void Atacar()
    {
        Console.WriteLine("Bola de fogo!");
    }
}

Nesse exemplo, a classe Personagem agora recebe uma instância de um objeto que implementa a interface IAtacante no construtor. Dessa forma, o comportamento de ataque é definido por uma implementação específica, que pode ser alterada dinamicamente.

A interface IAtacante define o método Atacar(), que é implementado pelas classes GolpeDeEspada e BolaDeFogo.

Agora, vamos criar instâncias dos personagens usando a herança e a composição:

1- Usando Herança:

// Uso da herança
Guerreiro guerreiro =
new Guerreiro();
guerreiro.Nome =
"Aragorn";
guerreiro.Nivel = 10;
guerreiro.Atacar();
// Saída: Golpe de espada!
guerreiro.UsarEscudo();
// Saída: Usando escudo para se defender!

Mago mago = new Mago();
mago.Nome =
"Gandalf";
mago.Nivel = 12;
mago.Atacar();
// Saída: Bola de fogo!
mago.UsarMagia();
// Saída: Usando magia poderosa!
 

2- Usando Composição

// Uso da composição
IAtacante golpeDeEspada =
new GolpeDeEspada();
Personagem personagemGuerreiro =
new Personagem(golpeDeEspada);
personagemGuerreiro.Nome =
"Aragorn";
personagemGuerreiro.Nivel = 10;
personagemGuerreiro.Atacar();
// Saída: Golpe de espada!

IAtacante bolaDeFogo =
new BolaDeFogo();
Personagem personagemMago =
new Personagem(bolaDeFogo);
personagemMago.Nome =
"Gandalf";
personagemMago.Nivel = 12;
personagemMago.Atacar();
// Saída: Bola de fogo!
 

Nesse exemplo, mostramos o uso da herança ao criar instâncias das classes Guerreiro e Mago. Cada classe possui suas próprias habilidades específicas de ataque e métodos adicionais.

Em seguida, mostramos o uso da composição ao criar instâncias das classes GolpeDeEspada e BolaDeFogo, que implementam a interface IAtacante. Essas instâncias são passadas para a classe Personagem no construtor, permitindo que o comportamento de ataque seja definido dinamicamente.

Considerações

Ambas as abordagens têm seus méritos, e a escolha entre herança e composição depende do cenário específico.

A herança é útil quando há uma relação de especialização clara entre as classes e quando o comportamento comum pode ser compartilhado através da hierarquia de classes. No entanto, a herança pode levar a uma hierarquia complexa e acoplamento forte entre as classes.

A composição, por outro lado, oferece maior flexibilidade e modularidade, permitindo que o comportamento seja definido de forma mais dinâmica e evitando uma hierarquia complexa. A composição favorece um acoplamento mais fraco entre as classes, tornando o código mais fácil de manter e estender.

Além disso, a composição também permite que diferentes objetos sejam combinados para criar comportamentos complexos, oferecendo maior flexibilidade na construção de objetos. Isso pode ser especialmente útil em cenários onde existem diferentes combinações de comportamentos ou quando é necessário alterar dinamicamente o comportamento de um objeto em tempo de execução.

Outra vantagem da composição é que ela evita o chamado "problema do diamante" (diamond problem) que ocorre quando há múltiplas classes derivadas que herdam de uma mesma classe base. Esse problema pode causar ambiguidade e conflito de membros herdados. Com a composição, cada objeto tem sua própria implementação dos comportamentos, eliminando a possibilidade de conflitos.

Além disso, a composição pode ajudar a facilitar a testabilidade e a manutenção do código, uma vez que as dependências são explicitamente injetadas através da interface, permitindo a substituição fácil de implementações e a criação de testes unitários mais isolados.

No entanto, a herança também tem suas vantagens. Uma delas é a capacidade de reutilizar facilmente o código existente na classe base, evitando a duplicação de código. Além disso, a herança pode ser útil quando há uma relação de "é-um" bem definida, onde a classe derivada é semanticamente uma extensão ou especialização da classe base.

A escolha entre herança e composição depende das necessidades do projeto. A herança é adequada quando há uma relação de especialização clara e um comportamento comum compartilhado, enquanto a composição é mais adequada quando há flexibilidade e modularidade desejadas, permitindo que o comportamento seja definido dinamicamente.

E estamos conversados.

"Esta é uma palavra fiel, e digna de toda a aceitação, que Cristo Jesus veio ao mundo, para salvar os pecadores, dos quais eu sou o principal."
1 Timóteo 1:15

Referências:


José Carlos Macoratti