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,extendeprependpara 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