Scala

Usar un Trait como interfaz en Scala

8 min lectura José Miguel

Puede que esté acostumbrad@ a crear interfaces puras en otros lenguajes, declarando métodos sin implementaciones, y desea usar un trait como interfaz en Scala y luego usar esas interfaces con clases concretas.

En lenguajes como Java (antes de la versión 8), las interfaces solo permitían declarar métodos abstractos: firmas sin cuerpo que las clases concretas debían implementar. Scala ofrece un concepto más poderoso llamado trait, que en su nivel más básico cumple exactamente esa función, pero además permite incluir implementaciones por defecto, campos y lógica compartida. En esta guía nos enfocaremos primero en el uso básico del trait como interfaz pura, y después exploraremos sus capacidades adicionales.

Definiendo un trait como interfaz

En su nivel más básico, los trait de Scala se pueden usar como interfaces anteriores a Java 8, donde define métodos pero no proporciona una implementación para ellos.

Por ejemplo, imagina que quieres escribir código para modelar un auto. Los autos tienen ruedas, por tanto, podría pensar en qué tipo de ruedas tiene el auto y el tipo de tracción de las mismas, por lo que define un trait como este, con dos métodos sin implementación:

trait Ruedas {
  def tipoRuedas: String
  def tipoTraccion: String
}

Esos dos métodos no toman ningún parámetro. Si los métodos que desea definir tomarán parámetros, declárelos como de costumbre:

trait Motor {
  def encender(llave: String): Boolean
  def acelerar(velocidad: Int): Unit
}

Extendiendo traits

Por otro lado de este proceso, cuando desee crear una clase que extienda un trait, use la palabra clave extends:

class Auto extends Ruedas {
  def tipoRuedas: String = "Radiales"
  def tipoTraccion: String = "Delantera"
}

Cuando una clase extiende varios traits, use extends para el primer trait y separe los traits subsiguientes con with:

class Auto extends Ruedas with Motor {
  def tipoRuedas: String = "Radiales"
  def tipoTraccion: String = "Delantera"
  def encender(llave: String): Boolean = true
  def acelerar(velocidad: Int): Unit = println(s"Acelerando a $velocidad km/h")
}

Si una clase extiende un trait pero no implementa todos sus métodos abstractos, la clase debe declararse abstract:

abstract class Vehiculo extends Ruedas {
  def tipoRuedas: String = "Radiales"
  // tipoTraccion queda sin implementar
}

Pero si la clase proporciona una implementación para todos los métodos abstractos de los traits que extiende, se puede declarar como una clase normal:

class Camioneta extends Ruedas {
  def tipoRuedas: String = "Todo terreno"
  def tipoTraccion: String = "4x4"
}

Traits con implementaciones por defecto

Hasta ahora hemos utilizado los traits como interfaces puras, es decir, sin ningún cuerpo en sus métodos. Sin embargo, una de las ventajas más importantes de los trait en Scala es que pueden incluir implementaciones por defecto. Esto los diferencia de las interfaces clásicas de Java (pre-8) y los acerca más a las interfaces con métodos default de Java 8+.

Veamos un ejemplo. Supongamos que queremos definir un trait llamado Saludable que incluya un método con implementación:

trait Saludable {
  def saludar(nombre: String): String = s"Hola, $nombre"
  def despedir(nombre: String): String
}

En este caso, saludar ya tiene una implementación por defecto, mientras que despedir sigue siendo abstracto. Una clase que extienda este trait solo está obligada a implementar despedir:

class Persona extends Saludable {
  def despedir(nombre: String): String = s"Adiós, $nombre"
}

La clase Persona hereda automáticamente la implementación de saludar y puede usarla directamente:

val p = new Persona()
println(p.saludar("Carlos"))   // Hola, Carlos
println(p.despedir("Carlos"))  // Adiós, Carlos

Si lo desea, la clase también puede sobrescribir el método por defecto usando la palabra clave override:

class PersonaFormal extends Saludable {
  override def saludar(nombre: String): String = s"Buenos días, estimado $nombre"
  def despedir(nombre: String): String = s"Fue un placer, $nombre"
}

Esta capacidad convierte a los traits en una herramienta muy flexible: puede definir contratos obligatorios (métodos abstractos) junto con comportamiento reutilizable (métodos con implementación) en una sola unidad.

Mezclando múltiples traits (Stackable Behavior)

Una de las características más poderosas de Scala es la posibilidad de mezclar múltiples traits en una sola clase, lo que permite componer comportamiento de forma modular. A diferencia de Java, donde una clase solo puede extender una única clase padre, en Scala se pueden combinar tantos traits como sea necesario.

Veamos un ejemplo con un sistema de vehículos más completo:

trait Ruedas {
  def tipoRuedas: String
  def tipoTraccion: String
}

trait Motor {
  def encender(llave: String): Boolean
  def acelerar(velocidad: Int): Unit
}

trait AireAcondicionado {
  def temperaturaActual: Int = 22
  def ajustarTemperatura(grados: Int): String = s"Temperatura ajustada a $grados°C"
}

