Los Companion Objects para manejar los Static y Factories en Kotlin

Video thumbnail

Los companion object en Kotlin vienen siendo, en definitiva, el mecanismo que tenemos para crear propiedades o métodos estáticos dentro de las clases. Así de simple.

Es importante entender que en Kotlin no existe la palabra reservada static como en otros lenguajes. Por lo tanto, no podemos crear directamente instancias únicas (singletons) usando static. En su lugar, Kotlin nos ofrece una estructura mucho más flexible llamada companion object, que es justamente lo que vamos a tratar en este artículo.

Los companion objects que viene siendo la forma en la que Kotlin trabaja con los static de Java; otro enfoque que le puedes colocar, son las famosas clases tipo singleton que son empleadas mucho pero mucho en las aplicaciones como lo es el caso de Android que es nuestro especial interés.

Partimos que ya sabemos como crear las clases enumeradas en Kotlin.

¿Qué es un Companion Object?

Un companion object siempre vive dentro de una clase.

Esto es importante recordarlo.

Hasta ahora, cuando queríamos usar una clase, hacíamos lo siguiente:

  • Definimos la clase.
  • Creamos una instancia.
  • Accedemos a sus métodos o propiedades.

Sin embargo, los métodos o propiedades estáticas no requieren instancias. Se acceden directamente desde la clase.

Como Kotlin no tiene estáticos, esto se maneja mediante companion object, que viene siendo el equivalente.

¿Para qué usamos Companion Object?

Principalmente, yo veo dos usos clave:

  • Crear métodos y propiedades estáticas
  • Usarlos como Factory, es decir, para crear constructores especiales

Por lo demás, la sintaxis puede variar un poco dependiendo del caso, pero vamos a ver varios ejemplos prácticos para que quede claro.

¿Cuándo tiene sentido usarlo?

Información estática

Un buen ejemplo es cuando manejas datos que no cambian:

  • Tipos de notificaciones (success, error, warning, etc.).
  • Configuraciones globales.
  • Datos únicos en toda la aplicación.

Por ejemplo, en una aplicación normalmente solo tienes un usuario autenticado.

No tiene sentido estar creando instancias del usuario todo el tiempo.

Lo lógico es:

  • Al iniciar la aplicación, traer la información del usuario (por ejemplo, desde la base de datos).
  • Guardarla en una estructura estática.
  • Acceder a ella desde cualquier parte de la app.

Ahí es donde un companion object encaja perfectamente.

En estos casos, no tiene sentido estar creando instancias constantemente. Lo ideal es cargar la información una sola vez y poder acceder a ella desde cualquier parte de la aplicación.

Sintaxis básica

La sintaxis es bastante simple:

  • El companion object va dentro de la clase.
  • El nombre es opcional.
  • Dentro de él puedes definir todas las propiedades y métodos que quieras que sean estáticos.

Vamos a ver ejemplos prácticos, pero antes quiero explicarte cuándo tiene sentido usar esto.

Ejemplos del funcionamiento del Companion Objects en Kotlin

Ejemplo con datos estáticos

En el ejemplo que ves, tenemos información como arrays, funciones auxiliares y métodos que no cambian.

No tiene sentido crear instancias cada vez que queremos acceder a estos datos, porque son datos planos, estáticos.

A diferencia del ejemplo de Persona, donde sí tiene sentido crear instancias porque los datos varían (nombre, edad, etc.).

Para emplear los Companion Objects en Kotlin los mismos deben ser declarados dentro de la instancia de una clase de la siguiente forma:

class Persona {
   companion object Raza {
       // clasificacion persona
       val edades: IntArray = intArrayOf(0, 2, 30, 60)
       var razas =
               arrayOf("Europeo", "Asiatico", "Latino", "Arabe", "Africano", "Americano", "Canadiense")
       var clasificacion = "Clasificación personas"
       init {
           println("Inicializando el companion")
       }
       fun enAmerica(): Array<String> {
           return arrayOf(razas[2], razas[5], razas[6])
       }
       fun adultoMayor(): Int {
           return edades[3]
       }
       fun printClasificacion() {
           println(clasificacion)
       }
   }
}

