Programación orientada a objetos en Ruby con clases y módulos

Capítulo 5

Tiempo estimado de lectura: 8 minutos

+ Ejercicio

El modelo mental: todo es un objeto (y las clases también)

En Ruby, trabajas con objetos: entidades que combinan estado (datos) y comportamiento (métodos). Una clase es la “plantilla” para crear objetos (instancias). Y un detalle importante: las clases también son objetos, por eso pueden tener métodos propios (métodos de clase).

  • Clase: define qué datos y comportamientos tendrán sus instancias.
  • Instancia: un objeto concreto creado a partir de una clase.
  • Estado: normalmente se guarda en variables de instancia como @nombre.
  • Comportamiento: métodos que operan sobre ese estado.

Crear una clase e instancias

class Usuario
end

u1 = Usuario.new
u2 = Usuario.new

p u1.class  # => Usuario
p u1 == u2   # => false (son objetos distintos)

initialize: construir objetos con estado válido

El método initialize se ejecuta automáticamente al hacer Clase.new. Se usa para dejar el objeto en un estado inicial coherente.

class Usuario
  def initialize(nombre)
    @nombre = nombre
  end
end

u = Usuario.new("Ana")

Regla práctica: si un dato es obligatorio para que el objeto tenga sentido, pídelo en initialize.

Atributos y encapsulación: proteger el estado con getters/setters

La encapsulación consiste en no exponer el estado interno de forma arbitraria. En Ruby, el estado suele guardarse en variables de instancia (@...) y se accede mediante métodos.

Getters y setters “a mano”

class Usuario
  def initialize(nombre)
    @nombre = nombre
  end

  def nombre
    @nombre
  end

  def nombre=(nuevo_nombre)
    @nombre = nuevo_nombre
  end
end

attr_reader, attr_writer, attr_accessor

Ruby ofrece atajos para generar esos métodos:

Continúa en nuestra aplicación.
  • Escuche el audio con la pantalla apagada.
  • Obtenga un certificado al finalizar.
  • ¡Más de 5000 cursos para que explores!
O continúa leyendo más abajo...
Download App

Descargar la aplicación

  • attr_reader :x crea el getter x
  • attr_writer :x crea el setter x=
  • attr_accessor :x crea ambos
class Usuario
  attr_reader :nombre
  attr_accessor :email

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

u = Usuario.new("Ana", "ana@ejemplo.com")
p u.nombre
u.email = "ana2@ejemplo.com"

Encapsulación con reglas de negocio en setters

Cuando hay validaciones o transformaciones, conviene escribir el setter manualmente (aunque uses attr_reader para el getter).

class Usuario
  attr_reader :email

  def initialize(email)
    self.email = email
  end

  def email=(valor)
    raise ArgumentError, "Email vacío" if valor.nil? || valor.strip.empty?
    @email = valor.strip.downcase
  end
end

Observa el uso de self.email = ... dentro de initialize: así reutilizas la validación del setter.

Métodos de instancia vs. métodos de clase

Métodos de instancia se llaman sobre un objeto concreto y suelen usar su estado (@...). Métodos de clase se llaman sobre la clase y suelen servir como “fábricas”, utilidades o consultas globales.

class Ticket
  attr_reader :codigo

  def initialize(codigo)
    @codigo = codigo
  end

  # Método de instancia
  def valido?
    @codigo.start_with?("T-")
  end

  # Método de clase
  def self.generar
    nuevo_codigo = "T-#{rand(1000..9999)}"
    new(nuevo_codigo)
  end
end

t = Ticket.generar
p t.codigo
p t.valido?

self: qué significa según el contexto

  • Dentro de un método de instancia, self es la instancia.
  • Dentro del cuerpo de la clase (a nivel de definición), self es la clase.
  • self.metodo define un método de clase.
  • Para llamar a un setter dentro de la instancia, necesitas self.atributo = ... (si no, Ruby lo interpreta como variable local).
class Producto
  attr_reader :precio

  def initialize(precio)
    self.precio = precio
  end

  def precio=(valor)
    raise ArgumentError, "Precio inválido" if valor.nil? || valor < 0
    @precio = valor
  end
end

Módulos: compartir comportamiento sin herencia (include/extend)