Ahora podemos crear una clase que combine los tres traits:

class AutoCompleto extends Ruedas with Motor with AireAcondicionado {
  def tipoRuedas: String = "Radiales 205/55 R16"
  def tipoTraccion: String = "Delantera"
  def encender(llave: String): Boolean = llave == "correcta"
  def acelerar(velocidad: Int): Unit = println(s"Acelerando a $velocidad km/h")
}

La clase AutoCompleto ahora tiene acceso a todos los métodos de los tres traits, incluyendo los métodos con implementación por defecto de AireAcondicionado:

val auto = new AutoCompleto()
println(auto.tipoRuedas)              // Radiales 205/55 R16
println(auto.encender("correcta"))    // true
println(auto.temperaturaActual)       // 22
println(auto.ajustarTemperatura(18))  // Temperatura ajustada a 18°C

Este patrón de composición es conocido como mixin y es una de las razones por las que los traits en Scala son más versátiles que las interfaces tradicionales.

Diferencias entre trait y abstract class

Una pregunta frecuente al aprender Scala es: ¿cuándo usar un trait y cuándo una abstract class? Ambos pueden contener métodos abstractos y métodos con implementación, pero tienen diferencias importantes:

Característica trait abstract class
Herencia múltiple Sí — una clase puede mezclar varios traits No — solo se puede extender una clase
Parámetros de constructor No (Scala 2)
Estado mutable Puede tener var, pero no es recomendado Puede tener var
Inicialización ordenada No garantizada Garantizada (constructor)
Interoperabilidad con Java Se compilan como interfaces (con limitaciones) Se compilan como clases abstractas

En resumen:

  • Use trait cuando necesite definir un contrato o un comportamiento que pueda mezclarse con otras clases y traits. Es la opción preferida en la mayoría de los casos.
  • Use abstract class cuando necesite parámetros de constructor o cuando la clase base tenga una relación de herencia estricta tipo «es un» (por ejemplo, Animal como clase base de Perro y Gato).

Veamos un ejemplo donde tiene sentido usar una abstract class:

abstract class Vehiculo(val marca: String, val modelo: String) {
  def descripcion: String = s"$marca $modelo"
  def tipo: String  // abstracto
}

class Sedan(marca: String, modelo: String) extends Vehiculo(marca, modelo) {
  def tipo: String = "Sedán"
}

Aquí, Vehiculo necesita recibir marca y modelo como parámetros del constructor, algo que un trait en Scala 2 no puede hacer.

Buenas prácticas al usar traits

Para aprovechar al máximo los traits en Scala, tenga en cuenta las siguientes recomendaciones:

  • Prefiera traits sobre clases abstractas — A menos que necesite parámetros de constructor, los traits ofrecen mayor flexibilidad gracias a la herencia múltiple.
  • Mantenga los traits pequeños y enfocados — Cada trait debe representar un único concepto o comportamiento. Es mejor tener varios traits pequeños que uno grande con muchas responsabilidades.
  • Use nombres descriptivos — Un buen nombre de trait describe el comportamiento que proporciona: Serializable, Comparable, Printable, etc.
  • Evite estado mutable en traits — Aunque es técnicamente posible declarar var en un trait, esto puede causar problemas inesperados cuando se mezclan múltiples traits. Prefiera métodos y valores inmutables (val).
  • Aproveche los métodos por defecto — Proporcionar implementaciones por defecto reduce la cantidad de código que las clases concretas deben escribir, siguiendo el principio DRY (Don’t Repeat Yourself).

Conclusiones

Como se muestra en los ejemplos, en su nivel más básico, los traits se pueden usar como interfaces simples. Luego, las clases extienden los traits usando la palabra clave extends, de acuerdo con las siguientes reglas:

  • Si una clase extiende un trait, use la palabra clave extends.
  • Si una clase extiende varios traits, use extends para el primer trait y separe el resto con with.
  • Si una clase extiende una clase (o una clase abstracta) y un trait, siempre incluya primero el nombre de la clase (usando extends antes del nombre de la clase) y luego use with antes de los nombres de los traits adicionales.

Pero los traits van mucho más allá de las interfaces puras. La posibilidad de incluir implementaciones por defecto, mezclar múltiples traits en una sola clase y componer comportamiento de forma modular los convierte en una de las herramientas más versátiles del lenguaje. Entender cuándo usar un trait frente a una abstract class le permitirá diseñar jerarquías de clases más limpias y mantenibles en sus proyectos de Scala.

José Miguel Moya Curbelo
José Miguel Moya Curbelo
Senior Data Engineer & Big Data Instructor

MSc Applied Mathematics · AWS Cloud Practitioner · SCRUM Master. Especializado en arquitecturas de datos de alto rendimiento con Apache Spark, Snowflake, Python y Scala.

Conectar en LinkedIn

Artículos Relacionados

Deja un comentario

Tu dirección de correo electrónico no será publicada.