Compartilhando Funcionalidades em Ruby: Herança, Módulos e Mixins

Um dos princípios fundamentais do bom design de software é a eliminação de duplicação desnecessária. Queremos garantir que cada conceito em nossa aplicação seja expresso apenas uma vez no código. Por quê? Porque o mundo muda constantemente, e quando adaptamos nossa aplicação a cada mudança, queremos saber que alteramos exatamente o código que precisava ser alterado.

Neste artigo, vamos explorar dois mecanismos diferentes mas relacionados para compartilhamento de funcionalidades em Ruby: herança de classes e mixins. Também discutiremos quando usar cada um.

Herança e Mensagens

A herança permite criar uma classe que é uma especialização de outra classe. Esta classe especializada é chamada de subclasse da original, e a original é uma superclasse da subclasse.

Exemplo Básico de Herança

class Parent
  def say_hello
    puts "Hello from #{self}"
  end
end

p = Parent.new
p.say_hello

class Child < Parent
end

c = Child.new
c.say_hello

Resultado:

Hello from #<Parent:0x0000000100937780>
Hello from #<Child:0x0000000100936f60>

A notação < significa que estamos criando uma subclasse. A classe filha herda todos os métodos da classe pai, mas quando executamos o método, o valor de self mostra que estamos em uma instância da classe Child.

A Cadeia de Herança

Toda classe em Ruby tem uma superclasse. Se não definirmos explicitamente, Ruby automaticamente usa a classe Object:

class Parent
end

Child.superclass  # => Parent
Parent.superclass # => Object
Object.superclass # => BasicObject
BasicObject.superclass # => nil

BasicObject é a raiz da hierarquia de classes em Ruby.

Exemplo Prático: Sistema de Status de Tarefas

Vamos ver um exemplo onde a herança pode nos poupar duplicação significativa. Imagine um sistema de rastreamento de tarefas onde uma tarefa pode estar em diferentes estados:

class Status
  def self.for(status_string)
    case status_string
    when "done" then DoneStatus.new
    when "started" then StartedStatus.new
    when "defined" then DefinedStatus.new
    end
  end

  def done? = false
  def chatty_string = raise NotImplementedError
end

class DoneStatus < Status
  def to_s = "done"
  def done? = true
  def chatty_string = "I'm done"
end

class StartedStatus < Status
  def to_s = "started"
  def chatty_string = "I'm not done"
end

class DefinedStatus < Status
  def to_s = "defined"
  def chatty_string = "I'm not even started"
end

Agora, em vez de ter lógica de case espalhada por todo o código, podemos simplesmente usar:

Status.for(task.status).chatty_string

A lógica de case fica encapsulada no método Status.for, eliminando duplicação potencial.

Módulos

Em Ruby, um módulo pode fazer tudo que uma classe pode fazer, exceto criar instâncias. Os módulos oferecem dois benefícios principais:

  1. Namespaces: Previnem colisões de nomes
  2. Mixins: Podem ser incluídos em outras classes

Namespaces

Módulos definem um namespace, um sandbox onde métodos e constantes podem existir sem se preocupar com conflitos:

module Trig
  PI = 3.141592654
  
  def self.sin(x)
    # implementação trigonométrica
  end
  
  def self.cos(x)
    # implementação trigonométrica
  end
end

module Morals
  VERY_BAD = 0
  BAD = 1
  
  def self.sin(badness)
    # implementação moral
  end
end

Para usar sem ambiguidade:

require_relative "trig"
require_relative "morals"

y = Trig.sin(Trig::PI / 4)
wrongdoing = Morals.sin(Morals::VERY_BAD)

Mixins

Módulos podem fornecer uma alternativa à herança como forma de estender classes. Quando você inclui um módulo em uma classe, todos os métodos de instância do módulo ficam disponíveis como métodos de instância na classe:

module Debug
  def who_am_i?
    "#{self.class.name} (id: #{self.object_id}): #{self.name}"
  end
end

class Phonograph
  include Debug
  attr_reader :name
  
  def initialize(name)
    @name = name
  end
end

class EightTrack
  include Debug
  attr_reader :name
  
  def initialize(name)
    @name = name
  end
end

phonograph = Phonograph.new("West End Blues")
eight_track = EightTrack.new("Surrealistic Pillow")

phonograph.who_am_i? # => "Phonograph (id: 60): West End Blues"
eight_track.who_am_i? # => "EightTrack (id: 80): Surrealistic Pillow"

Exemplo Prático: Módulo Comparable

O módulo Comparable é um ótimo exemplo de mixin. Ao incluí-lo, você ganha operadores de comparação (<, <=, ==, >=, >) e o método between?. Você só precisa definir o operador <=>:

class Person
  include Comparable
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def to_s
    @name.to_s
  end

  def <=>(other)
    name <=> other.name
  end
end

p1 = Person.new("Matz")
p2 = Person.new("Guido")
p3 = Person.new("Larry")

if p1 > p2
  puts "#{p1.name}'s name > #{p2.name}'s name"
end

puts "Sorted list:"
puts [p1, p2, p3].sort

Resultado:

Matz's name > Guido's name
Sorted list:
Guido
Larry
Matz

Diferenç

Diferenças entre include, extend e prepend

Ruby oferece três mecanismos para misturar comportamento de módulos:

include

Adiciona métodos do módulo como métodos de instância:

module Greeting
  def hello
    "Hello from #{self.class}"
  end
end

class Person
  include Greeting
end

Person.new.hello # => "Hello from Person"

extend

Adiciona métodos do módulo como métodos de classe:

module ExtendedNew
  def new_from_string(string, delimiter = ",")
    new(*string.split(delimiter))
  end
end

class Person
  extend ExtendedNew
  
  def initialize(first_name, last_name)
    @first_name = first_name
    @last_name = last_name
  end
  
  def full_name = "#{@first_name} #{@last_name}"
end

superman = Person.new_from_string("Clark,Kent")
puts superman.full_name # => "Clark Kent"

prepend

Funciona como include, mas métodos do módulo são executados antes dos métodos da classe:

module Logging
  def execute
    puts "Executando método..."
    super
  end
end

class Worker
  prepend Logging
  
  def execute
    puts "Trabalhando..."
  end
end

Worker.new.execute
# => Executando método...
# => Trabalhando...

Busca de Métodos (Method Lookup)

Quando um método é chamado, Ruby segue uma ordem específica para encontrar a definição:

  1. Métodos adicionados especificamente à instância
  2. Módulos adicionados com prepend (último primeiro)
  3. Métodos definidos na classe do receptor
  4. Módulos adicionados com include (último primeiro)
  5. Se não encontrado, repete todo o processo na superclasse

Você pode ver toda a cadeia de busca usando o método ancestors:

String.ancestors
# => [String, Comparable, Object, Kernel, BasicObject]

Enumerable: Um Mixin Poderoso

O módulo Enumerable é um exemplo excelente de mixin. Para usá-lo, você só precisa definir o método each:

class VowelFinder
  include Enumerable

  def initialize(string)
    @string = string
  end

  def each
    @string.scan(/[aeiou]/) do |vowel|
      yield vowel
    end
  end
end

vf = VowelFinder.new("the quick brown fox jumped")
puts vf.reduce(:+) # => "euiooue"
puts vf.map(&:upcase) # => ["E", "U", "I", "O", "O", "U", "E"]

Quando Usar Herança vs Mixins

Use Herança Quando:

  • Existe uma relação "é um" verdadeira
  • A subclasse pode ser substituída pela superclasse (Princípio da Substituição de Liskov)
  • Você está criando especializações de um tipo

Use Mixins Quando:

  • Existe uma relação "tem um" ou "usa um"
  • Você quer adicionar capacidades a classes diferentes
  • Você quer evitar acoplamento forte

Exemplo de Design Melhor

Em vez de:

class Person < DataWrapper  # Person "é um" DataWrapper? Não!
  # ...
end

Prefira:

class Person
  include Persistable  # Person "tem capacidade de" persistência
  # ...
end

Variáveis de Instância em Mixins

Cuidado com variáveis de instância em mixins, pois podem colidir:

module Observable
  def observers
    @observer_list ||= []
  end
  
  def add_observer(obj)
    observers << obj
  end
end

class TelescopeScheduler
  include Observable
  
  def initialize
    @observer_list = []  # Colisão com o mixin!
  end
end

Solução: Use nomes únicos ou uma hash no nível do módulo:

module Test
  def self.states
    @states ||= {}
  end

  def state=(value)
    Test.states[object_id] = value
  end

  def state
    Test.states[object_id]
  end
end

Conclusão

A herança e os mixins são ferramentas poderosas para compartilhar funcionalidades em Ruby. A herança deve ser reservada para relações "é um" verdadeiras, enquanto os mixins são ideais para adicionar capacidades e evitar duplicação de código.

Lembre-se:

  • Herança cria acoplamento forte - use com parcimônia
  • Mixins oferecem flexibilidade e reutilização - use para composição
  • O Ruby oferece include, extend e prepend para diferentes necessidades
  • O módulo Enumerable é um exemplo excelente de mixin bem projetado

Ao projetar suas classes, pense em composição em vez de hierarquias rígidas. Seus programas serão mais flexíveis e fáceis de manter!

Comentários (0)

Nenhum comentário:

Postar um comentário