Composição e Herança no Ruby

Publicado por Luiz Cezer Marrone Filho no dia dev

Desenvolvimento de Software

O paradigma de Orientação a Objetos é um dos mais populares dentro do desenvolvimento de software. Dentre os conceitos aplicados existem alguns que são considerados os pilares do paradigma, sendo eles: Abstração, Encapsulamento, Herança e Polimorfismo. Porém além dos pilares um outro conceito importante é o do reuso de código, onde determinado trecho de código pode ser reaproveitado por várias classes afim de evitar codificação duplicada dentro do software. E quando se fala de reuso, a Herança pode ser a primeira técnica que vem a cabeça, porém nem sempre ela é a mais indicada e ao invés de solucionar um problema pode acabar gerando inúmeros outros a medida que o software cresce.

O que é Herança ?

A Herança é um dos pilares da programação orientada a objetos e é uma técnica comumente utilizada quando se quer reaproveitar código já existente e também descrever uma relação entre classes de modo que determinadas classes possam herdar atributos e métodos de uma classe de nível superior.

Essas classes podem ser descritas como: Classe Pai, Superclasse ou Classe Base, que é a classe que contém as características que serão herdadas pelas classes de nível mais baixo na hierarquia.

E existem também as descritas: Classes Filhas, Derivadas ou Subclasses, que são justamente as classes que herdam as características de sua classe base e também podem conter suas próprias característas, ou ainda refinar (especializar para o seu contexto) uma determinada característica herdada de sua classe base.

A Herança descreve uma relação é do tipo entre as classes que estão nessa hierarquia. Conforme o exemplo abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Calendar
  attr_reader :start_date, :end_date

  def initialize; end

  def date_range
    raise 'Must implement!'
  end
end

class WeeklyCalendar < Calendar
  def date_range
    base_date.beginning_of_week(:sunday)..base_date.end_of_week(:sunday)
  end
end

class MonthlyCalendar < Calendar
  def date_range
    base_date.beginning_of_month.beginning_of_week(:sunday)..base_date.end_of_month.end_of_week(:sunday)
  end
end

Na hierarquia proposta acima é possível verificar a superclasse Calendar e analisar que as classes WeeklyCalendar e MonthlyCalendar possuem uma relação é do tipo com relação a superclasse Calendar, afinal de contas essas classes representam um tipo de calendário, porém mais especializadas.

Dessa forma o uso da Herança pode ser aplicado como técnica para descrever a relação entre as classes e também aplicar o reuso de código. Importante notar também que apesar de utilizar a Herança para reaproveitar determinados trechos de código, cada tipo de calendário também pode especializar as características que forem necessárias para seu contexto, no caso do exemplo cada calendário implementa seu próprio método date_range.

Quais os principais problemas com uso de Herança ?

Se a Herança faz tudo isso que foi dito acima, por quais motivos ela pode ser ruim para o desenvolvimento ?

Um dos principais problemas encontrados ao fazer uso massivo de Herança é o alto acoplamento e o baixo encapsulamento de informações de cada classe.

Quanto maior o acoplamento mais difícil é alterar as classes, realizar modificações em uma classe base sem que essa modificação se propague para suas classes filhas, ou em casos mais graves, um erro escrito em uma classe irá se propagar para as demais dentro da hierarquia. E o baixo encapsulamento se da ao fato que as classes envolvidas podem acabar tendo muito conhecimento do estado interno de outras classes.

Outro problema relacionado ao auto acoplamento criado pela Herança é que torna difícil que comportamentos sejam adicionados ou removidos em tempo de execução, ou então fazer objetos terem outros comportamentos para diferentes contextos em diferentes momentos dentro do software.

Nem sempre uma subclasse precisa herdar todos os atributos ou métodos de sua superclasse, nesse caso há uma quebra do Princípio de Segregação de Interface, que dita que classes não devem implementar aquilo que não irão utilizar apenas para satisfazer a Herança.

Por fim, a Herança pode parecer uma boa alternativa em primeiro momento porém com o passar do tempo e o crescimento do software as classes começam a crescer e ganhar diferentes comportamentos, ficando cada vez mais diferentes até chegar a um ponto que a relação entre as classes antes ditas como é do tipo X estarem mais com cara de compartilham algumas funcionalidades com Y, desse modo não fazendo mais sentido o uso de Herança.

O que é Composição ?

A Composição assim como a Herança é uma forma de reaproveitamento de código, porém possui certas vantagens sobre a Herança que a torna uma opção mais segura e flexível. A Composição descreve uma relação tem uma (habilidade) entre dois objetos. Ou seja, dessa forma qualquer objeto que necessita uma determinada habilidade pode ter essa habilidade quando precisar.

A Composição ao contrário da Herança possui um baixo acoplamento e permite que objetos trabalhem de forma independente, cada uma com a sua responsabilidade.

