R

R

Tutorial R: Sistema S3: clases implícitas y métodos genéricos

Aprende a crear clases S3 y métodos genéricos en R para programación orientada a objetos flexible y polimórfica con ejemplos prácticos.

Aprende R y certifícate

Creación de clases S3 con atributos

El sistema S3 es el mecanismo más sencillo y ampliamente utilizado para implementar programación orientada a objetos en R. A diferencia de otros lenguajes como Java o Python, R no tiene una sintaxis formal para definir clases. En su lugar, el sistema S3 utiliza un enfoque más flexible y menos estructurado.

En S3, una "clase" es simplemente una lista de R con un atributo especial llamado "class". Este enfoque minimalista hace que crear objetos S3 sea muy accesible, incluso para programadores que están comenzando con R.

Estructura básica de una clase S3

Para crear una clase S3, seguimos estos pasos fundamentales:

  1. Crear una lista con los atributos (elementos) que queremos que tenga nuestro objeto
  2. Asignar un nombre de clase mediante el atributo class

La sintaxis básica es la siguiente:

# Creación de un objeto S3
mi_objeto <- list(
  atributo1 = valor1,
  atributo2 = valor2,
  ...
)
class(mi_objeto) <- "nombre_clase"

Veamos un ejemplo concreto creando una clase persona:

# Creando un objeto de clase "persona"
ana <- list(
  nombre = "Ana",
  edad = 28,
  profesion = "Estadística"
)
class(ana) <- "persona"

# Verificamos la clase del objeto
class(ana)  # [1] "persona"

En este ejemplo, hemos creado un objeto llamado ana que pertenece a la clase persona. El objeto contiene tres atributos: nombre, edad y profesión.

Funciones constructoras

Aunque podemos crear objetos S3 directamente como se mostró arriba, es una buena práctica definir una función constructora. Esto nos permite:

  • Estandarizar la creación de objetos
  • Validar los datos de entrada
  • Establecer valores predeterminados

Veamos cómo crear una función constructora para nuestra clase persona:

# Función constructora para la clase "persona"
crear_persona <- function(nombre, edad, profesion = "No especificada") {
  # Validación básica
  if (!is.character(nombre)) {
    stop("El nombre debe ser una cadena de texto")
  }
  if (!is.numeric(edad) || edad < 0) {
    stop("La edad debe ser un número positivo")
  }
  
  # Crear el objeto
  persona <- list(
    nombre = nombre,
    edad = edad,
    profesion = profesion
  )
  
  # Asignar la clase
  class(persona) <- "persona"
  
  return(persona)
}

# Uso de la función constructora
juan <- crear_persona("Juan", 35, "Programador")
maria <- crear_persona("María", 42)

# Verificamos
juan
# $nombre
# [1] "Juan"
# 
# $edad
# [1] 35
# 
# $profesion
# [1] "Programador"
# 
# attr(,"class")
# [1] "persona"

Nuestra función constructora crear_persona hace varias cosas importantes:

  • Valida los datos de entrada para asegurarse de que son del tipo correcto
  • Proporciona un valor predeterminado para el atributo profesion
  • Estandariza la estructura del objeto
  • Asigna la clase correcta al objeto

Acceso a los atributos

Podemos acceder a los atributos de un objeto S3 de la misma manera que accedemos a los elementos de una lista en R:

# Acceso a atributos usando $
nombre_juan <- juan$nombre
edad_juan <- juan$edad

# Acceso a atributos usando [[]]
profesion_juan <- juan[["profesion"]]

# Imprimimos los valores
nombre_juan      # [1] "Juan"
edad_juan        # [1] 35
profesion_juan   # [1] "Programador"

Modificación de atributos

También podemos modificar los atributos de un objeto S3 como lo haríamos con una lista:

# Modificar la edad de Juan
juan$edad <- 36

# Añadir un nuevo atributo
juan$ciudad <- "Barcelona"

# Ver el objeto modificado
juan
# $nombre
# [1] "Juan"
# 
# $edad
# [1] 36
# 
# $profesion
# [1] "Programador"
# 
# $ciudad
# [1] "Barcelona"
# 
# attr(,"class")
# [1] "persona"

Clases S3 con múltiples clases

Un objeto S3 puede pertenecer a múltiples clases simultáneamente. Esto se logra asignando un vector de caracteres al atributo class:

# Crear un objeto con múltiples clases
empleado <- crear_persona("Carlos", 45, "Analista de datos")
class(empleado) <- c("empleado", "persona")

# Verificar las clases
class(empleado)  # [1] "empleado" "persona"

En este caso, el objeto empleado pertenece tanto a la clase empleado como a la clase persona. El orden de las clases es importante, ya que determina la prioridad en el sistema de dispatching de métodos (que veremos en otra sección).

Verificación de clases

Para verificar si un objeto pertenece a una clase específica, podemos usar la función inherits():

# Verificar si un objeto pertenece a una clase
inherits(juan, "persona")     # TRUE
inherits(empleado, "persona") # TRUE
inherits(empleado, "empleado") # TRUE
inherits(juan, "empleado")    # FALSE

Ejemplo práctico: clase para representar puntos 2D

Veamos un ejemplo más práctico creando una clase para representar puntos en un plano bidimensional:

# Función constructora para la clase "punto2d"
crear_punto2d <- function(x = 0, y = 0) {
  # Validación
  if (!is.numeric(x) || !is.numeric(y)) {
    stop("Las coordenadas deben ser valores numéricos")
  }
  
  # Crear el objeto
  punto <- list(
    x = x,
    y = y
  )
  
  # Asignar la clase
  class(punto) <- "punto2d"
  
  return(punto)
}

# Crear algunos puntos
origen <- crear_punto2d()
p1 <- crear_punto2d(3, 4)
p2 <- crear_punto2d(-1, 2)

# Función para calcular la distancia al origen
distancia_origen <- function(punto) {
  if (!inherits(punto, "punto2d")) {
    stop("El objeto debe ser de clase 'punto2d'")
  }
  
  return(sqrt(punto$x^2 + punto$y^2))
}

# Calcular distancias
distancia_origen(p1)  # [1] 5
distancia_origen(p2)  # [1] 2.236068

En este ejemplo, hemos creado:

  1. Una función constructora crear_punto2d que valida y crea objetos de la clase punto2d
  2. Una función auxiliar distancia_origen que calcula la distancia de un punto al origen
  3. La función auxiliar verifica que el objeto sea de la clase correcta antes de operar con él

Atributos adicionales con attr()

Además de los elementos de la lista que representan los atributos principales, podemos añadir atributos adicionales usando la función attr():

# Añadir un atributo adicional a un punto
attr(p1, "color") <- "rojo"
attr(p1, "etiqueta") <- "Punto A"

# Ver los atributos
attributes(p1)
# $x
# [1] 3
# 
# $y
# [1] 4
# 
# $class
# [1] "punto2d"
# 
# $color
# [1] "rojo"
# 
# $etiqueta
# [1] "Punto A"

# Acceder a un atributo específico
attr(p1, "color")  # [1] "rojo"

Los atributos adicionales pueden ser útiles para almacenar metadatos o información complementaria sobre el objeto.

Resumen

Las clases S3 en R son una forma sencilla pero potente de implementar programación orientada a objetos:

  • Se basan en listas con un atributo especial class
  • Es recomendable crear funciones constructoras para estandarizar la creación de objetos
  • Podemos acceder y modificar los atributos como en cualquier lista
  • Un objeto puede pertenecer a múltiples clases
  • Podemos añadir atributos adicionales usando la función attr()

Esta flexibilidad hace que el sistema S3 sea muy accesible para principiantes, a la vez que proporciona una base sólida para desarrollar código orientado a objetos en R.

Implementación de métodos genéricos

Los métodos genéricos son una parte fundamental del sistema S3 en R. Estos métodos permiten que una misma función se comporte de manera diferente según la clase del objeto con el que trabaja, implementando así el concepto de polimorfismo de la programación orientada a objetos.

