Nivel inicial11 min de lectura · Publicado el

Módulos en Ruby: qué son, para qué sirven y mixins

Módulos en Ruby: la caja Comparable trae <, >, ==, between? y clamp; la clase Tramo la incluye, define <=> y compara tramos. Un <=> y ya puedes compararrubentejera.com < > ==between? clampComparableclass Tramoinclude Comparabledef <=>tramo1 < tramo2 → true
Comparable es la caja de herramientas: Tramo la coge con include Comparable, solo define <=> y ya tiene <, >, ==, between? y clamp. Por eso tramo1 < tramo2 da true.

Un módulo es como una caja de herramientas: no es un objeto que haga nada por sí mismo, pero cualquier clase puede cogerla y usar lo que trae dentro.

¿Qué son los módulos en Ruby?

Los módulos se parecen a las clases porque contienen métodos, constantes y otros módulos. La diferencia es que no se pueden instanciar ni heredar de ellos:

module Numeros
end

Numeros.new  # NoMethodError: undefined method 'new' for module Numeros

Ruby no tiene interfaces ni herencia múltiple. En su lugar, usa módulos.

¿Buscabas el operador módulo (%), el resto de una división? Está en operador módulo (%).

Para qué sirven los módulos

  1. Como librería o espacio de nombres (namespace). Agrupan métodos y constantes bajo un nombre para que no choquen con otros que se llamen igual.
  2. Para ampliar las clases (mixin). Si una clase incluye un módulo, sus objetos ganan los métodos del módulo.

Ejemplo de módulo como librería:

module Geometria
  PI_APROX = 3.14

  def self.area_circulo(radio) = PI_APROX * radio * radio
end

puts Geometria.area_circulo(2)  # 12.56

Módulos dentro de módulos: el operador ::

Un módulo puede contener clases y otros módulos. Es como el apellido de una familia: puede haber dos Producto en el mismo programa si uno es Tienda::Producto y otro Almacen::Producto. Para llegar a lo que hay dentro se usa :::

module Tienda
  IVA = 0.21

  class Producto
    attr_reader :nombre, :precio

    def initialize(nombre, precio)
      @nombre = nombre
      @precio = precio
    end

    def precio_con_iva = (precio * (1 + IVA)).round(2)
  end

  module Formato
    def self.euros(cantidad) = format("%.2f €", cantidad)
  end
end

libro = Tienda::Producto.new("Libro", 20)
puts Tienda::Formato.euros(libro.precio_con_iva)   # 24.20 €
puts Tienda::IVA                                   # 0.21

Dentro de Producto basta con escribir IVA: Ruby busca la constante en las clases y módulos que la rodean, de dentro afuera (primero Tienda::Producto, luego Tienda; es la lista que devuelve Module.nesting). Desde fuera hay que nombrarla entera, Tienda::IVA. Así organizan sus clases las gemas y Rails, y suele coincidir con la estructura de carpetas: Tienda::Producto vive en tienda/producto.rb. Cómo cargar esos ficheros se explica en incluir otros archivos en Ruby.

module_function: un método que sirve de las dos formas

A veces quieres llamar a un método como Conversor.km_a_millas(10) y, además, poder incluirlo en una clase. module_function crea las dos versiones a la vez:

module Conversor
  module_function

  def km_a_millas(km) = (km * 0.621371).round(2)
end

puts Conversor.km_a_millas(10)   # 6.21

class Viaje
  include Conversor

  def resumen(km) = "#{km} km son #{km_a_millas(km)} millas"
end

puts Viaje.new.resumen(5)        # 5 km son 3.11 millas
Viaje.new.km_a_millas(5)
private method 'km_a_millas' called for an instance of Viaje (NoMethodError)

La copia que llega a la clase es privada: Viaje la usa por dentro, pero no la ofrece hacia fuera. Así es como funciona Math: puedes escribir Math.sqrt(16) o incluir Math y escribir sqrt(16).

¿Qué es un mixin?

Un mixin es un módulo que se «mezcla» en una clase para darle métodos que no ha heredado. La clase sigue teniendo un único padre, pero puede incluir tantos módulos como quiera.

Hay dos formas de hacerlo: include y extend.

include: todos los objetos de la clase ganan los métodos

module Numeros
  def to_s
    case @value
    when 1 then "uno"
    when 2 then "dos"
    when 3 then "tres"
    else "mucho"
    end
  end
end

class Contador
  include Numeros
  attr_accessor :value
end

contador = Contador.new
contador.value = 3
puts contador.to_s                  # tres
puts contador.is_a?(Numeros)        # true
p Contador.ancestors.first(3)       # [Contador, Numeros, Object]

Fíjate en la última línea: el módulo se coloca en la cadena de antepasados de la clase, justo encima de ella. Por eso is_a?(Numeros) devuelve true, aunque no sea una subclase.

El módulo Comparable

Ruby trae módulos listos para usar. Comparable te da <, >, ==, between?, clamp… con solo definir el método <=>:

class Tramo
  include Comparable
  attr_reader :nombre, :distancia

  def initialize(nombre, distancia)
    @nombre = nombre
    @distancia = distancia
  end

  def <=>(otro_tramo)
    distancia <=> otro_tramo.distancia
  end
end

tramo1 = Tramo.new("Tramo 1", 1000)
tramo2 = Tramo.new("Tramo 2", 2000)

puts tramo1 <=> tramo2            # -1
puts tramo1 < tramo2              # true
puts tramo1 == tramo2             # false
puts tramo1 > tramo2              # false
puts [tramo2, tramo1].min.nombre  # Tramo 1

extend: solo un objeto (o la clase) gana los métodos

Con extend el módulo se añade a un objeto concreto. Los demás objetos de la misma clase no cambian:

class Marcador
  attr_accessor :value
  def to_s = @value.to_s
end

a = Marcador.new
a.value = 3
b = Marcador.new
b.value = 2

puts a.to_s   # 3
a.extend(Numeros)
puts a.to_s   # tres
puts b.to_s   # 2  (b no ha cambiado)

Si haces extend dentro de una clase, el objeto que se amplía es la propia clase, así que los métodos del módulo pasan a ser métodos de clase:

module Saludo
  def saluda = "Hola desde #{name}"
end

class Persona
  extend Saludo
end

puts Persona.saluda   # Hola desde Persona
include extend
¿Quién gana los métodos? Todos los objetos de la clase Un objeto concreto, o la clase si se usa dentro de ella
Los métodos son… De instancia Del objeto (o de clase)

Diferencias entre módulos y clases

Módulos Clases
Instancias No pueden crear objetos. Crean objetos, y cada uno tiene su estado (sus variables de instancia).
Mezcla Se pueden mezclar en clases y en otros módulos. Sus métodos y constantes se añaden a quien los incluye. No se pueden mezclar en nada.
Herencia Un módulo no hereda ni se puede heredar de él. Una clase hereda de otra, nunca de un módulo.

La gran ventaja frente a la herencia es que puedes incluir varios módulos a la vez, mientras que una clase solo tiene un padre. Así cada módulo aporta una característica, sin tener que montar jerarquías de clases.

Rails usa mucho esta técnica, y también la de reabrir clases y módulos ya definidos para añadirles métodos. Un ejemplo son los concerns de Rails, que son módulos con algo de ayuda extra.

¿Cuándo heredar de una clase y cuándo incluir un módulo?

Hazte esta pregunta: ¿la clase hija «es un» tipo de la clase padre?

  • Sí: usa herencia.
  • No: usa un módulo.

Por ejemplo: ¿un autobús es un vehículo? Sí, así que Autobus < Vehiculo. ¿Una radio es un autobús? No: la radio es algo que el autobús tiene o sabe hacer, así que encaja mejor en un módulo.

Cómo se escribe la herencia con < y super se explica en Clases en Ruby.

Herencia para lo que algo es; módulos para lo que algo sabe hacer.

Errores frecuentes

1. Llamar al método de un módulo sin haberlo definido con self. Un def normal dentro de un módulo crea un método para quien lo incluya, no para el propio módulo:

module Geometria
  def area_circulo(radio) = 3.14 * radio * radio
end

puts Geometria.area_circulo(2)
undefined method 'area_circulo' for module Geometria (NoMethodError)

Escribe def self.area_circulo si lo vas a llamar como Geometria.area_circulo, o usa module_function.

2. Usar include cuando querías extend. Con include, los métodos son para los objetos, no para la clase:

module Saludo
  def saluda = "Hola desde #{name}"
end

class Persona
  include Saludo
end

puts Persona.saluda
undefined method 'saluda' for class Persona (NoMethodError)

El mensaje da la pista: dice for class Persona, no for an instance of Persona. Estás llamando a la clase, así que necesitas extend Saludo.

3. Incluir una clase. Solo se pueden mezclar módulos:

class Contador
  include String
end
wrong argument type Class (expected Module) (TypeError)

4. Incluir Comparable sin definir <=>. El módulo trae <, > y compañía, pero todos dependen de <=>. Si no lo defines, Ruby usa el de Object, que solo sabe decir si dos objetos son el mismo:

class Tramo
  include Comparable
  attr_reader :distancia

  def initialize(distancia)
    @distancia = distancia
  end
end

puts Tramo.new(1000) < Tramo.new(2000)
comparison of Tramo with Tramo failed (ArgumentError)

5. Escribir el nombre en minúscula. El nombre de un módulo es una constante, así que empieza por mayúscula:

module tienda
end
tienda.rb: --> tienda.rb

