En los artículos anteriores de esta serie, veíamos una pequeña introducción a Scala, y hacíamos una kata para comprobar que habíamos entendido la sintaxis. En este artículo veremos dos construcciones del lenguaje que resultan bastante interesantes, llamadas traits y case classes.
Traits
Podemos entender los traits como una mezcla entre interfaces y clases abstractas, ya que podemos definir un contrato como haríamos con una interfaz, y por otra parte podemos definir también una implementación.
A diferencia de Java, Scala nos permite implementar varias traits, y podemos utilizarlas para agregar características adicionales a nuestro código. Veamos un ejemplo.
En este caso vamos a definir una clase base llamada Employee, que tiene un salario, y luego vamos a definir varios traits, algunos solamente con valores, y otros con pequeñas comprobaciones.
- El trait Temporary calcula el mes final del contrato
- El trait Authority nos permite saber si un empleado tiene gente a su cargo
- El trait Remote nos permite establecer una localización y una zona horaria
- Finalmente, el trait InOffice nos permite establecer un despacho.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
abstract class Employee { | |
def salary: Int | |
} | |
trait InOffice { | |
def desk: String | |
} | |
trait Remote { | |
def location: String | |
def timeZone: String | |
} | |
trait Temporary { | |
def startDate: Int | |
def stay: Int | |
def endDate(): Int = (startDate + stay) % 12 | |
} | |
trait Authority { | |
def directReports: Array[Employee] | |
def hasDirectReports() :Boolean = directReports.length != 0 | |
} |
Con estos cuatro traits podemos componer las clases que forman a nuestros empleados, donde tenemos a Developer, a Manager y a Intern:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class Intern (salary_param: Int, desk_param: String, startDate_param: Int) extends Employee with InOffice with Temporary { | |
val salary = salary_param | |
val desk = desk_param | |
val stay = 6 | |
val startDate = startDate_param | |
} | |
class Developer (salary_param: Int, desk_param: String) extends Employee with InOffice { | |
val salary = salary_param | |
val desk = desk_param | |
} | |
class Manager (salary_param: Int, location_param: String, reports: Array[Employee]) extends Employee with Remote with Authority { | |
val salary = salary_param | |
val location = location_param | |
val directReports = reports | |
} |
Como vemos en el ejemplo, podemos utilizar los traits para definir características adicionales a nuestras clases, y podemos también agregar lógica dentro de las mismas, mezclando los conceptos de interfaz y clase abstracta.
Case classes
La otra cara de la moneda son las Case Classes, que podemos encontrar cuando definimos jerarquías, pero sobre todo las podemos encontrar cuando tenemos un número específico de entidades y lo que realmente cambia es lo que hacemos con ellas. Recuperando el mismo caso de empleados, becarios y managers, vamos a redefinirlo como Case Classes:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
abstract class Employee | |
case class Manager(startYear: Int, direct: Array[Employee]) extends Employee | |
case class Dev(startYear: Int) extends Employee | |
case class Intern(stay: Int) extends Employee |
En este caso hemos simplificado la manera de definir los diferentes datos, y solamente utilizaremos la fecha inicial para Dev y Manager, y el tiempo total de estancia para Intern. En este caso, podemos asumir que nuestro conjunto de datos está fijo.
Sin embargo, ¿qué pasaría si necesitáramos agregar información de nóminas a nuestra aplicación? Si recurriéramos a la herencia, deberíamos implementar un método “salary” o similar, para cada una de las clases, sin embargo, como estamos usando “case classes” podemos crear un único método que las compruebe todas, y se comporte de manera consistente:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
object Payroll { | |
def printSalary(employee: Employee) { | |
val currentYeay = 2001 | |
employee match { | |
case Intern(stay) => | |
println(stay * 300) | |
case Dev(startYear) => | |
println(1000 + (currentYeay – startYear) * 500) | |
case Manager(startYear, direct) => | |
println(1000 + (currentYeay – startYear) * 500 + direct.length * 20) | |
} | |
} | |
} |
En este caso, el método nos devolverá distintos resultados en función de la clase que estemos aplicando y los datos que contenga la misma, permitiendo una separación entre las clases que contienen los datos de los métodos para manipularlos.
Este enfoque tiene sus inconvenientes, y es que si tenemos que agregar un nuevo tipo de dato (por ejemplo, External) tendremos que cambiar todos los métodos auxiliares para soportar el nuevo caso, así que se reduce a elegir en función de las necesidades de nuestro proyecto.
Conclusiones
Construcciones como Case Classes o Traits nos permiten establecer restricciones de una manera sencilla, las traits nos permiten mezclar conceptos de interfaces y clases, y las case classes nos permiten separar los datos de nuestra clase de las transformaciones que hacemos con los mismos.
En C# tenemos a nuestra disposición los métodos de extensión, con los que podemos lograr un comportamiento similar que con las case classes, y que hemos visto con anterioridad en este artículo «Clases abstractas VS interfaces + métodos de extensión«.
En el próximo artículo de la serie veremos más construcciones y, como siempre, más ejemplos.
Para más información:
- Tutorial de traits de la documentación de scala: http://docs.scala-lang.org/tutorials/tour/traits.html
- Otro excelente tutorial de traits: http://joelabrahamsson.com/learning-scala-part-seven-traits/
- Guía de Scala para programadores Java: http://docs.scala-lang.org/tutorials/scala-for-java-programmers.html#traits
- Tutorial de Case Classes: http://docs.scala-lang.org/es/tutorials/tour/case-classes.html
- Libro: Scala by Example: http://www.scala-lang.org/docu/files/ScalaByExample.pdf