Subsecciones

Los Módulos como Mixins

  1. Modules have another, wonderful use.

    At a stroke, they pretty much eliminate the need for inheritance, providing a facility called a mixin.

  2. What happens if I define instance methods within a module?

    Good question

  3. A module can’t have instances, because a module isn’t a class.

  4. However, you can include a module within a class definition.

    When this happens, all the module’s instance methods are suddenly available as methods in the class as well.

  5. They get mixed in.

  6. In fact, mixed-in modules effectively behave as superclasses.

DaveThomas
Si un módulo define métodos de instancia en vez de métodos de clase, esos métodos de instancia pueden ser combinados/mezclados (mixin) en otras clases.

Para mezclar un módulo en una clase llamamos al método include:

class Point
  include Enumerable
end

  1. Thanks to the magic of mixins and module Enumerable. All you have to do is write an iterator called each, which returns the elements of your collection in turn. Mix in Enumerable, and suddenly your class supports things such as map, include?, and find_all?.
  2. If the objects in your collection implement meaningful ordering semantics using the <=> method, you’ll also get methods such as min, max, and sort.

include es un método privado de instancia de Module:

ruby-1.9.2-head :004 > Module.private_instance_methods.select {|x| x =~ /inc/ }
 => [:included, :include]
Es legal incluir un módulo en una clase o en un módulo. Al hacerlo los métodos de instancia del módulo incluído se convierten en métodos de instancia del incluyente.

  module D
    def initialize(name)
     @name =name
    end
    def to_s
      @name
    end
  end

  module Debug
  include D
    def who_am_i?
      "#{self.class.name} (\##{self.__id__}): #{self.to_s}"
    end
  end

  class Phonograph
    include Debug
    # ...
  end

  class EightTrack
    include Debug
    # ...
  end

  ph = Phonograph.new("West End Blues")      # "Phonograph (#70270868282220): West End Blues"º)
  et = EightTrack.new("Surrealistic Pillow") # "EightTrack (#70270868320420): Surrealistic Pillow")
  
  ph.who_am_i?
  et.who_am_i?

We’ll make a couple of points about the include statement before we go on.

  1. First, it has nothing to do with files.
  2. C programmers use a preprocessor directive called #include to insert the contents of one file into another during compilation. The Ruby include statement simply makes a reference to a module.
  3. If that module is in a separate file, you must use require (or its less commonly used cousin, load) to drag that file in before using include.
  4. Second, a Ruby include does not simply copy the module’s instance methods into the class. Instead, it makes a reference from the class to the included module.
  5. If multiple classes include that module, they’ll all point to the same thing.
  6. If you change the definition of a method within a module, even while your program is running, all classes that include that module will exhibit the new behavior.

Dave Thomas

Variables de Instancia en Mixins

  1. Remember how instance variables work in Ruby: the first mention of an @-prefixed variable creates the instance variable in the current object, self.

  2. For a mixin, this means that the module you mix into your client class may create instance variables in the client object and may use attr_reader and friends to define accessors for these instance variables.
  3. However, this behavior exposes us to a risk. A mixin’s instance variables can clash with those of the host class or with those of other mixins.

The example that follows shows a class that uses our Observer module but that unluckily also uses an instance variable called @observer_list.

  module Observable
    def observers
      @observer_list ||= []
    end

    def add_observer(obj)
      observers << obj
    end

    def notify_observers
      observers.each {|o| o.update }
    end
  end

require 'code/observer_impl'
class TelescopeScheduler

  # other classes can register to get notifications
  # when the schedule changes
  include Observable   

  def initialize
    @observer_list = []  # folks with telescope time
  end

  def add_viewer(viewer)
    @observer_list << viewer
  end

  # ...
end
At runtime, this program will go wrong in some hard-to-diagnose ways.

  1. For the most part, mixin modules don’t use instance variables directly—they use accessors to retrieve data from the client object
  2. But if you need to create a mixin that has to have its own state, ensure that the instance variables have unique names to distinguish them from any other mixins in the system (perhaps by using the module’s name as part of the variable name).
  3. Alternatively, the module could use a module-level hash, indexed by the current object ID, to store instance-specific data without using Ruby instance variables. A downside of this approach is that the data associated with a particular object will not get automatically deleted if the object is deleted.

Dave Thomas

[~/local/src/ruby/rubytesting/programmingRuby/instance_variables_in_mixins]$ cat private_state_in_mixins.rb 
require 'pp'

module Test
  State = {}
  def state=(value)
    State[__id__] = value
  end
  def state
    State[__id__]
  end

  def delete_state
    State.delete(__id__)
  end
end

class Client
  include Test
end

