Ao aprender programação orientada para objectos, e depois de dominar as noções básicas de objectos, campos e métodos, passa-se a maior parte do tempo a falar de herança. Herança significa que adquirimos uma parte da implementação de uma classe base. Basta criar uma subclasse de uma classe de base para herdar todos os campos e métodos não privados.
Um carro e um avião são veículos, pelo que é óbvio que estas duas classes devem ser expandidas a partir da sua classe base comum chamada Veículo. Este é um exemplo académico típico, mas ao decidirmos sobre a ligação destas classes com a relação de herança, devemos ter em conta algumas consequências.
Fig. 1 Implementação da relação de herança.
Neste caso, as classes estão intimamente ligadas umas às outras, o que significa que as alterações no comportamento de cada classe podem ser efectuadas através de alterações na classe base código. Isto pode ser tanto uma vantagem como uma desvantagem - depende do tipo de comportamento que esperamos. Se a herança for aplicada no momento errado, o processo de adição de uma nova função pode encontrar algumas dificuldades de implementação, porque não se adequará ao modelo de classe criado. Teremos que escolher entre duplicar o código e reorganizar o nosso modelo - e isso pode ser um processo realmente demorado. Podemos chamar o código que executa a relação de herança de "aberto-fechado" - isso significa que ele está aberto para extensões, mas fechado para modificações. Partindo do princípio de que na classe Veículo existe um funcionamento geral e definido do motor de cada veículo, no momento em que quisermos acrescentar um modelo de veículo sem motor (por exemplo, uma bicicleta) à nossa hierarquia de classes, teremos de efetuar algumas alterações sérias às nossas classes.
classe Veículo
def start_engine
fim
def stop_engine
fim
fim
classe Avião < Veículo
def mover
motor_de_partida
...
parar_motor
fim
fim
Composição
Se estivermos interessados apenas numa parte do comportamento da classe existente, uma boa alternativa à herança é utilizar a composição. Em vez de criar subclasses que herdam todos os comportamentos (os que precisamos e os que não precisamos de todo), podemos isolar as funções de que precisamos e equipar os nossos objectos com referências a elas. Desta forma, deixamos de pensar que o objeto é um tipo de um objeto de base, a favor da afirmação de que contém apenas algumas partes das suas propriedades.
Fig. 2 Utilização da composição
Seguindo esta abordagem, podemos isolar o código responsável pelo funcionamento do motor na classe autónoma denominada Motor e fazer-lhe referência apenas nas classes que representam veículos com motor. Isolar as funções com o uso de composição tornará a estrutura da classe Veículo mais simples e fortalecerá o encapsulamento das classes individuais. Agora, a única forma de os veículos poderem ter um efeito no motor é utilizar a sua interface pública, porque já não terão informações sobre a sua implementação. Além disso, permitirá utilizar diferentes tipos de motores em diferentes veículos, e até mesmo permitir a sua troca durante a execução do programa. É claro que a utilização da composição não é perfeita - estamos a criar um conjunto de classes pouco interligadas, que pode ser facilmente alargado e está aberto a modificações. Mas, ao mesmo tempo, cada classe está ligada a muitas outras classes e deve ter informações sobre as suas interfaces.
classe Veículo
fim
classe Motor
def start
fim
def stop
fim
fim
class Plane < Veículo
def initialize
@engine = Engine.new
fim
def mover
@engine.start
@engine.stop
fim
def change_engine(new_engine)
@motor = novo motor
end
fim
A escolha
Ambas as abordagens descritas têm vantagens e desvantagens, por isso, como escolher entre elas? A herança é uma especialização, pelo que é melhor aplicá-la apenas a problemas em que existam relações de tipo "is-a" - para lidarmos com a verdadeira hierarquia de tipos. Como a herança liga fortemente as classes entre si, em primeiro lugar, devemos sempre considerar se devemos ou não utilizar a composição. A composição deve ser aplicada a problemas em que existem relações de tipo "has-a" - assim, a classe tem muitas partes, mas é algo mais do que um conjunto de classes. Um avião é composto por partes, mas sozinho é algo mais - tem capacidades adicionais, como o voo. Continuando com este exemplo, as partes individuais podem ocorrer em diferentes variantes especializadas e, nesse caso, é uma boa altura para utilizar a herança.
Tanto a herança como a composição são apenas ferramentas que os programadores têm à sua disposição, pelo que a escolha da ferramenta correta para um determinado problema requer experiência. Por isso, vamos praticar e aprender com os nossos erros 🙂