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:
- Escuche el audio con la pantalla apagada.
- Obtenga un certificado al finalizar.
- ¡Más de 5000 cursos para que explores!
Descargar la aplicación
attr_reader :xcrea el getterxattr_writer :xcrea el setterx=attr_accessor :xcrea 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,
selfes la instancia. - Dentro del cuerpo de la clase (a nivel de definición),
selfes la clase. self.metododefine 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ódulo | Responsabilidad | Ejemplos de reglas |
|---|---|---|
Libro | Representar un libro (título, autor, ISBN) y su disponibilidad | No se puede prestar si ya está prestado |
Usuario | Representar un usuario (nombre) y sus préstamos | Límite de préstamos activos |
Biblioteca | Gestionar catálogo y operaciones (alta, préstamo, devolución) | Buscar por ISBN, prestar/devolver |
Identificable (módulo) | Generar IDs internos para objetos | ID 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_autorybuscar_por_tituloenBiblioteca. - Módulo de formateo: crea un módulo
Presentablecon un métodofichae inclúyelo enLibropara imprimir una ficha legible. - Herencia opcional: si quieres ampliar, crea
Publicacioncomo padre deLibroyRevista, solo si comparten atributos y comportamiento de forma clara.