Clases en Ruby: atributos, métodos, herencia y acceso
Una clase es un molde y cada objeto es una pieza hecha con ese molde. El molde de galletas dice qué forma tienen todas; cada galleta es una distinta y puede llevar su propio glaseado. En Ruby, la clase dice qué datos y qué comportamiento tienen sus objetos; cada objeto guarda sus propios valores.
Crear una clase e instanciarla
El nombre de la clase va en CamelCase (Coche, CuentaBancaria). Al llamar a new, Ruby crea el objeto y ejecuta su método initialize con los argumentos que le pases:
class Coche
def initialize(color)
@color = color
end
end
coche = Coche.new("rojo")
@color es una variable de instancia: cada objeto tiene la suya. initialize es privado: no lo llamas tú, lo llama new.
Las clases de Ruby funcionan igual. Array.new crea un array vacío y Array.new(3, 0) uno con tres ceros:
p Array.new # []
p Array.new(["1", "2"]) # ["1", "2"]
p Array.new(3, 0) # [0, 0, 0]
Atributos: leer y cambiar datos
Desde fuera no puedes tocar @color directamente. Necesitas métodos que lo lean (getter) y lo cambien (setter). Puedes escribirlos a mano:
class Coche
def initialize(color)
@color = color
end
def color # lectura
@color
end
def color=(nuevo) # escritura
@color = nuevo
end
end
O dejar que Ruby los genere con una línea:
| Método | Crea | Para qué |
|---|---|---|
attr_reader :color |
color |
Solo lectura |
attr_writer :color |
color= |
Solo escritura |
attr_accessor :color |
color y color= |
Lectura y escritura |
class Coche
attr_accessor :color
def initialize(color)
@color = color
end
end
coche = Coche.new("rojo")
coche.color = "azul"
puts coche.color # azul
Atributos virtuales
Un atributo virtual parece un atributo más desde fuera, pero no tiene una variable de instancia con su nombre: sus métodos calculan o transforman el valor. Aquí nombre_completo guarda el dato en @nombre añadiendo el tratamiento:
class Persona
def nombre_completo
@nombre
end
def nombre_completo=(nombre)
@nombre = "Señor #{nombre}"
end
end
persona = Persona.new
persona.nombre_completo = "Pepe"
puts persona.nombre_completo # Señor Pepe
p persona.instance_variables # [:@nombre]
Para quien usa la clase, nombre_completo es un atributo; por dentro solo existe @nombre.
Métodos
Los métodos se definen con def y su nombre va en snake_case (arrancar, descripcion), no en camelCase:
class Coche
attr_accessor :color
def initialize(color)
@color = color
end
def arrancar(sonido)
puts sonido
end
def descripcion
"Soy un coche de color #{@color}"
end
end
coche = Coche.new("rojo")
puts coche.descripcion # Soy un coche de color rojo
coche.arrancar("Brum brum")
Más sobre parámetros y valores de retorno en Métodos en Ruby.
to_s: cómo se muestra el objeto
to_s (to string) es el método que Ruby usa al convertir el objeto en texto, por ejemplo con puts o con la interpolación "#{coche}". Si no lo defines, verás el nombre de la clase y una dirección: #<Coche:0x000…>.
to_s debe devolver un String. Si devuelve otra cosa (un hash, por ejemplo), la interpolación lo ignora y vuelves a ver #<Coche:0x…>.
class Coche
def to_s
"Coche de color #{@color}"
end
end
puts coche # Coche de color azul
puts "#{coche}" # Coche de color azul
to_s e inspect: lo que ve puts y lo que ve p
Ruby tiene dos formas de convertir un objeto en texto. to_s es la versión para el usuario y la usan puts y la interpolación. inspect es la versión para quien programa y la usan p, irb y algunos mensajes de error, como los de los tests. Sin definir ninguna, verías esto:
class Coche
def initialize(color) = @color = color
end
coche = Coche.new("rojo")
puts coche
p coche
#<Coche:0x00007cd7420730c8>
#<Coche:0x00007cd7420730c8 @color="rojo">
Puedes definir las dos:
class Coche
def initialize(color) = @color = color
def to_s = "Coche #{@color}"
def inspect = "#<Coche color=#{@color.inspect}>"
end
coche = Coche.new("rojo")
puts coche # Coche rojo
p coche # #<Coche color="rojo">
puts [coche].inspect # [#<Coche color="rojo">]
Un inspect claro se agradece al depurar y al leer el fallo de un test, porque es lo que Minitest muestra al comparar objetos. Si buscabas el toString de Java, en Ruby es to_s.
Otros métodos útiles de cualquier objeto
coche.object_id # número que identifica al objeto
coche.respond_to?(:arrancar) # true: el objeto tiene ese método
coche.respond_to?(:volar) # false
coche.methods # todos sus métodos, también los heredados
Coche.instance_methods(false) # solo los definidos en la clase Coche
Herencia
Con < una clase hereda de otra: recibe sus métodos y puede añadir o sustituir los que quiera. super llama al mismo método de la clase padre.
class Vehiculo
def initialize(ruedas, color)
@ruedas = ruedas
@color = color
end
def to_s
"Ruedas: #{@ruedas} · Color: #{@color}"
end
end
class Turismo < Vehiculo
def initialize(ruedas, color, puertas)
super(ruedas, color)
@puertas = puertas
end
def to_s
"#{super} · Puertas: #{@puertas}"
end
end
puts Vehiculo.new(4, "gris") # Ruedas: 4 · Color: gris
puts Turismo.new(4, "gris", 5) # Ruedas: 4 · Color: gris · Puertas: 5
Si Turismo no tuviera su propio to_s, Ruby lo buscaría en Vehiculo, y después en Object. Para compartir comportamiento sin herencia, Ruby tiene los módulos: lo verás en Módulos en Ruby.
Control de acceso: public, protected y private
| Nivel | Quién puede llamarlo |
|---|---|
public |
Cualquiera. Es el nivel por defecto (salvo initialize, que es privado). |
protected |
Objetos de la misma clase o de sus subclases, desde dentro de sus métodos. Útil para comparar dos objetos. |
private |
Solo el propio objeto, sin receptor explícito (metodo, no otro.metodo). |
Hay dos formas de indicarlo. La primera, una palabra que afecta a todos los métodos que vienen después:
class Ejemplo
def publico = "público"
def compara(otro) = otro.protegido # puede llamar al protegido de otro Ejemplo
protected
def protegido = "protegido"
private
def privado = "privado"
end
e = Ejemplo.new
e.publico # "público"
e.compara(Ejemplo.new) # "protegido"
e.protegido # NoMethodError: protected method 'protegido' called for an instance of Ejemplo
e.privado # NoMethodError: private method 'privado' called for an instance of Ejemplo
La segunda, nombrar el método. Como def devuelve el nombre del método, también puedes escribirlo en la misma línea:
class Otro
def a; end
private :a
private def b; end
end
Un setter privado: leer desde fuera, cambiar solo desde dentro
Es muy habitual que un dato se pueda leer desde fuera, pero que solo el propio objeto lo cambie, y siempre a través de sus reglas. Para eso, attr_reader público y attr_writer privado:
class Cuenta
attr_reader :saldo
def initialize(saldo)
self.saldo = saldo
end
def ingresar(importe)
self.saldo = saldo + importe
end
private
attr_writer :saldo
end
cuenta = Cuenta.new(10)
cuenta.ingresar(5)
puts cuenta.saldo # 15
cuenta.saldo = 1_000_000
private method 'saldo=' called for an instance of Cuenta (NoMethodError)
Dentro de la clase hay que escribir self.saldo =: un método privado se puede llamar con self delante, y sin él Ruby entendería que creas una variable local (lo verás en los errores frecuentes).
Público lo justo; privado lo demás.
Errores frecuentes
1. Olvidar la @ en initialize. Sin @, color = color crea una variable local que desaparece al terminar el método. No hay error; el atributo se queda en nil:
class Coche
attr_reader :color
def initialize(color)
color = color
end
end
p Coche.new("rojo").color # nil
2. Usar el setter sin self dentro de la clase. Con el anterior pasa lo mismo: color = "azul" dentro de un método crea una variable local, no llama a color=:
class Coche
attr_accessor :color
def initialize(color)
@color = color
end
def pintar_de_azul
color = "azul"
end
end
coche = Coche.new("rojo")
coche.pintar_de_azul
puts coche.color # rojo
Escribe self.color = "azul" o @color = "azul".
3. Leer un atributo que no tiene getter. Que exista @color no significa que se pueda leer desde fuera:
class Coche
def initialize(color)
@color = color
end
end
coche = Coche.new("rojo")
puts coche.color
undefined method 'color' for an instance of Coche (NoMethodError)
Añade attr_reader :color.
4. Llamar a new sin los argumentos de initialize. new pasa sus argumentos a initialize, así que tienen que coincidir. Con la clase Coche del error anterior, que pide un color:
Coche.new # initialize(color) espera un argumento
wrong number of arguments (given 0, expected 1) (ArgumentError)
5. Usar super sin paréntesis cuando el padre pide otros argumentos. super a secas pasa al padre los mismos argumentos que recibió el método hijo. Si el padre espera menos, falla (salida de Ruby 3.4):
class Vehiculo
def initialize(ruedas)
@ruedas = ruedas
end
end
class Turismo < Vehiculo
def initialize(ruedas, puertas)
super
@puertas = puertas
end
end
Turismo.new(4, 5)
turismo.rb:2:in 'initialize': wrong number of arguments (given 2, expected 1) (ArgumentError)
from turismo.rb:9:in 'Turismo#initialize'
Escribe super(ruedas) para pasarle solo lo que necesita, o super() para no pasarle nada.
6. Escribir el nombre de la clase en minúscula. Como en los módulos, el nombre es una constante:
class coche
end
unexpected constant path after `class`; class/module name must be CONSTANT
Compruébalo con un test
Una clase se prueba por lo que hace hacia fuera, no por sus variables de instancia. Este test, con Minitest (incluido en Ruby), comprueba la Cuenta de arriba: que ingresar suma, que la comparación entre cuentas funciona y que el saldo no se puede cambiar desde fuera:
require "minitest/autorun"
class Cuenta
attr_reader :saldo
def initialize(saldo)
self.saldo = saldo
end
def ingresar(importe)
self.saldo = saldo + importe
end
def mas_rica_que?(otra) = saldo > otra.saldo
private
attr_writer :saldo
end
class CuentaTest < Minitest::Test
def test_ingresar_suma_al_saldo
cuenta = Cuenta.new(10)
cuenta.ingresar(5)
assert_equal 15, cuenta.saldo
end
def test_compara_dos_cuentas
assert Cuenta.new(20).mas_rica_que?(Cuenta.new(10))
end
def test_el_saldo_no_se_cambia_desde_fuera
refute Cuenta.new(10).respond_to?(:saldo=)
end
end
ruby cuenta_test.rb
3 runs, 3 assertions, 0 failures, 0 errors, 0 skips
El tercer test deja escrita una decisión de diseño: si alguien cambia attr_writer por attr_accessor público «para ir más rápido», el test lo detecta. respond_to? solo mira los métodos públicos, por eso sirve para comprobarlo. Para empezar con tests en Ruby, lee la introducción al testing en Ruby.
Cuándo no crear una clase
Una clase tiene sentido cuando junta datos y el comportamiento que los cuida, como la Cuenta que no deja cambiar su saldo desde fuera. Hay casos en los que sobra:
- Solo agrupa funciones, sin datos propios. Si todos sus métodos podrían ser de clase y nunca haces
new, probablemente es un módulo. - Solo guarda datos, sin comportamiento. Para un punto, una fecha con hora o una respuesta de una API, Ruby trae
Data, desde la versión 3.2, que crea en una línea una clase de objetos que no se pueden modificar y que se comparan por su contenido:
Punto = Data.define(:x, :y)
origen = Punto.new(x: 0, y: 0)
p origen # #<data Punto x=0, y=0>
p origen == Punto.new(x: 0, y: 0) # true
p origen.with(y: 5) # #<data Punto x=0, y=5>
origen.x = 3
undefined method 'x=' for an instance of Punto (NoMethodError)
with devuelve un punto nuevo con el cambio; el original no se toca. Si necesitas que los datos sí se puedan cambiar, existe Struct.
- Heredas solo para reutilizar código. Si la subclase no «es un» tipo de la madre, la herencia ata las dos clases para siempre. Un módulo o un objeto que la clase usa (composición) suele encajar mejor. Más sobre clases pequeñas y con una sola responsabilidad en clases en código limpio.
Ejercicio de cinco minutos
- Copia el test de
Cuentay añade un métodoretirar(importe). Escribe primero un test que compruebe que retirar 3 de una cuenta con 10 deja 7. - Cambia
privateporprotecteddelante deattr_writer. ¿Sigue pasando el tercer test? ¿Por qué? - Añade un test que compare dos cuentas:
assert_equal Cuenta.new(10), Cuenta.new(10). Falla, porque dos objetos distintos no son iguales aunque tengan el mismo saldo, y el mensaje muestra algo como#<Cuenta:0xXXXXXX @saldo=10>. Define uninspectque devuelva#<Cuenta saldo=10>y fíjate en cómo cambia el mensaje. Para que pase, define==comparando los saldos.
Para seguir, mira los métodos de clase en Ruby (self.metodo) y las variables de clase.
Fuentes
Comprobado en noviembre de 2026 en la documentación oficial de Ruby 4.0: clases y módulos (herencia; super sin argumentos usa los del método hijo; un método privado se puede llamar con self como receptor), Module (attr_accessor, private, instance_methods), Object (to_s, inspect, respond_to?, object_id) y Data (define y with).
Los ejemplos del artículo original se ejecutaron el 7 de noviembre de 2026 con Ruby 4.0.9 (imagen oficial ruby:latest de Docker). Los nuevos (to_s e inspect, el setter privado, los errores frecuentes, el test y Data) se han ejecutado el 11 de noviembre de 2026 en Docker con las imágenes ruby:3.4 (Ruby 3.4.12, Minitest 5.25.4) y ruby:4.0 (Ruby 4.0.9, Minitest 6.0.0), con la misma salida en las dos salvo el formato de la traza del ArgumentError, que en Ruby 4.0 añade el fragmento de la llamada y de la definición. Las salidas de error están recortadas a las líneas que importan; las direcciones como 0x00007cd7420730c8 cambian en cada ejecución.