En primera instancia, pareciera que emplear una estructura más compleja, ya que estamos anidando elementos, en este caso nuestra clase dentro de un Companion Object, y es verdad, pero gana en legibilidad, ya que es muy común que queramos definir un bloque importante como static, y no tenemos que estar colocando cada rato la palabra static para definir múltiples funciones, o parámetros como hacíamos en Java.

Con el englobamiento que hacemos con el Companion Object es más que suficiente para englobar todos nuestros métodos y propiedades estáticas de una sola vez; en Java tendríamos que hacer lo siguiente.

public static final String KEY = "key";

Ahora, si tenemos varias estructuras y variables que queramos que sean static, entonces todo se nos complica bastante, porque tenemos que estar colocando static a cada rato, y si además tenemos métodos y atributos que no son estáticos, a la final lo que tenemos es una buena mezcla de varias cosas; cosa que no sucede con Kotlin al englobar en un bloque de código las propiedades y funciones que queramos sean estáticas mediante los Companion Objects.

En el ejemplo anterior, podemos apreciar que declaramos nuestro Companion Object dentro de la clase Persona, como principio fundamental, para crear y posteriormente usar un Companion Object el mismo debe de estar declarado dentro de una clase como hicimos anteriormente, que declaramos el Companion Object llamado Raza dentro de la clase madre Persona.

Accediendo a los métodos del Companion Object

Con el Companion Object ya declarado, creamos algunas propiedades como lo son edades y razas que son Arrays las cuales creamos método personalizados para cada ámbito en este ejemplo, es decir, creamos como ejemplo un método enAmerica() que retorna un Array con las razas nativas del continente Americano, que serían las de los latinos, los Americanos (américa del norte) y los canadienses.

Hacemos algo parecido con las edades, la cual creamos un método llamado adultoMayor() que retorna las edad en la que se considera que a partir de la misma, la persona es un adulto mayor.

Además creamos un texto mediante una variable llamada clasificacion que retorna su valor mediante el método printClasificacion.

Por último, podemos emplear un constructor definido como init en caso de que sea necesario.

Variaciones en la estructura del Companion Object para manejar los static

Podemos omitir el nombre del Companion Object de la declaración, quedando de la siguiente forma:

class Persona {
   companion object {
       // clasificacion persona
       val edades: IntArray = intArrayOf(0, 2, 30, 60)
       var razas =
               arrayOf("Europeo", "Asiatico", "Latino", "Arabe", "Africano", "Americano", "Canadiense")
       var clasificacion = "Clasificación personas"
/*Demas metodos*/
}

Para poder emplear métodos o propiedades del Companion Object empleamos el siguiente esquema; tomando como ejemplo el código anterior, tenemos que:

println(Persona.enAmerica()[0]) println(Persona.printClasificacion()) println(Persona.adultoMayor()) }

Accediendo a las propiedades del Companion Object

Para referenciar los métodos que vimos anteriormente; por supuesto, podemos referencias directamente las propiedades como hemos hecho en anteriores entradas, y esto es excelente, ya que no tenemos que estar creados los famosos métodos get y set para cada propiedad de nuestro objeto; con declarar la propiedad y su tipo es condición necesaria y suficiente:

Persona.edades
Persona.razas
Persona.clasificacion

Como podemos ver, debemos emplear el nombre de la clase madre seguido del método o propiedad definido dentro del Companion Object, es el mismo enfoque que el realizado en Java al momento de referenciar a un método o propiedad de una clase estática en Java; es decir, primero referenciamos el nombre de la clase para luego referenciar el método o propiedad que del Companion Object sin la necesidad de crear un objeto de la clase.

Métodos de la clase madre del Companion Object

Podemos definir la estructura de nuestra clase como queramos, tomando como referencia la estructura que vimos en una sobre el uso de las clases.

Tenemos que nuestro código quedaría así:

