← Volver a la lista de posts

Módulos en Ruby

Los módulos en Ruby cumplen una doble función: evitan colisiones de nombres y nos ayudan a reutilizar código. En este post te explicamos qué son, cómo se definen y cómo se utilizan.

Un módulo es un contenedor que agrupa constantes, clases y métodos. Los módulos se definen utilizando la palabra clave module:

module MyModule
  MAX_CONNECTIONS = 5

  def method_one
  end

  def method_two
  end

  class ThingOne
  end

  class ThingTwo
  end
end

En este ejemplo hemos definiendo un módulo llamado MyModule que contiene una constante MAX_CONNECTIONS, los métodos method_one y method_two, y las clases ThingOne y ThingTwo.

Vamos a empezar explicando cómo los módulos nos ayudan a evitar colisiones de nombres y después hablaremos de las diferentes formas en que nos permiten reutilizar código.

Los módulos evitan colisiones de nombres

Los módulos nos permiten definir clases con el mismo nombre pero que se encuentren en módulos diferentes. Al crear un módulo, estás creando un espacio de nombramiento (namespace) que minimiza la probabilidad de encontrar dos clases que se llamen igual (el módulo se vuelve parte del nombre de la clase). Por ejemplo:

module ActiveRecord
  class Base
  end
end

module ActionView
  class Base
  end
end

Fíjate que las dos clases se llaman Base pero se encuentran en diferentes módulos.

Para referirse a una clase dentro de un módulo se debe especificar tanto el nombre del módulo como el nombre la clase separándolos con ::. En el ejemplo anterior nos referiríamos a ActiveRecord::Base y ActionView::Base, por ejemplo.

También es posible anidar módulos:

module System
  module Currency
    class Dollar
    end
  end
end

Dentro del módulo System estamos definiendo otro módulo llamado Currency, que a su vez define una clase llamada Dollar. En este caso, la forma de referirnos a la clase Dollar sería System::Currency::Dollar.

¿Qué pasa si hay una colisión de nombres?

En Ruby, si dos módulos se llaman igual, su contenido se mezcla en uno solo (lo mismo pasa con las clases). Esto nos permite extender y sobrescribir funcionalidad de librerías (gemas) o hasta del mismo lenguaje Ruby en nuestras aplicaciones. A esto se le conoce como monkey patching, y en general no se considera una buena práctica.

Por ejemplo, supongamos que queremos incluir un método palindrome? dentro de la clase String que devuelva si una cadena es un palíndrome1:

class String
  def palindrome?
    letters = self.downcase.scan(/\w/)
    letters == letters.reverse
  end
end

"anita lava la tina".palindrome? # => true
"hora de irse".palindrome? # => false

Ten mucho cuidado al abrir módulos o clases como lo acabamos de hacer en este ejemplo, podrías dañar alguna funcionalidad importante de alguna librería o del mismo lenguaje.

Ruby on Rails aprovecha el monkey patching para extender las clases estándar de Ruby. Por ejemplo, en este archivo, Rails extiende la clase String con métodos como pluralize, singularize y humanize.

Los módulos nos permiten reutilizar código

La segunda función de los módulos es definir métodos que se pueden incluir en otras clases. Por ejemplo:

module Dancer
  def dance
    puts "I'm dancing"
  end
end

class Human
  include Dancer # incluye los métodos del módulo Dancer
end

class Dog
  include Dancer # incluye los métodos del módulo Dancer
end

# ahora Humano y Perro pueden bailar
pedro = Human.new
pedro.dance # "I'm dancing"

max = Perro.new
max.dance # "I'm dancing"

El módulo Dancer está definiendo un método llamado dance. Las clases Human y Dog incluyen el módulo Dancer y por lo tanto “reciben” el método dance. De nuevo, es una buena forma de reutilizar código.

Los módulos pueden llamar los métodos de la clase que los incluye. Sin embargo, es importante documentar estos requerimientos dentro del módulo.

# To use this module, the class must provide a method +favorite_music+
module Dancer
  def dance
    puts "I'm dancing #{favorite_music}"
  end
end

class Dog
  include Dancer

  def favorite_music
    "Rock & Roll"
  end
end

max = Dog.new
max.dance # "I'm dancing Rock & Roll"

Como te puedes dar cuenta, el método dance ahora utiliza el método favorite_music, que debe estar definido en las clases que incluyan el módulo Dancer.

También es posible definir variables de instancia dentro de un módulo. Esas variables van a quedar almacenadas en las instancias de las clases que incluyan el módulo:

module Dancer
  def dance
    @dancing = true # definimos la variable de instancia
  end

  def stop_dance
    @dancing = false # definimos la variable de instancia
  end

  def is_dancing?
    @dancing || false # si @dancing no está definida, retorna false
  end
end

class Humano
  include Dancer
end