unexpected constant path after `module`; class/module name must be CONSTANT

> 1  module tienda
> 2  end

tienda.rb:1: syntax error found (SyntaxError)
> 1 | module tienda
    |        ^~~~~~ unexpected constant path after `module`; class/module name must be CONSTANT
  2 | end

Compruébalo con un test

Un módulo pensado para mezclarse se prueba con una clase mínima hecha solo para el test. Así compruebas lo que el módulo promete sin depender de ninguna clase real. Este test usa Minitest, que viene con Ruby, y Struct, que crea en una línea una clase con los atributos que le digas:

require "minitest/autorun"

module Medible
  include Comparable

  def <=>(otro) = distancia <=> otro.distancia
end

class MedibleTest < Minitest::Test
  # Una clase mínima, solo para el test: lo único que Medible necesita es #distancia
  Tramo = Struct.new(:distancia) { include Medible }

  def test_compara_por_distancia
    corto = Tramo.new(1000)
    largo = Tramo.new(2000)

    assert corto < largo
    assert_equal corto, [largo, corto].min
    assert Tramo.new(1500).between?(corto, largo)
  end
end
ruby medible_test.rb
1 runs, 3 assertions, 0 failures, 0 errors, 0 skips

Si alguien da la vuelta a la comparación (otro.distancia <=> distancia), el código sigue pareciendo razonable, pero el test lo detecta:

MedibleTest#test_compara_por_distancia [medible_test.rb:17]:
Expected false to be truthy.

El test también deja escrito el contrato del módulo: quien lo incluya tiene que responder a distancia. Para empezar con tests en Ruby, lee la introducción al testing en Ruby.

Cuándo no usar un módulo

Un módulo es la herramienta correcta para una capacidad que comparten varias clases sin estar emparentadas, como Comparable. Hay tres señales de que no encaja:

  • El módulo depende de variables que no son suyas. El Numeros del principio lee @value, pero no lo crea: confía en que la clase lo tenga. Si una clase lo llama de otra forma, nada falla; el módulo simplemente da un resultado equivocado:
# Numeros es el módulo de la sección «include», que lee @value
class Marcador
  include Numeros
  attr_accessor :valor   # se llama valor, no value
end

m = Marcador.new
m.valor = 1
puts m.to_s   # mucho

Sin error, sin aviso: @value vale nil y el case cae en el else. Es más seguro que el módulo pida un método con nombre (como Medible pide distancia) que leer variables de instancia ajenas: si el método falta, al menos salta un error (NameError o NoMethodError) que dice qué falta.

  • El módulo es un cajón de sastre. Un Utils con veinte métodos sin relación entre sí no agrupa nada; solo esconde el desorden. Si te cuesta ponerle un nombre que diga qué hace, probablemente son varios módulos, o métodos que pertenecen a una clase concreta.
  • Solo lo usa una clase. Sacar código a un módulo para que la clase «parezca» más corta no reduce la complejidad: la reparte en dos ficheros. Si además necesita guardar estado propio, suele ser mejor un objeto aparte que la clase recibe y usa (composición), que se puede probar por separado.

¿Se pueden hacer subclases de módulos?

No. Lo que sí puedes es incluir un módulo en una clase o en otro módulo. No crea una subclase, pero, como has visto, is_a? devuelve true para el módulo incluido. Es la forma que tiene Ruby de conseguir algo parecido a la herencia múltiple.

Ejercicio de cinco minutos

  1. Copia el test de Medible y añade un caso con clamp: un tramo de 3000 limitado entre corto y largo debe dar largo. ¿Tienes que escribir algo en el módulo para que pase?
  2. Crea un módulo Tienda con una clase Carrito dentro y llámala desde fuera con Tienda::Carrito.new.
  3. Reescribe Numeros para que llame a un método valor en vez de leer @value, e inclúyelo en una clase que no lo tenga. Fíjate en qué error sale ahora y compáralo con el mucho silencioso de antes.

¿Quieres practicar más? Tienes un ejercicio práctico de módulos en Ruby y más ejemplos de mixin en Ruby, con prepend incluido.

Fuentes

  • Documentación oficial de Ruby 3.4: Module (include, extend, Module.nesting y module_function, cuyas copias de instancia son privadas), Comparable y Struct.
  • Todos los ejemplos se han ejecutado en Docker con la imagen oficial ruby:3.4 (Ruby 3.4.12): los del artículo original el 8 de noviembre de 2026; los espacios de nombres, module_function, los errores frecuentes y el test (Minitest 5.25.4), el 11 de noviembre de 2026. Estos últimos se han repetido con ruby:4.0 (Ruby 4.0.9, Minitest 6.0.0) con la misma salida. Las salidas de error están recortadas a las líneas que importan.
  • Vídeo: Curso de Ruby - 26 - Módulos, include y extend, de FuryCode.