Un módulo es una unidad de código reutilizable. No se instancia como una clase, pero puede aportar métodos a otras clases.

include: métodos como instancia

Con include, los métodos del módulo pasan a ser métodos de instancia.

module Formateo
  def moneda(cantidad)
    "€#{'%.2f' % cantidad}"
  end
end

class Factura
  include Formateo

  def initialize(total)
    @total = total
  end

  def total_formateado
    moneda(@total)
  end
end

f = Factura.new(12.5)
p f.total_formateado  # => "€12.50"

extend: métodos como métodos de clase

Con extend, los métodos del módulo se agregan como métodos de clase (o como métodos singleton del objeto que extiendes).

module GeneradorIds
  def siguiente_id
    @contador ||= 0
    @contador += 1
  end
end

class Libro
  extend GeneradorIds

  attr_reader :id, :titulo

  def initialize(titulo)
    @id = self.class.siguiente_id
    @titulo = titulo
  end
end

l1 = Libro.new("Ruby")
l2 = Libro.new("POO")
p [l1.id, l2.id]  # => [1, 2]

Herencia: úsala cuando aclare, no cuando complique

La herencia modela una relación “es un/una”. Úsala si realmente hay una generalización clara. Si solo quieres compartir comportamiento, un módulo suele ser mejor.

class Publicacion
  attr_reader :titulo

  def initialize(titulo)
    @titulo = titulo
  end
end

class Libro < Publicacion
  attr_reader :autor

  def initialize(titulo, autor)
    super(titulo)
    @autor = autor
  end
end

super llama al initialize de la clase padre para reutilizar la inicialización común.

Proyecto guiado: Sistema simple de Biblioteca

Vas a modelar una biblioteca con reglas de negocio básicas y pruebas manuales desde consola. Objetivo: practicar clases, instancias, atributos, encapsulación, módulos, métodos de clase y un poco de herencia (solo si aporta claridad).

1) Definir el dominio (objetos y responsabilidades)

Clase/MóduloResponsabilidadEjemplos de reglas
LibroRepresentar un libro (título, autor, ISBN) y su disponibilidadNo se puede prestar si ya está prestado
UsuarioRepresentar un usuario (nombre) y sus préstamosLímite de préstamos activos
BibliotecaGestionar catálogo y operaciones (alta, préstamo, devolución)Buscar por ISBN, prestar/devolver
Identificable (módulo)Generar IDs internos para objetosID incremental por clase

2) Módulo Identificable (extend para métodos de clase)

module Identificable
  def siguiente_id
    @contador ||= 0
    @contador += 1
  end
end

3) Clase Libro: estado, encapsulación y reglas

class Libro
  extend Identificable

  attr_reader :id, :titulo, :autor, :isbn

  def initialize(titulo:, autor:, isbn:)
    @id = self.class.siguiente_id
    @titulo = titulo
    @autor = autor
    self.isbn = isbn
    @prestado = false
  end

  def prestado?
    @prestado
  end

  def prestar!
    raise "El libro ya está prestado" if prestado?
    @prestado = true
  end

  def devolver!
    raise "El libro no estaba prestado" unless prestado?
    @prestado = false
  end

  private

  def isbn=(valor)
    raise ArgumentError, "ISBN vacío" if valor.nil? || valor.strip.empty?
    @isbn = valor.strip
  end
end

Aquí hay encapsulación real: el setter de isbn es private para evitar cambios arbitrarios desde fuera. El estado @prestado solo cambia mediante prestar! y devolver!.

4) Clase Usuario: límite de préstamos y API clara

class Usuario
  extend Identificable

  LIMITE_PRESTAMOS = 2

  attr_reader :id, :nombre, :prestamos

  def initialize(nombre)
    @id = self.class.siguiente_id
    @nombre = nombre
    @prestamos = []
  end

  def puede_prestar?
    @prestamos.size < LIMITE_PRESTAMOS
  end

  def registrar_prestamo(libro)
    raise "Límite de préstamos alcanzado" unless puede_prestar?
    @prestamos << libro
  end

  def registrar_devolucion(libro)
    @prestamos.delete(libro)
  end
end

5) Clase Biblioteca: coordina operaciones