Dentro do Ruby a Composição pode ser aplicada de duas formas:

A primeira delas delas através do uso de classes que serão responsáveis por executar determinada ação, sendo acionadas através de delegação, conforme exemplo abaixo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Foo
  def increment(number)
    number +=1
  end
end

class Bar
  def initialize; end

  def action(number)
    Foo.new.increment(number)
  end
end

Bar.new.action(10) => 11

Nesse exemplo não estou dando atenção a outros aspectos do código, como Injeção de Dependência.

Dessa forma tem-se duas classes distintas Foo e Bar, onde Foo é a classe que contém a implementação do método increment e Bar é a classe que tem a necessidade de executar uma ação de incrementar um número, porém fazendo o uso de Composição delega essa ação para Foo#increment.

Ou fazendo uso de módulos, utilizando-os como mixins:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module Foo
  def increment(number)
    number += 1
  end
end

class Bar
  include Foo

  def initialize; end

  def action(number)
    increment(number)
  end
end

class Something
  include Foo

  def initialize; end

  def action(number)
    increment(number)
  end
end

Bar.new.action(1)       => 2
Something.new.action(2) => 3

Dessa forma podemos descrever que as classes Something e Bar possuem uma habilidade Foo, assim é possível fazer a inclusão do módulo e utilizar seus métodos onde for necessário. Ao optar por fazer o uso de módulos é preciso levar em conta se determinado comportamento irá ser compartilhado entre mais classes, do contrário é preferível utilizar como a primeira opção, criando classes responsáveis por executar a ação e fazer delegação.

Módulos podem ser utilizados para estender o comportamento das instâncias da classe ou da própria classe em si. Para mais detalhes sobre como isso funciona leia esse do blog Nomedojogo.

Como a Composição resolve os problemas que a Herança pode gerar ?

Tomando como base os exemplos descritos acima é possível ter noção de como o uso da Composição irá ajudar a resolver os problemas encontrados com o uso de Herança.

A Herança é uma relação estática entre os objetos, uma vez feita a relação é difícil desfaze-lá. Já a Composição é uma relação dinâmica que permite uma maior flexibilidade, extensibilidade e pode ser adicionada ao objeto quando houver essa necessidade. Um forte exemplo disso é o padrão de projeto Decorator que faz uso de Composição para adicionar novos comportamentos a objetos em tempo de execução. Outros padrões de projeto como Strategy também favorecem Composição sobre a Herança.

Por permiter adição e remoção de comportamentos em tempo de execução, a Composição também permite ao objeto ter vários papéis e responsabilidades durante sua vida dentro do software, alterando esses comportamentos nos momentos necessários.

Quando usada da forma correta, também incentiva o software a obedecer os princípios SOLID de desenvolvimento criando classes que tenham responsabilidades únicas, sejam abertas para extensão e favorecam injeção de dependência.

Por fim, pelo fato de não ter conhecimento de uma hierarquia de objetos e focar em classes que façam apenas um determinado trabalho, a Composição provê um maior encapsulamento aos objetos.

Mas e agora, qual eu utilizo ?

Herança

  • Serve bem para os casos onde a modelagem está bem definida e também casos onde é preciso centralizar o que for comum na hierarquia e especializar o que for diferente, como no exemplo do Calendário.
  • Quando dentro da hierarquia as classes de nível mais baixo representarem uma relação é do tipo com relação a classe base.

Composição

  • Quando uma relação é menos hierárquica e tende a mudar conforme o software cresce é uma escolha melhor, pois irá permitir que vários objetos tenham uma determinada habilidade comum, sem que seja necessário uma grande alteração em sua estrutura.
  • É mais flexível e permite que objetos sejam estendidos em tempo de execução.
  • Por essas características se adequa bem em situações onde ocorrem diversas mudanças em regras de negócio e classes podem ganhar ou perder determinadas características conforme o software evolui.

Considerações finais

  • Modele bem como irá funcionar a hierarquia e a troca de mensagens entre suas classes para ter uma noção melhor do que irá acontecer e saber tomar uma decisão melhor entre Composição e Herança.
  • Utilize Herança com cuidado, no geral favoreça o uso de Composição sobre Herança.
  • Escolher Composição irá deixar suas classes mais reaproveitáveis, flexíveis e fáceis de se manter.
  • Composição permite adicionar/remover comportamentos em tempo de execução e criar código menos acoplado e favorece o encapsulamento.
  • Golang por exemplo, uma linguagem relativamente nova não implementa uso de Herança, apenas Composição de tipos, visando evitar os problemas apontados pelo uso de Herança.

E você tem alguma experiência com relação aos pontos levantados no post e gostaria de compartilhar? Deixa seu comentário.

Luiz Cezer Marrone Filho

Luiz Cezer Marrone Filho

Full Stack Developer

Comentários