En R, un método genérico es simplemente una función que examina la clase de su primer argumento y llama a la función específica (método) apropiada para esa clase. Esta capacidad permite escribir código más limpio y extensible.

Estructura de los métodos genéricos

Un método genérico en S3 tiene dos componentes principales:

  • La función genérica que sirve como punto de entrada
  • Los métodos específicos para cada clase

La convención de nomenclatura para los métodos específicos es:

nombre_funcion.nombre_clase

Por ejemplo, si tenemos una función genérica llamada imprimir y una clase llamada persona, el método específico se llamaría imprimir.persona.

Creando un método genérico desde cero

Veamos cómo crear un método genérico simple para nuestra clase persona:

# Función genérica
describir <- function(x, ...) {
  UseMethod("describir")
}

# Método por defecto
describir.default <- function(x, ...) {
  cat("Objeto de clase:", class(x)[1], "\n")
}

# Método específico para la clase "persona"
describir.persona <- function(x, detallado = FALSE, ...) {
  cat("Persona:", x$nombre, "\n")
  cat("Edad:", x$edad, "años\n")
  
  if (detallado) {
    cat("Profesión:", x$profesion, "\n")
  }
}

# Probemos con nuestros objetos
ana <- list(nombre = "Ana", edad = 28, profesion = "Estadística")
class(ana) <- "persona"

numeros <- c(1, 2, 3, 4, 5)

describir(ana)
# Persona: Ana 
# Edad: 28 años

describir(ana, TRUE)
# Persona: Ana 
# Edad: 28 años
# Profesión: Estadística

describir(numeros)
# Objeto de clase: numeric

En este ejemplo:

  1. Creamos la función genérica describir que llama a UseMethod("describir")
  2. Definimos un método por defecto describir.default que se usa cuando no hay un método específico para la clase del objeto
  3. Implementamos un método específico describir.persona para objetos de clase "persona"

La función UseMethod() es la que hace la magia del dispatching: examina la clase del primer argumento y llama al método apropiado.

Utilizando métodos genéricos existentes

