Crear un JAR con dependencias con Apache Maven
¿Qué es un JAR con dependencias? Un requisito típico de los proyectos es agregar la salida junto con sus dependencias, módulos y otros archivos en…
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.
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
}
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"
}
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.
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.
trait y abstract classUna 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) | Sí |
| 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:
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.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.
Para aprovechar al máximo los traits en Scala, tenga en cuenta las siguientes recomendaciones:
Serializable, Comparable, Printable, etc.var en un trait, esto puede causar problemas inesperados cuando se mezclan múltiples traits. Prefiera métodos y valores inmutables (val).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:
extends.extends para el primer trait y separe el resto con with.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.