Programando en Ruby

La Guía de los "Programadores Pragmáticos"

Modules



Los módulos son un sistema de agrupar métodos, clases y constantes. Los módulos te proporcionan principalmente dos ventajas:
  1. Los módulos proporcionan un espacio de nombres y previenen conflictos de nombres.
  2. Los módulos implementan la característica mixin.

Espacios de nombres

A medida que escribes programas Ruby más y más grandes te encontrarás al final con trozos de código reusables---librerías o rutinas relacionadas que pueden ser aplicadas de forma general. Desearás separar este código en archivos separados de modo que los contenidos puedan ser compartidos entre distintos programas Ruby.

A menudo este código se organizará en clases, por lo que probablemente metas una clase (o un conjunto de clases interrelacionadas) en un archivo.

Sin embargo hay veces en que quieres agrupar cosas que no forman una clase propiamente dicha.

Una aproximación inicial podría ser poner todas esas cosas en un archivo y simplemente cargar ese archivo en cualquier programa que lo necesites. Ésta es la manera en que el lenguaje C funciona. Sin embargo, existe un problema. Imagínate que programas un conjunto de funciones trigonométricas como sin, cos y demás. Los metes todos dentro de un archivo, trig.rb, para que futuras generaciones lo disfruten. Mientras tanto, Sally trabaja en una simulación del bien y el mal y programa una serie de rutinas útiles para ella, incluyendo beGood y sin (N del T: `Pecado' en inglés), y los mete en action.rb. Joe, quien quiere hacer un programa para descubrir cuántos ángeles pueden bailar en la cabeza de un alfiler, necesita cargar tanto trig.rb como action.rb en su programa. Pero ambos definen un método llamado sin. Malas noticias.

La respuesta es el mecanismo de los módulos. Los módulos definen un espacio de nombers, una caja de arena en la que tus métodos y constantes pueden crearse libremente sin tener que preocuparse de que sean sobreescritos por otros métodos y constantes. Las funciones trigonométricas podrían ir en un módulo:

module Trig
  PI = 3.141592654
  def Trig.sin(x)
   # ..
  end
  def Trig.cos(x)
   # ..
  end
end

y los métodos de las acciones buenas y malas podrían ir en otro:

module Action
  VERY_BAD = 0
  BAD      = 1
  def Action.sin(badness)
    # ...
  end
end

Las constantes de los módulos se nombran igual que las constantes de clase, con una letra mayúscula al principio. Las definiciones de los métodos son parecidas también: los métodos del módulo se definen como si fueran métodos de clase.

Si un tercer programa quiere usar estos módulos puede simplemente cargar los dos archivos (usando la sentencia Ruby require, la cual se discute en la página 103) y referenciar a los nombres adecuados.

require "trig"
require "action"

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

Del mismo modo que con los métodos de clase, puedes llamar a un método del módulo precediendo su nombre con el del módulo y dos puntos, y referenciar una constante usando el nombre del módulo y dos veces dos puntos.

Mixins

Los módulos tienen otro uso maravilloso. De un plumazo, eliminan casi toda ecesidad de herencia múltiple proporcionando una característica llamada mixin.

En los ejemplos de la sección anterior definimos métodos de módulo, métodos cuyos nombres van prefijados del nombre del módulo. Si ésto te recuerda a los métodos de clase, tu siguiente pensamiento podría ser ``¿que pasaría si defino métodos de instancia dentro de un módulo?'' Buena pregunta. Un módulo no puede tener instancias por que no es una clase. Sin embargo puedes hacer include de un módulo dentro de una definición de una clase. Cuando se hace esto todos los métodos de instancia del módulo pasan a estar inmediatamente disponibles como métodos de la clase. Se mezclan. De hecho, los módulos con los que se hace mixin se comportan como si fueran superclases.

module Debug
  def whoAmI?
    "#{self.type.name} (\##{self.id}): #{self.to_s}"
  end
end
class Phonograph
  include Debug
  # ...
end
class EightTrack
  include Debug
  # ...
end
ph = Phonograph.new("West End Blues")
et = EightTrack.new("Surrealistic Pillow")
ph.whoAmI? » "Phonograph (#537766170): West End Blues"
et.whoAmI? » "EightTrack (#537765860): Surrealistic Pillow"

Al incluir el módulo Debug tanto Phonograph como EightTrack ganan acceso al método de instancia whoAmI?.

Un par de puntualizaciones sobre la sentencia include antes de continuar. Primero, no hace nada con los archivos. Los programadores C usan una directiva del preprocesador llamada #include para insertar los contenidos de un archivo dentro de otro durante la compilación. La sentencia include de Ruby simplemente crea una referencia al módulo llamado. Si ese módulo está en un archivo diferente debes usar require para importar ese archivo antes de usar include. Segudno, un include Ruby no copia simplemente los métodos de instancia del módulo en la clase. En su lugar, crea una referencia en la clase al módulo indicado. Si varias clases incluyen el mismo módulo todas apuntarán a la misma cosa. Si cambias la definición de un método dentro de un módulo, incluso mientras tu programa se ejecuta, todas las clases que lo incluyan cambiarán su comportamiento.[Por supuesto, estamos hablando solo de métodos aquí. Las variables de instancia son siempre por objeto por ejemplo.]

Los mixins te proporcinan una manera perfectamente controlada para añadir funcionalidad a las clases. Sin embargo su verdadero poder destaca cuando el código en el mixin empieza a interactuar con el código de la clase que lo usa. Tomemos el mixin estándar de Ruby Comparable como ejemplo. El mixin Comparable puede usarse para añadir los operadores de comparación (<, <=, ==, >=, y >) así como el método between? a una clase. Para que esto funcione Comparable asume que cualquier clase que lo use define el operador <=>. Por lo tanto, como programador de la clase, defines un método, <=>, incluyes Comparable, y obtienes seis funciones de comparación gratis. Pobemos ésto con nuestra clase Song, haciendo que las canciones sean comparables basadas en su duración. Todo lo que tenemos que hacer es incluir el módulo Comparable e implementar el operador de comparación <=>.

class Song
  include Comparable
  def <=>(other)
    self.duration <=> other.duration
  end
end

Podemos ver como funciona con unas pocas canciones de prueba.

song1 = Song.new("My Way",  "Sinatra", 225)
song2 = Song.new("Bicylops", "Fleck",  260)
song1 <=> song2 » -1
song1  <  song2 » true
song1 ==  song1 » true
song1  >  song2 » false

Por último, en la página 43 mostramos una implementación de la función de Smalltalk inject, implementándola dentro de la clase Array. Prometimos hacerla aplicable de forma más general. ¿Qué mejor que hacerlo con el mixin de un módulo?

module Inject
  def inject(n)
     each do |value|
       n = yield(n, value)
     end
     n
  end
  def sum(initial = 0)
    inject(initial) { |n, value| n + value }
  end
  def product(initial = 1)
    inject(initial) { |n, value| n * value }
  end
end

Podemos probarlo haciendo mixin en alguna clase predefinida.

class Array
  include Inject
end
[ 1, 2, 3, 4, 5 ].sum » 15
[ 1, 2, 3, 4, 5 ].product » 120

class Range
  include Inject
end
(1..5).sum » 15
(1..5).product » 120
('a'..'m').sum("Letters: ") » "Letters: abcdefghijklm"

Para un ejemplo más extenso de mixin échale un vistazo a la documentación del módulo Enumerable que empieza en la página 403.

Variables de instancia en mixins

La gente que llega a Ruby de C++ muchas veces nos pregunta, ``¿Qué pasa con las variables de instancia en un mixin? En C++ tengo que cumplir ciertos requisitos para controlar cómo se comparten las variables en una jerarquía de herencia múltiple. ¿Cómo maneja esto Ruby?''.

Bueno, de entrada ésta no es una pregunta sencilla, les decimos. Recuerda cómo funcionan las variables de instancia en Ruby: la primera mención de una variable prefijada de ``@'' crea una variable de instancia en el objeto actual, self.

En un mixin, ésto implica que el módulo que mezclas en tu clase cliente (la mezclada?) puede crear variables de instancia en el objeto cliente y puede usar attr y demás para definir el acceso a esas variables de instancia. Por ejemplo:

module Notes
  attr  :concertA
  def tuning(amt)
    @concertA = 440.0 + amt
  end
end

class Trumpet   include Notes   def initialize(tune)     tuning(tune)     puts "Instance method returns #{concertA}"     puts "Instance variable is #{@concertA}"   end end

# The piano is a little flat, so we'll match it Trumpet.new(-5.3)
produce:
Instance method returns 434.7
Instance variable is 434.7

No sólo tenemos acceso a los métodos definidos en el mixin, sino que también lo obtenemos a las variables de instancia necesarias. Existe un riesgo en ésto por supuesto, que diferentes mixins usen una variable de instancia con el mismo nombre y provoquen una colisión:

module MajorScales
  def majorNum
    @numNotes = 7 if @numNotes.nil?
    @numNotes # Return 7
  end
end

module PentatonicScales   def pentaNum     @numNotes = 5 if @numNotes.nil?     @numNotes # Return 5?   end end

class ScaleDemo   include MajorScales   include PentatonicScales   def initialize     puts majorNum # Should be 7     puts pentaNum # Should be 5   end end

ScaleDemo.new
produce:
7
7

Los dos trozos de código que mezclamos usan una variable de instancia llamada @numNotes. Desafortunadamente el resultado no es lo que el autor esperaba.

Por lo general, los módulos mixin no intentarán añadir sus propios datos de instancia ---usarán los accessors para obtener datos del cliente. Pero si necesitas crear un mixin con su propio estado, asegúrate que las variables de instancia tienen nombres únicos para distinguirlas de otros mixins en el sistema (quizá usar el nombre del módulo como parte del nombre de la variable).

Iteradores y el módulo Enumerable

Probablemente te habrás dado cuenta de que las clases de colección de Ruby soportan un gran número de operaciones que hacen varias cosas con las colecciones: recorrerlas, ordenarlas y demás. Podrías estar pensando, ``¡Guau, estaría estupendo si mi clase también soportara todas esas geniales características!'' (Bueno, si realmente lo pensaste así probablemente sea el momento de dejar de ver programas de televisión de los 60.)

Bien, tus clases pueden soportar todas esas geniales características, gracias a la magia de los mixins y al módulo Enumerable. Todo lo que tienes que hacer es escribir un iterador llamado each, el cual a su vez tiene que devolver los elementos de tu colección. Mezcla Enumerable e inmediatamente tu clase soportará cosas como map, include? y find_all?. Si los objetos en tu colección implementan relaciones de orden usando el método <=> también obtendras min, max y sort.

Incluir otros archivos

Debido a que Ruby hace fácil escribir código modular y de calidad muchas veces te encontrarás a ti mismo creando pequeños archivos conteniendo algún tipo de funcionalidad relacionada---una interfaz x, un algoritmo para hacer y, y demás. Normalmente organizarás estos archivos como librerías de clases o módulos.

Una vez hayas creado estos archivos querrás incorporarlos en nuevos programas. Ruby tiene dos sentencias para hacer ésto.

load "filename.rb"

require "filename"

El método load incluye el código fuente Ruby referenciado cada vez que el método se ejecute, mientras que require carga cualquier archivo dado una sola vez. require tiene una funcionalidad adicional: puede cargar librerías binarias compartidas. Ambas rutinas aceptan tanto rutas absolutas como relativas. Si se les pasa una ruta relativa (o simplemente un nombre), buscarán en cada directorio de la ruta de carga actual ($:, tratado en la página 140) ese archivo.

Los archivos cargados usando load y require pueden por supuesto, incluir otros archivos, los cuales a su vez incluyan otros, y así. Lo que puede parecer no tan obvio es que require es una sentencia ejecutable---puede aparecer dentro de una sentencia if o podría incluir una cadena que se acaba de construir. La ruta de búsqueda puede modificarse además en tiempo de ejecución. Simplemente añade el directorio que quieres a la cadena $:.

Ya que load incluirá el archivo incondicionalmente, puedes usarlo para recargar un archivo fuente que haya cambiado desde que el programa comenzó:

5.times do |i|
   File.open("temp.rb","w") { |f|
     f.puts "module Temp\ndef Temp.var() #{i}; end\nend"
   }
   load "temp.rb"
   puts Temp.var
 end
produces:
0
1
2
3
4


Extraído del libro "Programming Ruby - The Pragmatic Programmer's Guide"
Copyright © 2000 Addison Wesley Longman, Inc. liberado bajo los términos de la Open Publication License V1.0.
La referencia está disponible en: download.