pedro = Humano.new
pedro.dance
pedro.is_dancing? #=> true
pedro.stop_dance
pedro.is_dancing? #=> false

En este caso los métodos dance y stop_dance están definiendo una variable de instancia llamada @dancing. El método is_dancing? utiliza la variable para saber si está bailando o no.

Cuando una clase tiene muchos métodos, una buena práctica es agruparlos en módulos:

module Needs
  # métodos relacionados con necesidades humanas
end

module Feelings
  # métodos relacionados con sentimientos humanos
end

class Human
  include Needs
  include Feelings
end

Si revisas las clases ActiveJob::Base y ActiveRecord::Base de Ruby on Rails te darás cuenta que siguen el mismo patrón.

Los módulos nos permiten definir constantes que necesitamos en nuestra aplicación:

module HttpHeaders
  CONTENT_TYPE = "Content-Type"
  AUTHORIZATION = "X-Token"
  ...
end

La forma de acceder a las constantes es muy parecido a la forma que se hace con las clases: separando los módulos y la constante con ::. En el ejemplo anterior tendríamos que escribir HttpHeaders::AUTHORIZATION para acceder a la constante AUTHORIZATION.

Sin embargo, si incluyes el módulo dentro de una clase, ya no es necesario especificar el módulo para acceder a la constante:

class WebServer
  include HttpHeaders

  def authenticate
    auth_header = headers[AUTHORIZATION] # no es necesario especificar el módulo
    ...
  end
end

Por último, los módulos son ideales para incluir los métodos utilitarios de tu aplicación, esos métodos que no parecen encajar en ninguna clase:

module Utilities
  def self.utility_one
    ...
  end

  def self.utility_two(param1, param2)
    ..
  end
end

# llamemos nuestros métodos utilitarios
Utilities.utility_one
Utilities.utility_two("Hola", 1)

Fíjate que los métodos tiene un prefijo self. antes del nombre. De esa forma podemos llamarlos directamente sobre el módulo, sin necesidad de incluirlos en una clase.

¿Incluir un módulo vs extender una clase?

Los módulos, al igual que la herencia, nos permiten reutilizar código. En vez de duplicar el método dance en las clases Human y Dog, lo definimos en un módulo que podemos incluir en las clases que tengan ese comportamiento.

Es posible hacer lo mismo con herencia. En vez de definir un módulo, vamos a definir una clase Dancer con un método dance:

class Dancer
  def dance
    puts "Estoy bailando"
  end
end

class Human < Dancer # extiende Dancer
end

class Dog < Dancer # extiende Dancer
end

El resultado es el mismo: Human y Dog “reciben” el método dance y se pueden utilizar igual que lo hicimos en un ejemplo anterior:

pedro = Human.new
pedro.dance # "I'm dancing"

max = Perro.new
max.dance # "I'm dancing"

Para saber cuándo incluir un módulo o extender de una clase te puedes hacer la siguiente pregunta: ¿La ClaseHija especializa a la ClasePadre? o mejor ¿ClaseHija es un ClasePadre? Si la respuesta es si, entonces es mejor crear una relación de herencia (extender una clase). De lo contrario, es mejor utilizar un módulo.

Por ejemplo, supongamos que necesitamos definir la relación entre un Bus y un Auto. ¿Bus es un Auto? La respuesta es si y por lo tanto esta es una relación de herencia, Bus extiende Auto.

En nuestro ejemplo de los humanos y perros bailadores, la pregunta es ¿Human es un Dancer? La respuesta es no. Ser un “bailador” es más una cualidad de los humanos (o de los perros), y por lo tanto es mejor utilizar un módulo.

Hay ocasiones en que no tenemos opción sino utilizar un módulo. Ruby no permite múltiple herencia, es decir, una clase no puede extender varias clases a la vez, y si una clase ya extiende otra2 a veces la única opción es crear un módulo e incluirlo así la relación sea de herencia.


Los módulos son una herramienta muy particular de Ruby, especialmente porque cumplen la doble función de evitar colisiones de nombres y reutilizar código3.

La mayoría de lenguajes Orientados a Objetos tienen algún mecanismo para evitar colisiones entre Clases. En Java se utilizan paquetes, en JavaScript se emula con closures, en C++ namespaces, etc.

A los módulos como mecanismo de reutilización de código también se les llama Mixins en otros lenguajes. Otros, como Java, C++ y JavaScript, no incluyen esa funcionalidad, aunque generalmente existen formas de emularla.


Notas

  1. Un palíndrome es una palabra o frase que se lee igual hacia adelante que hacia atrás. 

  2. En algunas ocasiones es posible redefinir la jerarquía de clases para lograr la relación deseada. 

  3. Python funciona de forma similar aunque no es necesario definir el módulo, el mismo archivo actúa como un módulo. 

comments powered by Disqus