class Persona {
   var nombre: String = ""
   var apellido: String = ""
   var edad: Int = 0
   init {
       this.nombre = nombre
       this.apellido = apellido
       this.edad = edad
   }
   companion object Raza {
       // clasificacion persona
       val edades: IntArray = intArrayOf(0, 2, 30, 60)
       var razas =
               arrayOf("Europeo", "Asiatico", "Latino", "Arabe", "Africano", "Americano", "Canadiense")
       var clasificacion = "Clasificación personas"
       init {
           println("Inicializando el companion")
       }
       fun enAmerica(): Array<String> {
           return arrayOf(razas[2], razas[5], razas[6])
       }
       fun adultoMayor(): Int {
           return edades[3]
       }
       fun printClasificacion() {
           println(clasificacion)
       }
   }
}

Ejemplo: Base de datos

Un ejemplo muy clásico es una base de datos.

Los parámetros de conexión:

  • Versión
  • Nombre
  • Configuración

Aquí tenemos un ejemplo sencillo de una clase con un companion object:

class BaseDeDatos {
    companion object {
        const val VERSION = 1
        
        fun obtenerNombre(): String {
            return "MiApp_DB"
        }
    }
}

Para usarlo, no creamos una instancia:

println(BaseDeDatos.VERSION)
println(BaseDeDatos.obtenerNombre())

Esto tiene todo el sentido del mundo, ya que esta información no va a variar durante la ejecución del programa.
A diferencia, por ejemplo, de los datos del usuario, que sí pueden cambiar (edad, nombre, etc.).

No deberían cambiar durante la ejecución del programa.

Por eso:

  • Creamos la clase.
  • Definimos un companion object sin nombre (porque no lo necesitamos).
  • Colocamos ahí la información estática.

Para acceder a ella, simplemente hacemos:

  • Database.version
  • Database.getName()

Sin crear instancias.

Tiene todo el sentido del mundo que estas propiedades sean constantes, ya que no varían durante la ejecución.

Factory: Constructores especiales

Ahora vamos con otro uso muy importante: los factory.

¿Qué es un Factory?

Un factory (fábrica) es una estructura que nos permite crear objetos de forma controlada.

Esto está muy ligado a la programación orientada a objetos, donde usamos clases para representar objetos del mundo real:
un usuario, un carro, una casa, una pizza.

Otro uso extremadamente común del companion object es como Factory, es decir, como una fábrica de objetos.

Ejemplo de la pizza

Imagina una pizzería:

  • Hay un menú.
  • Puedes pedir pizza de pepperoni, queso o margarita.
  • No puedes pedir cosas que no existen.

Cuando tú pides una pizza:

  • El proceso ya está optimizado.
  • Los ingredientes ya están preparados.
  • No se empieza desde cero cada vez.

Eso es exactamente lo que hace un factory.

Aplicado a programación

Supongamos que tienes usuarios:

  • Usuario normal.
  • Usuario administrador.
  • Usuario invitado.

No tiene sentido repetir la misma lógica de validación en todos los módulos de la aplicación:

  • Dashboard
  • Registro
  • API
  • App móvil

Un factory centraliza toda esa lógica en un solo lugar.

class User private constructor(
    val name: String,
    val role: String,
    val permissions: List<String>
) {
    companion object {
        fun createGuest(name: String): User {
            return User(name, "Guest", listOf("Read"))
        }
        fun createAdmin(name: String): User {
            return User(name, "Admin", listOf("Read", "Write", "Delete"))
        }
    }
    fun showInfo() {
        println("Usuario: $name | Rol: $role | Permisos: $permissions")
    }
}

Aquí el constructor es privado, lo que evita que se creen usuarios inválidos.

Toda la lógica queda centralizada en un solo lugar.

val guest = User.createGuest("Carlos")
val admin = User.createAdmin("Ana")
guest.showInfo()
admin.showInfo()

Esto evita tener validaciones repetidas en distintos módulos de la aplicación.

Factory con Companion Object

En el ejemplo:

  • El constructor es privado.
  • Nadie puede crear usuarios directamente.
  • Solo se pueden crear mediante los métodos del companion object.

Esto evita errores como:

  • Roles inválidos.
  • Permisos incorrectos.
  • Usuarios mal configurados.

