Clases, Case Classes y sus secretos!
Scala es un lenguaje de programación que combina programación orientada a objetos y funcional. En este artículo veremos class y case class, cómo usarlas, algunos secretos de las case class y por qué son las favoritas para el uso diario.
¿Qué son las Clases?
Como otros lenguajes de programación, una clase es una plantilla que define un objeto; puede contener valores, variables, tipos y métodos que operan principalmente sobre ellos. En Scala, una clase se define con la palabra clave class y un identificador o nombre para describirla. Veamos un ejemplo:
class MyFirstClass
val x = new MyFirstClass
La palabra clave del lenguaje new se usa para crear una instancia de la clase. MyFirstClass tiene un constructor predeterminado que no toma argumentos. Pero a menudo queremos un constructor y un cuerpo en cada clase para dar propiedades y comportamiento. Veamos otro ejemplo con constructor de parámetros:
class Vehicle(
var passengers: Int,
var speed: Int,
val unit: String
) {
override def toString: String = s"(passengers: $passengers, speed: $speed $unit)"
}
val bicycle = new Vehicle(1, 30, "km")
bicycle.passengers // 1
bicycle.passengers = 2
bicycle.passengers // 2
println(bicycle) // (passengers: 2, speed: 30 km)
Como podemos ver, esta clase Vehicle tiene 4 miembros: las variables passengers, speed y unit que se pueden editar, el método toString, a diferencia de otros lenguajes, el constructor principal está en la firma de la clase y no después. Los constructores como métodos también pueden tomar un valor predeterminado, definir variables mutables o inmutables y privadas o públicas como veremos a continuación:
class Vehicle1(
var passengers: Int,
private val speed: Int,
private val unit: String = "km"
) {
val speedDescription: String = s"$speed $unit"
override def toString: String = s"(passengers: $passengers, speed: $speedDescription)"
}
object Vehicle1 {
def speed220km(passengers: Int): Vehicle1 = new Vehicle1(passengers, 220, "km")
}
val motorcycle = new Vehicle1(2, 100)
motorcycle.passengers // 2
motorcycle.speed // no compila porque el atributo es privado
motorcycle.speedDescription // 100 km
println(motorcycle) // (passengers: 2, speed: 100 km)
val car: Vehicle1 = Vehicle1.speed220km(5)
println(car) // (passengers: 5, speed: 220 km)
En este último fragmento, podemos ver un objeto companion donde podemos definir otros constructores predeterminados, como el de crear un vehículo con 220 km de velocidad, también se puede crear un constructor interno que también se puede sobrescribir, pero no mostramos ningún ejemplo porque la verdad no se usa tanto como en otros lenguajes, de hecho, muchas publicaciones aconsejan escribir el código más funcional posible, y para esto, recomiendan crear sus atributos de clase como val o también usan case class que explicaremos a continuación.
¿Qué son las case classes?
Una case class es una clase con todas sus características y más, cuando el compilador de Scala ve la palabra reservada “case” delante de cada class genera múltiples beneficios tales como:
- Los parámetros del constructor son val y public por defecto, también son inmutables, por lo que se generan métodos de acceso para cada uno de ellos.
- Se genera automáticamente un método apply en el objeto companion que permite instanciar sin usar la palabra clave new.
- Se genera un método unapply que permite usar case classes de más formas en expresiones de matching/pattern matching.
- Se genera un método copy beneficioso y se usa todo el tiempo en programación funcional.
- Además de equals, hashCode y toString, lo que permite una mejor coincidencia, uso de claves de mapas, tipado conciso, etc.
Intentaremos mostrar todas estas características a continuación (comenzando y transformando el mismo ejemplo del vehículo):
case class Vehicle2(passengers: Int, speed: Int, unit: String){
val speedDescription: String = s"$speed $unit"
}
Ya podemos ver una clase mucho más limpia y concisa, vemos cómo instanciar objetos de múltiples formas:
// Constructor normal y el más usado
val myCar = Vehicle2(5, 200, "km")
// Usando apply explícitamente
val myCar1 = Vehicle2.apply(5, 200, "km")
// Por "tupla" de valores
val myCar2 = Vehicle2.tupled((5, 200, "km"))
// A través de parámetros en modo currying
val myCar3 = Vehicle2.curried(5)(200)("km")
Podemos usar los métodos generados automáticamente:
myCar.passengers // 5
myCar.speed // 200
myCar.speed = 300 // no compila -> error: reassignment to val
myCar.unit // km
println(myCar) // Vehicle2(5,200,km)
val myFastCar = myCar.copy(passengers = 2, speed = 320)
println(myFastCar) // Vehicle2(2,320,km)
Comparamos por estructura y no por referencia:
myCar == myCar1 // true
myCar == myCar2 // true
myCar == myCar3 // true
myCar == myFastCar // false
Podemos usar el unapply en expresiones match (Modo Simple):
def recognizeVehicle(x: Vehicle2): String = x match {
case Vehicle2(10, speed, unit) =>
s"Minivan de 10 pasajeros con velocidad de $speed $unit"
case Vehicle2(2, speed, _) if speed > 300 =>
s"Auto deportivo de alta velocidad ${x.speedDescription}"
case _ =>
"Cualquier vehículo que no sea minivan o deportivo: " + x
}
val minivan = Vehicle2(10, 100, "km")
println(recognizeVehicle(minivan))
// Minivan de 10 pasajeros con velocidad de 100 km
println(recognizeVehicle(myFastCar))
// Auto deportivo de alta velocidad 320 km
println(recognizeVehicle(myCar))
// Cualquier vehículo que no sea minivan o deportivo: Vehicle2(5,200,km)
Veamos ahora un ejemplo un poco más completo usando unapply y pattern matching:
sealed trait Animal {
def name: String
}
case class Dog(name: String, owner: String) extends Animal
case class Cat(name: String, color: String) extends Animal
def recognizeAnimal(a: Animal): String = a match {
case Dog(name, owner) => s"El perro $name pertenece a $owner."
case Cat(name, color) => s"${name.capitalize} es un gato $color muy hermoso."
}
val bony = Dog("Bony", "Pedrito")
val tom = Cat("tom", "negro")
println(recognizeAnimal(bony))
// El perro Bony pertenece a Pedrito.
println(recognizeAnimal(tom))
// Tom es un gato negro muy hermoso.
Esto funciona gracias al estándar de Scala de que un método unapply devuelve los atributos del constructor de cada case class en una tupla que está envuelta en un Option (Veremos en artículos posteriores qué significa). Esta característica es considerada según el propio Martin Odersky en su libro Programming in Scala como la de mayor ventaja de la case class, ya que Pattern matching es una característica fundamental en todos los lenguajes de programación funcional.
Referencias y otros enlaces de interés
- Scala Lang(class): Classes
- Alvin Alexander / Scala: Case Classes
- Scala Exercises: Classes vs Case Classes
- Code Snippet: Scala Case Classes