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:
- Namespaces: Previnem colisões de nomes
- 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:
- Métodos adicionados especificamente à instância
- Módulos adicionados com
prepend
(último primeiro) - Métodos definidos na classe do receptor
- Módulos adicionados com
include
(último primeiro) - 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
eprepend
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!
Nenhum comentário:
Postar um comentário