c1 = Client.new
c2 = Client.new
c1.state = 'cat'
c2.state = 'dog'

pp Test::State.inspect # {70259441214220=>"cat", 70259441214140=>"dog"}

pp c1.state
c1.delete_state

pp Test::State.inspect # {70259441214140=>"dog"}

pp c2.state

[~/local/src/ruby/rubytesting/programmingRuby/instance_variables_in_mixins]$ ruby private_state_in_mixins.rb 
"{70278990371120=>\"cat\", 70278990371100=>\"dog\"}"
"cat"
"{70278990371100=>\"dog\"}"
"dog"


Colisiones

One of the other questions folks ask about mixins is, how is method lookup handled? In particular, what happens if methods with the same name are defined in a class, in that class’s parent class, and in a mixin included into the class?

  1. The answer is that Ruby looks first in the immediate class of an object,
  2. then in the mixins included into that class,
  3. and then in superclasses and their mixins.
  4. If a class has multiple modules mixed in, the last one included is searched first.

Dave Thomas

¿Que ocurre si se incluyen dos módulos en los que existen métodos con el mismo nombre?

[~/TheRubyProgrammingLanguage/Chapter7ClassesAndModules/modules]$ cat module_collision.rb 
module M1
  def foo
    puts "M1"
  end
end

module M2
  def foo
    puts "M2"
  end
end

Cuando ambos módulos son incluídos en la clase C, la última definición de foo es la que domina:

[~/TheRubyProgrammingLanguage/Chapter7ClassesAndModules/modules]$ cat class_require_module_collision.rb 
require 'module_collision'
class C
  include M1
  include M2
end

C.new.foo # 'M2'
puts C.ancestors.inspect # => [C, M2, M1, Object, Kernel, BasicObject]

Ahora bien, si en la clase existe un método foo ese será encontrado primero:

[~/TheRubyProgrammingLanguage/Chapter7ClassesAndModules/modules]$ cat -n module_class_collision.rb 
 1  module M
 2    def foo
 3      puts "M"
 4    end
 5  end
 6  
 7  class C
 8    def foo
 9      puts "C"
10    end
11    include M
12  end
13  C.new.foo                # => "C"
14  puts C.ancestors.inspect # => [C, M, Object, Kernel, BasicObject]

Composición frente a Herencia/Composition versus Inheritance

Inheritance and mixins both allow you to write code in one place and effectively inject that code into multiple classes.

So, when do you use each?

  1. First, let’s look at subclassing. Classes in Ruby are related to the idea of types.

  2. When we create our own classes, you can think of it as adding new types to the language.

  3. And when we subclass either a built-in class or our own class, we’re creating a subtype.

  4. Now, a lot of research has been done on type theories. One of the more famous results is the Liskov Substitution Principle. Formally, this states:

    Let q(x) be a property provable about objects x of type T. Then q(y) should be true for objects y of type S where S is a subtype of T

  5. What this means is that you should be able to substitute an object of a child class wherever you use an object of the parent class—the child should honor the parent’s contract.

    We’re used to saying this in English: a car is a vehicle, a cat is an animal, and so on.

    This means that a cat should, at the very least, be capable of doing everything we say that an animal can do.

  6. So, when you’re looking for subclassing relationships while designing your application, be on the lookout for these is-a relationships.

  7. But...here’s the bad news. In the real world, there really aren’t that many true is a relationships. Instead, it’s far more common to have has a or uses a relationships between things.

Dave Thomas

  1. Think of containment as a has a relationship. A car has an engine, a person has a name, etc.
  2. Think of inheritance as an is a relationship. A car is a vehicle, a person is a mammal, etc.


extend

Aunque Module.include es la forma normal de mezclar un módulo también es posible mezclar con Object.extend. Este método hace que los métodos de instancia del módulo especificado se incorporen como métodos singleton del objeto receptor. Si el objeto receptor es una clase, esto es, es un objeto de la clase Class, entonces los métodos son métodos de clase de dicha clase receptora.

[~/srcLPP/Chapter7ClassesAndModules/modules]$ cat module_mixin_with_extend.rb 
module Chuchu
  def tutu
    "method tutu. self: #{self.inspect}"
  end
end

class ChanChan
end

p = "hello "
q = "world"
p.extend Chuchu

puts p.tutu

ChanChan.extend Chuchu
puts ChanChan.tutu

puts q.tutu # exception
Ejecución:
[~/srcLPP/Chapter7ClassesAndModules/modules]$ ruby module_mixin_with_extend.rb 
method tutu. self: "hello "
method tutu. self: ChanChan
module_mixin_with_extend.rb:19:in `<main>': undefined method `tutu' for "world":String (NoMethodError)

Casiano Rodriguez León 2015-01-07