La biblioteca actúa como “orquestador”: valida, busca y ejecuta reglas combinadas entre objetos.

class Biblioteca
  attr_reader :catalogo

  def initialize
    @catalogo = []
  end

  def agregar_libro(libro)
    raise "ISBN duplicado" if buscar_por_isbn(libro.isbn)
    @catalogo << libro
  end

  def buscar_por_isbn(isbn)
    @catalogo.find { |l| l.isbn == isbn }
  end

  def prestar(isbn:, usuario:)
    libro = buscar_por_isbn(isbn)
    raise "No existe un libro con ese ISBN" unless libro
    raise "El usuario no puede pedir más libros" unless usuario.puede_prestar?

    libro.prestar!
    usuario.registrar_prestamo(libro)

    libro
  end

  def devolver(isbn:, usuario:)
    libro = buscar_por_isbn(isbn)
    raise "No existe un libro con ese ISBN" unless libro

    libro.devolver!
    usuario.registrar_devolucion(libro)

    libro
  end
end

6) Pruebas manuales desde consola (IRB)

Abre una consola Ruby (por ejemplo irb) y ejecuta paso a paso. La idea es “probar el modelo” como si fuera una API.

# 1) Crear biblioteca
biblio = Biblioteca.new

# 2) Crear libros
l1 = Libro.new(titulo: "El lenguaje Ruby", autor: "Matz", isbn: "ISBN-001")
l2 = Libro.new(titulo: "POO práctica", autor: "Ada", isbn: "ISBN-002")
l3 = Libro.new(titulo: "Diseño limpio", autor: "Bob", isbn: "ISBN-003")

# 3) Agregar al catálogo
biblio.agregar_libro(l1)
biblio.agregar_libro(l2)
biblio.agregar_libro(l3)

# 4) Crear usuario
u = Usuario.new("Ana")

# 5) Prestar dos libros (límite = 2)
biblio.prestar(isbn: "ISBN-001", usuario: u)
biblio.prestar(isbn: "ISBN-002", usuario: u)

p u.prestamos.map { |lib| [lib.titulo, lib.prestado?] }
# => [["El lenguaje Ruby", true], ["POO práctica", true]]

# 6) Intentar prestar un tercero (debe fallar)
biblio.prestar(isbn: "ISBN-003", usuario: u)
# => raise "El usuario no puede pedir más libros"

# 7) Devolver uno y volver a prestar
biblio.devolver(isbn: "ISBN-001", usuario: u)
biblio.prestar(isbn: "ISBN-003", usuario: u)

p u.prestamos.map { |lib| lib.isbn }
# => ["ISBN-002", "ISBN-003"]

7) Ejercicios de refuerzo (para practicar POO real)

  • Regla adicional: impedir devolver un libro si el usuario no lo tiene en préstamos (lanza un error en Biblioteca#devolver).
  • Búsquedas: agrega buscar_por_autor y buscar_por_titulo en Biblioteca.
  • Módulo de formateo: crea un módulo Presentable con un método ficha e inclúyelo en Libro para imprimir una ficha legible.
  • Herencia opcional: si quieres ampliar, crea Publicacion como padre de Libro y Revista, solo si comparten atributos y comportamiento de forma clara.

Ahora responde el ejercicio sobre el contenido:

¿Cuál es la forma correcta de aplicar una validación de un setter al inicializar un objeto, evitando asignar el valor directamente a la variable de instancia?

¡Tienes razón! Felicitaciones, ahora pasa a la página siguiente.

¡Tú error! Inténtalo de nuevo.

Al usar self.atributo = valor dentro de initialize, Ruby llama al setter y se aplica la validación o transformación definida allí. Si asignas con @atributo = valor, te saltas esa lógica.

Siguiente capítulo

Manejo de errores en Ruby y escritura de código robusto

Arrow Right Icon
Portada de libro electrónico gratuitaRuby desde Cero para principiantes: Programación Moderna, Clara y Eficiente
50%

Ruby desde Cero para principiantes: Programación Moderna, Clara y Eficiente

Nuevo curso

10 páginas

Descarga la aplicación para obtener una certificación gratuita y escuchar cursos en segundo plano, incluso con la pantalla apagada.