R viene con varios métodos genéricos predefinidos que podemos extender para nuestras propias clases. Algunos de los más comunes son:

  • print(): Para mostrar un objeto
  • summary(): Para obtener un resumen de un objeto
  • plot(): Para visualizar un objeto
  • [: Para la indexación

Implementemos algunos de estos para nuestra clase punto2d:

# Primero, recreamos nuestra clase punto2d
crear_punto2d <- function(x = 0, y = 0) {
  punto <- list(x = x, y = y)
  class(punto) <- "punto2d"
  return(punto)
}

# Método print para punto2d
print.punto2d <- function(x, ...) {
  cat("Punto en coordenadas (", x$x, ",", x$y, ")\n", sep = "")
  invisible(x)
}

# Método summary para punto2d
summary.punto2d <- function(object, ...) {
  distancia <- sqrt(object$x^2 + object$y^2)
  cuadrante <- ifelse(object$x >= 0,
                     ifelse(object$y >= 0, 1, 4),
                     ifelse(object$y >= 0, 2, 3))
  
  cat("Resumen del punto:\n")
  cat("Coordenadas: (", object$x, ",", object$y, ")\n", sep = "")
  cat("Distancia al origen:", round(distancia, 2), "\n")
  cat("Cuadrante:", cuadrante, "\n")
}

# Creamos algunos puntos
p1 <- crear_punto2d(3, 4)
p2 <- crear_punto2d(-2, 5)

# Probamos nuestros métodos
p1  # Usa print.punto2d automáticamente
# Punto en coordenadas (3,4)

summary(p2)
# Resumen del punto:
# Coordenadas: (-2,5)
# Distancia al origen: 5.39
# Cuadrante: 2

Observa que no necesitamos llamar explícitamente a print.punto2d() o summary.punto2d(). Al usar print() o summary() con un objeto de clase punto2d, R automáticamente selecciona el método correcto.

Implementando operadores como métodos genéricos

Los operadores en R también pueden ser métodos genéricos. Por ejemplo, podemos implementar el operador + para nuestra clase punto2d:

# Método para sumar dos puntos
"+.punto2d" <- function(e1, e2) {
  # Si el segundo argumento es otro punto2d
  if (inherits(e2, "punto2d")) {
    return(crear_punto2d(e1$x + e2$x, e1$y + e2$y))
  }
  
  # Si el segundo argumento es un número (escalar)
  if (is.numeric(e2) && length(e2) == 1) {
    return(crear_punto2d(e1$x + e2, e1$y + e2))
  }
  
  stop("No se puede sumar un punto2d con este tipo de objeto")
}

# Probamos la suma de puntos
p1 <- crear_punto2d(3, 4)
p2 <- crear_punto2d(1, 2)
p3 <- p1 + p2
p3
# Punto en coordenadas (4,6)

# Probamos la suma con un escalar
p4 <- p1 + 10
p4
# Punto en coordenadas (13,14)

En este ejemplo, hemos sobrecargado el operador + para que funcione con objetos de clase punto2d. Nuestro método maneja dos casos:

  1. Suma de dos puntos (coordenada a coordenada)
  2. Suma de un punto y un escalar (añade el escalar a ambas coordenadas)

Métodos genéricos con múltiples argumentos

Aunque el dispatching en S3 se basa principalmente en la clase del primer argumento, podemos manejar diferentes combinaciones de clases dentro de nuestros métodos:

# Creamos una nueva clase para representar vectores
crear_vector2d <- function(x = 0, y = 0) {
  vector <- list(x = x, y = y)
  class(vector) <- "vector2d"
  return(vector)
}

# Método print para vector2d
print.vector2d <- function(x, ...) {
  cat("Vector <", x$x, ",", x$y, ">\n", sep = "")
  invisible(x)
}

# Método para sumar un punto y un vector
"+.punto2d" <- function(e1, e2) {
  # Si el segundo argumento es otro punto2d
  if (inherits(e2, "punto2d")) {
    return(crear_punto2d(e1$x + e2$x, e1$y + e2$y))
  }
  
  # Si el segundo argumento es un vector2d
  if (inherits(e2, "vector2d")) {
    return(crear_punto2d(e1$x + e2$x, e1$y + e2$y))
  }
  
  # Si el segundo argumento es un número (escalar)
  if (is.numeric(e2) && length(e2) == 1) {
    return(crear_punto2d(e1$x + e2, e1$y + e2))
  }
  
  stop("No se puede sumar un punto2d con este tipo de objeto")
}

# Probamos la suma de un punto y un vector
p1 <- crear_punto2d(3, 4)
v1 <- crear_vector2d(2, -1)
p5 <- p1 + v1
p5
# Punto en coordenadas (5,3)

En este ejemplo, hemos ampliado nuestro método +.punto2d para manejar también objetos de clase vector2d.

Métodos genéricos para clases múltiples

Recordemos que un objeto S3 puede pertenecer a múltiples clases. Cuando llamamos a un método genérico con un objeto de múltiples clases, R busca un método específico para cada clase en orden:

# Creamos un objeto con múltiples clases
punto_especial <- crear_punto2d(1, 1)
class(punto_especial) <- c("punto_especial", "punto2d")

# Método específico para la nueva clase
print.punto_especial <- function(x, ...) {
  cat("¡Punto especial en (", x$x, ",", x$y, ")!\n", sep = "")
  invisible(x)
}

# Probamos
punto_especial
# ¡Punto especial en (1,1)!

# Si no existiera print.punto_especial, R usaría print.punto2d

R primero busca print.punto_especial, y como existe, lo usa. Si no existiera, buscaría print.punto2d, y así sucesivamente.

Ejemplo práctico: clase para datos estadísticos

Veamos un ejemplo más completo creando una clase para manejar datos estadísticos básicos:

# Función constructora
crear_estadisticas <- function(datos) {
  if (!is.numeric(datos)) {
    stop("Los datos deben ser numéricos")
  }
  
  # Calculamos estadísticas básicas
  stats <- list(
    datos = datos,
    n = length(datos),
    media = mean(datos),
    mediana = median(datos),
    desv_est = sd(datos),
    min = min(datos),
    max = max(datos)
  )
  
  class(stats) <- "estadisticas"
  return(stats)
}

# Método print
print.estadisticas <- function(x, ...) {
  cat("Estadísticas de", x$n, "observaciones\n")
  cat("Media:", round(x$media, 2), "\n")
  cat("Mediana:", round(x$mediana, 2), "\n")
  cat("Desviación estándar:", round(x$desv_est, 2), "\n")
  cat("Rango: [", x$min, ", ", x$max, "]\n", sep = "")
  invisible(x)
}

# Método summary (más detallado que print)
summary.estadisticas <- function(object, ...) {
  cuartiles <- quantile(object$datos)
  cat("RESUMEN ESTADÍSTICO\n")
  cat("===================\n")
  cat("Número de observaciones:", object$n, "\n\n")
  cat("Mínimo:", object$min, "\n")
  cat("1er Cuartil:", cuartiles[2], "\n")
  cat("Mediana:", object$mediana, "\n")
  cat("Media:", round(object$media, 2), "\n")
  cat("3er Cuartil:", cuartiles[4], "\n")
  cat("Máximo:", object$max, "\n\n")
  cat("Desviación estándar:", round(object$desv_est, 2), "\n")
  cat("Coeficiente de variación:", round(object$desv_est/object$media, 2), "\n")
}

# Método plot
plot.estadisticas <- function(x, ...) {
  par(mfrow = c(1, 2))
  hist(x$datos, main = "Histograma", xlab = "Valor", col = "lightblue")
  boxplot(x$datos, main = "Diagrama de caja", col = "lightgreen")
  par(mfrow = c(1, 1))
}

# Probamos nuestra clase
set.seed(123)  # Para reproducibilidad
datos_ejemplo <- rnorm(100, mean = 50, sd = 10)
stats <- crear_estadisticas(datos_ejemplo)

# Imprimimos las estadísticas
stats
# Estadísticas de 100 observaciones
# Media: 50.08
# Mediana: 49.68
# Desviación estándar: 9.3
# Rango: [25.3, 77.08]

# Resumen detallado
summary(stats)
# RESUMEN ESTADÍSTICO
# ===================
# Número de observaciones: 100 
# 
# Mínimo: 25.30167
# 1er Cuartil: 44.27957
# Mediana: 49.68397
# Media: 50.08
# 3er Cuartil: 56.36864
# Máximo: 77.08747
# 
# Desviación estándar: 9.3
# Coeficiente de variación: 0.19

# Visualización
# plot(stats)  # Esto generaría un histograma y un diagrama de caja

En este ejemplo completo, hemos:

  1. Creado una clase estadisticas con una función constructora
  2. Implementado tres métodos genéricos: print, summary y plot
  3. Cada método proporciona una forma diferente de interactuar con nuestros datos

Ventajas de los métodos genéricos

Los métodos genéricos ofrecen varias ventajas:

  • Polimorfismo: La misma función puede comportarse de manera diferente según el tipo de objeto
  • Extensibilidad: Podemos añadir soporte para nuevas clases sin modificar el código existente
  • Consistencia: Los usuarios pueden usar las mismas funciones familiares con diferentes tipos de objetos
  • Legibilidad: El código es más limpio y expresivo

Estas características hacen que los métodos genéricos sean una herramienta poderosa para escribir código orientado a objetos en R.

Dispatching y UseMethod()

El dispatching es el mecanismo central que permite al sistema S3 de R implementar el polimorfismo. Este proceso determina qué método específico debe ejecutarse cuando llamamos a una función genérica. El corazón de este mecanismo es la función UseMethod(), que actúa como un distribuidor de llamadas hacia los métodos apropiados según la clase del objeto.

Funcionamiento básico del dispatching

Cuando llamamos a una función genérica en R, ocurre la siguiente secuencia:

  1. La función genérica recibe los argumentos
  2. La función llama a UseMethod() con el nombre de la función
  3. UseMethod() examina la clase del primer argumento
  4. Se busca y ejecuta el método específico correspondiente

Veamos un ejemplo sencillo para entender este proceso:

# Definimos una función genérica simple
saludar <- function(x, ...) {
  UseMethod("saludar")
}

# Método por defecto
saludar.default <- function(x, ...) {
  cat("Hola, objeto desconocido\n")
}

# Método para la clase "character"
saludar.character <- function(x, ...) {
  cat("Hola,", x, "\n")
}

# Probamos con diferentes tipos de objetos
saludar("María")      # Usa saludar.character
# Hola, María

saludar(42)           # Usa saludar.default
# Hola, objeto desconocido

En este ejemplo, cuando llamamos a saludar("María"), R detecta que el primer argumento es de clase "character" y busca el método saludar.character. Al encontrarlo, lo ejecuta con los argumentos proporcionados.

Anatomía de UseMethod()

La función UseMethod() toma dos argumentos:

UseMethod(generic, object)

Donde:

  • generic: Es el nombre de la función genérica (como cadena de texto)
  • object: Es opcional y especifica el objeto para el dispatching (por defecto es el primer argumento)

En la mayoría de los casos, solo necesitamos proporcionar el primer argumento:

mi_funcion <- function(x, ...) {
  UseMethod("mi_funcion")
}

Reglas de búsqueda de métodos

Cuando UseMethod() busca el método apropiado, sigue estas reglas de prioridad:

  1. Busca un método para la primera clase del objeto
  2. Si no lo encuentra, busca para la segunda clase (si existe)
  3. Continúa con las demás clases en orden
  4. Si no encuentra ningún método específico, usa el método .default
  5. Si no existe un método .default, genera un error

Veamos un ejemplo con múltiples clases:

# Creamos un objeto con múltiples clases
obj <- structure(list(), class = c("tipo_a", "tipo_b", "tipo_c"))

# Definimos una función genérica
describir <- function(x, ...) {
  UseMethod("describir")
}

# Métodos para diferentes clases
describir.tipo_a <- function(x, ...) {
  cat("Soy un objeto de tipo A\n")
}

describir.tipo_b <- function(x, ...) {
  cat("Soy un objeto de tipo B\n")
}

describir.default <- function(x, ...) {
  cat("Soy un objeto de tipo desconocido\n")
}

# Probamos el dispatching
describir(obj)  # Usa describir.tipo_a
# Soy un objeto de tipo A

# Cambiamos el orden de las clases
class(obj) <- c("tipo_c", "tipo_b", "tipo_a")
describir(obj)  # Ahora usa describir.tipo_b
# Soy un objeto de tipo B

Este ejemplo muestra claramente cómo el orden de las clases afecta al dispatching. Cuando cambiamos el orden, R busca primero un método para "tipo_c", no lo encuentra, luego busca para "tipo_b" y lo encuentra, por lo que lo ejecuta.

Accediendo al método genérico desde un método específico

A veces, queremos que un método específico realice algunas acciones y luego llame al método de otra clase. Podemos hacerlo con NextMethod():

# Función genérica
formatear <- function(x, ...) {
  UseMethod("formatear")
}

# Método por defecto
formatear.default <- function(x, ...) {
  return(as.character(x))
}

# Método para números
formatear.numeric <- function(x, decimales = 2, ...) {
  return(format(round(x, decimales), nsmall = decimales))
}

# Método para una clase personalizada que extiende numeric
crear_porcentaje <- function(valor) {
  if (!is.numeric(valor)) stop("El valor debe ser numérico")
  class(valor) <- c("porcentaje", "numeric")
  return(valor)
}

# Método específico para porcentaje que usa NextMethod()
formatear.porcentaje <- function(x, decimales = 1, ...) {
  # Primero obtenemos el formato numérico usando el método para numeric
  resultado <- NextMethod("formatear", decimales = decimales)
  # Luego añadimos el símbolo de porcentaje
  return(paste0(resultado, "%"))
}

# Probamos
num <- 42.567
porcentaje <- crear_porcentaje(42.567)

formatear(num)         # "42.57"
formatear(porcentaje)  # "42.6%"

En este ejemplo, formatear.porcentaje llama a NextMethod(), que ejecuta el método formatear.numeric (la siguiente clase en la jerarquía). Luego, toma el resultado y le añade el símbolo de porcentaje.

Dispatching interno vs. externo

R tiene dos tipos de dispatching:

  • Dispatching interno: Ocurre dentro de funciones primitivas como [, +, etc.
  • Dispatching externo: El que implementamos con UseMethod()

La principal diferencia es que el dispatching interno es más eficiente pero menos flexible. Como programadores de R, normalmente trabajamos con el dispatching externo.

Inspección del proceso de dispatching

Para entender mejor cómo funciona el dispatching, podemos usar la función methods() para ver todos los métodos disponibles para una función genérica:

# Ver todos los métodos para la función print
methods("print")

# Ver el código fuente de un método específico
getS3method("print", "factor")

También podemos usar sloop::s3_dispatch() del paquete sloop para ver el proceso de dispatching en acción:

# Necesitamos instalar el paquete sloop
# install.packages("sloop")
library(sloop)

# Ver el proceso de dispatching
s3_dispatch(print(factor(c("a", "b"))))

Ejemplo práctico: Sistema de notificaciones

Veamos un ejemplo más completo creando un sistema simple de notificaciones:

# Función constructora base
crear_notificacion <- function(mensaje, timestamp = Sys.time()) {
  notif <- list(
    mensaje = mensaje,
    timestamp = timestamp
  )
  class(notif) <- "notificacion"
  return(notif)
}

# Funciones constructoras específicas
crear_info <- function(mensaje, timestamp = Sys.time()) {
  notif <- crear_notificacion(mensaje, timestamp)
  class(notif) <- c("info", "notificacion")
  return(notif)
}

crear_advertencia <- function(mensaje, timestamp = Sys.time()) {
  notif <- crear_notificacion(mensaje, timestamp)
  class(notif) <- c("advertencia", "notificacion")
  return(notif)
}

crear_error <- function(mensaje, timestamp = Sys.time()) {
  notif <- crear_notificacion(mensaje, timestamp)
  class(notif) <- c("error", "notificacion")
  return(notif)
}

# Función genérica para mostrar notificaciones
mostrar <- function(x, ...) {
  UseMethod("mostrar")
}

# Método por defecto
mostrar.default <- function(x, ...) {
  cat("Objeto no mostrable\n")
}

# Método base para notificaciones
mostrar.notificacion <- function(x, ...) {
  cat("[", format(x$timestamp, "%H:%M:%S"), "] ", x$mensaje, "\n", sep = "")
}

# Métodos específicos
mostrar.info <- function(x, ...) {
  cat("INFO: ")
  NextMethod()
}

mostrar.advertencia <- function(x, ...) {
  cat("ADVERTENCIA: ")
  NextMethod()
}

mostrar.error <- function(x, ...) {
  cat("ERROR: ")
  NextMethod()
}

# Probamos el sistema
info <- crear_info("El proceso ha iniciado")
adv <- crear_advertencia("Memoria baja")
err <- crear_error("No se pudo conectar a la base de datos")

mostrar(info)
# INFO: [12:34:56] El proceso ha iniciado

mostrar(adv)
# ADVERTENCIA: [12:34:56] Memoria baja

mostrar(err)
# ERROR: [12:34:56] No se pudo conectar a la base de datos

En este ejemplo:

  1. Creamos una jerarquía de clases para diferentes tipos de notificaciones
  2. Implementamos una función genérica mostrar con métodos específicos
  3. Usamos NextMethod() para reutilizar la funcionalidad del método base
  4. El dispatching selecciona automáticamente el método correcto según la clase

Consideraciones sobre el dispatching

Al trabajar con el sistema de dispatching de S3, es importante tener en cuenta:

  • El dispatching se basa únicamente en la clase del primer argumento
  • El orden de las clases es crucial para determinar qué método se ejecuta
  • NextMethod() permite la reutilización de código entre métodos
  • Es una buena práctica proporcionar un método .default para manejar casos inesperados

Ventajas del sistema de dispatching S3

El sistema de dispatching S3 ofrece varias ventajas:

  • Simplicidad: Es fácil de entender y usar
  • Flexibilidad: Permite extender funcionalidades existentes
  • Eficiencia: Tiene una sobrecarga mínima en comparación con sistemas OOP más complejos
  • Integración: Funciona perfectamente con el resto del ecosistema de R

Estas características hacen que el sistema S3 sea ideal para muchas aplicaciones en R, especialmente para quienes están comenzando con la programación orientada a objetos.

Limitaciones del dispatching S3

A pesar de sus ventajas, el sistema S3 tiene algunas limitaciones:

  • Solo realiza dispatching basado en la clase del primer argumento
  • No hay verificación de tipos o validación de argumentos automática
  • No hay encapsulación real (los atributos son accesibles directamente)
  • No hay mecanismo formal para definir interfaces o contratos

Para aplicaciones más complejas que requieren estas características, R ofrece sistemas más avanzados como S4 y R6, pero estos están fuera del alcance de esta lección.

Aprende R online

Otros ejercicios de programación de R

Evalúa tus conocimientos de esta lección Sistema S3: clases implícitas y métodos genéricos con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.

Todas las lecciones de R

Accede a todas las lecciones de R y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Instalación De R Y Rstudio

R

Introducción Y Entorno

Introducción A R

R

Introducción Y Entorno

Operadores

R

Sintaxis

Estructuras De Datos

R

Sintaxis

Funciones

R

Sintaxis

Estructuras De Control Iterativo

R

Sintaxis

Scopes Y Closures

R

Sintaxis

Estructuras De Control Condicional

R

Sintaxis

Funciones Anónimas

R

Sintaxis

Tipos De Datos Y Variables

R

Sintaxis

Sistema R6: Clases Referenciales Y Encapsulamiento

R

Programación Orientada A Objetos

Sistema S4: Clases Formales Y Validación

R

Programación Orientada A Objetos

Herencia Y Polimorfismo En R

R

Programación Orientada A Objetos

Sistemas De Oop En R

R

Programación Orientada A Objetos

Sistema S3: Clases Implícitas Y Métodos Genéricos

R

Programación Orientada A Objetos

Tidyverse Para Transformación De Datos

R

Manipulación De Datos

Lubridate Para Fechas Y Tiempo

R

Manipulación De Datos

Group_by Y Summarize Para Agrupación Y Resumen

R

Manipulación De Datos

Stringr Para Expresiones Regulares

R

Manipulación De Datos

Tidyr Para Limpieza De Valores Faltantes

R

Manipulación De Datos

Joins En R Para Combinación Y Relaciones De Tablas

R

Manipulación De Datos

Pivot_longer Y Pivot_wider Para Reestructuración

R

Manipulación De Datos

Mutate Y Transmute Para Transformación

R

Manipulación De Datos

Dplyr Para Filtrado Y Selección

R

Manipulación De Datos

Readr Y Read.csv Para Importar Datos

R

Manipulación De Datos

Gráficos Bivariantes En R

R

Visualización De Datos

Gráficos Univariantes En R

R

Visualización De Datos

Facetas En Ggplot2

R

Visualización De Datos

Personalización Y Temas

R

Visualización De Datos

Ggplot2 Para Visualización De Datos

R

Visualización De Datos

Gráficos Multivariantes En R

R

Visualización De Datos

Correlación En R

R

Estadística

Regresión Lineal En R

R

Estadística

Pruebas De Hipótesis En R

R

Estadística

Anova En R

R

Estadística

Estadística Descriptiva En R

R

Estadística

Accede GRATIS a R y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender la estructura y creación de clases S3 mediante listas con atributo 'class'.
  • Aprender a definir funciones constructoras para estandarizar y validar objetos S3.
  • Implementar y utilizar métodos genéricos y específicos para diferentes clases.
  • Entender el mecanismo de dispatching con UseMethod() y la jerarquía de clases en S3.
  • Aplicar la sobrecarga de operadores y el uso de NextMethod() para reutilizar métodos.