Cada método del factory crea un usuario con una estructura válida y bien definida.

Ventajas del enfoque

  • Toda la lógica está en un solo lugar.
  • Evitas duplicar validaciones.
  • El código es más limpio y mantenible.
  • Es imposible crear objetos inválidos por accidente.

Exactamente igual que en la pizzería:

  • No vas al supermercado cada vez que alguien pide una pizza.

Singleton + Factory

En este caso:

  • El companion object actúa como factory.
  • La clase puede tener múltiples instancias.
  • Pero la lógica de creación es única.

Luego, una vez creada la instancia, puedes acceder a los métodos normales de la clase.

Factory con Singleton: Notificaciones

Finalmente, Kotlin también ofrece la palabra clave object, que crea un singleton real.

A diferencia del companion object:

  • No está dentro de una clase.
  • Existe una sola instancia en toda la aplicación.

Esto es muy útil cuando quieres crear, por ejemplo:

  • Un NotificationFactory.
  • Un gestor global de servicios.

Aquí combinamos Factory + Singleton usando object:

 
interface Notificacion {
    fun enviar(mensaje: String)
}
class NotificacionEmail : Notificacion {
    override fun enviar(mensaje: String) =
        println("Enviando Email: $mensaje")
}
class NotificacionSMS : Notificacion {
    override fun enviar(mensaje: String) =
        println("Enviando SMS: $mensaje")
}
object NotificacionFactory {
    fun crearNotificacion(tipo: String): Notificacion {
        return when (tipo) {
            "EMAIL" -> NotificacionEmail()
            "SMS" -> NotificacionSMS()
            else -> throw IllegalArgumentException("Tipo desconocido")
        }
    }
}

Uso:

val notif = NotificacionFactory.crearNotificacion("EMAIL")
notif.enviar("¡Hola desde la Factory!")

Aquí tenemos:

  • Una instancia única (object)
  • Capacidad de crear múltiples tipos de objetos
  • Una interfaz común (Notificacion) que garantiza un método compartido

Resumen

Aquí vemos un caso más avanzado:

  • Una interfaz Notification.
  • Implementaciones distintas: email, SMS, push.
  • Un singleton que decide qué tipo de notificación crear.

Desde fuera, tú solo llamas:

  • createNotification(type)
  • send(message)

No te importa cómo funciona internamente cada tipo de notificación.

Ventaja principal

La gran ventaja de todo este enfoque es que:

  • Centralizas la lógica.
  • Trabajas con estructuras claras.
  • Evitas errores.
  • El código es fácil de leer y mantener.

Resumen rápido

  • Kotlin no tiene static
  • companion object es su equivalente
  • Sirve para:
    • Métodos y propiedades estáticas
    • Singletons
    • Factory Methods
  • Centraliza lógica
  • Evita errores y duplicación
  • Hace el código más limpio y mantenible

Conclusión

  • companion object: estáticos dentro de una clase.
  • object: singleton real.
  • Factory: creación controlada de objetos.
  • Todo se puede combinar según el caso.

Espero que ahora quede mucho más claro cómo funciona cada uno y cuándo usar cada patrón.

Todos los ejemplos que vimos anteriormente son clases Singleton, que por definición son clases con una sola instancia en todo el ámbito de la aplicación; son ampliamente utilizadas para tener una sola conexión con la base de datos y evitar saturar o las famosas condiciones de carrera (cómo nos podría pasar en Android con las base de datos de SQLite), datos de usuarios y un largo etc.

Como puedes ver, los Companion Objects son muy útiles cuando trabajamos con estructuras que deben tener una instancia única, como dados de usuarios, o datos de conexión a la base de datos en Android.

Puedes consultar la documentación oficial en el siguiente enlace: Companion Objects

El siguiente paso, Los arrays y listas en Kotlin: Primeros pasos estas estructuras mutables e inmutables.

Vamos a hablar sobre los companion objects que viene siendo la forma en la que Kotlin trabaja con los static y de crear métodos factories y singletons.

Acepto recibir anuncios de interes sobre este Blog.

Andrés Cruz

EN In english