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
Aqui estão algumas considerações:
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:
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() // Classe derivada Guerreiro public void UsarEscudo() // Classe derivada Mago public void UsarMagia() |
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
Personagem(IAtacante atacante) public void
Atacar() // Interface
para atacantes //
Implementação da interface para Guerreiro //
Implementação da interface para Mago |
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: