scala - sirve - que es una clase abstracta en poo
¿Cuáles son las desventajas de declarar las clases de casos de Scala? (4)
Si está escribiendo código que usa muchas estructuras de datos hermosas e inmutables, las clases de casos parecen ser un regalo del cielo, y le ofrece todo lo siguiente de forma gratuita con solo una palabra clave:
- Todo inmutable por defecto
- Getters definidos automáticamente
- Decent toString () implementación
- Cumple con equals () y hashCode ()
- Companion object with unapply () método para hacer coincidir
¿Pero cuáles son las desventajas de definir una estructura de datos inmutables como una clase de caso?
¿Qué restricciones coloca en la clase o sus clientes?
¿Hay situaciones en las que debería preferir una clase que no sea de caso?
¿Hay situaciones en las que debería preferir una clase que no sea de caso?
Martin Odersky nos da un buen punto de partida en su curso Principios de Programación Funcional en Scala (Lección 4.6 - Patrón de coincidencia) que podríamos usar cuando debemos elegir entre clase y clase de caso. El capítulo 7 de Scala por ejemplo contiene el mismo ejemplo.
Digamos, queremos escribir un intérprete para expresiones aritméticas. Para simplificar las cosas inicialmente, nos limitamos a solo números y + operaciones. Dichas expresiones se pueden representar como una jerarquía de clases, con una clase base abstracta Expr como raíz y dos subclases Número y Suma. Entonces, una expresión 1 + (3 + 7) se representaría como
nueva suma (nuevo número (1), nueva suma (nuevo número (3), nuevo número (7)))
abstract class Expr {
def eval: Int
}
class Number(n: Int) extends Expr {
def eval: Int = n
}
class Sum(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval + e2.eval
}
Además, agregar una nueva clase Prod no implica ningún cambio en el código existente:
class Prod(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval * e2.eval
}
Por el contrario, agregar un nuevo método requiere la modificación de todas las clases existentes.
abstract class Expr {
def eval: Int
def print
}
class Number(n: Int) extends Expr {
def eval: Int = n
def print { Console.print(n) }
}
class Sum(e1: Expr, e2: Expr) extends Expr {
def eval: Int = e1.eval + e2.eval
def print {
Console.print("(")
print(e1)
Console.print("+")
print(e2)
Console.print(")")
}
}
El mismo problema resuelto con las clases de casos.
abstract class Expr {
def eval: Int = this match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
}
}
case class Number(n: Int) extends Expr
case class Sum(e1: Expr, e2: Expr) extends Expr
Agregar un nuevo método es un cambio local.
abstract class Expr {
def eval: Int = this match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
}
def print = this match {
case Number(n) => Console.print(n)
case Sum(e1,e2) => {
Console.print("(")
print(e1)
Console.print("+")
print(e2)
Console.print(")")
}
}
}
Agregar una nueva clase Prod requiere potencialmente cambiar todas las coincidencias de patrones.
abstract class Expr {
def eval: Int = this match {
case Number(n) => n
case Sum(e1, e2) => e1.eval + e2.eval
case Prod(e1,e2) => e1.eval * e2.eval
}
def print = this match {
case Number(n) => Console.print(n)
case Sum(e1,e2) => {
Console.print("(")
print(e1)
Console.print("+")
print(e2)
Console.print(")")
}
case Prod(e1,e2) => ...
}
}
Transcripción de la videolectura 4.6 Coincidencia de patrones
Ambos diseños son perfectos y elegirlos a veces es una cuestión de estilo, pero aún así hay algunos criterios que son importantes.
Un criterio podría ser: ¿ está usted creando nuevas subclases de expresión con más frecuencia o más a menudo está creando nuevos métodos? Entonces, es un criterio que analiza la extensibilidad futura y el posible pase de extensión de su sistema.
Si lo que haces es principalmente crear nuevas subclases, entonces en realidad la solución de descomposición orientada a objetos tiene la ventaja. La razón es que es muy fácil y un cambio muy local crear una nueva subclase con un método eval , donde como en la solución funcional, tendrías que retroceder y cambiar el código dentro del método eval y agregar un nuevo caso lo.
Por otro lado, si lo que haces será crear muchos métodos nuevos, pero la jerarquía de clases se mantendrá relativamente estable, entonces la coincidencia de patrones es realmente ventajosa. Porque, de nuevo, cada nuevo método en la solución de coincidencia de patrones es solo un cambio local , ya sea que lo coloque en la clase base, o tal vez incluso fuera de la jerarquía de clases. Mientras que un nuevo método como show en la descomposición orientada a objetos requeriría un nuevo incremento en cada subclase. Entonces, habría más partes, que tienes que tocar.
Por lo tanto, la problemática de esta extensibilidad en dos dimensiones, en la que es posible que desee agregar nuevas clases a una jerarquía, o si desea agregar nuevos métodos, o tal vez ambos, se ha denominado problema de expresión .
Recuerde: debemos usar esto como un punto de partida y no como el único criterio.
Creo que el principio TDD se aplica aquí: no sobre-diseñar. Cuando declaras que algo es una case class
, estás declarando mucha funcionalidad. Eso disminuirá la flexibilidad que tiene para cambiar la clase en el futuro.
Por ejemplo, una case class
tiene un método equals
sobre los parámetros del constructor. Es posible que no le importe eso la primera vez que escriba su clase, pero, luego, puede decidir que desea la igualdad para ignorar algunos de estos parámetros, o hacer algo un poco diferente. Sin embargo, el código del cliente se puede escribir en el tiempo medio que depende de case class
igualdad de la case class
.
Primero los buenos bits:
Todo inmutable por defecto
Sí, e incluso puede ser anulado (usando var
) si lo necesita
Getters definidos automáticamente
Posible en cualquier clase prefijando params con val
Decent toString()
implementación
Sí, muy útil, pero posible a mano en cualquier clase si es necesario
Cumple con equals()
y hashCode()
Combinado con una sencilla coincidencia de patrones, esta es la razón principal por la que las personas usan clases de casos
Companion object with unapply()
método para hacer coincidir
También es posible hacerlo a mano en cualquier clase mediante el uso de extractores
Esta lista también debe incluir el método de copia uber-poderoso, una de las mejores cosas para venir a Scala 2.8
Luego, lo malo, solo hay un puñado de restricciones reales con clases de casos:
No puede definir apply
en el objeto complementario utilizando la misma firma que el método generado por el compilador
En la práctica, sin embargo, esto rara vez es un problema. Se garantiza que el comportamiento cambiante del método de aplicación generado sorprenda a los usuarios y debe desaconsejarse enérgicamente, la única justificación para hacerlo es validar los parámetros de entrada, una tarea que se realiza mejor en el cuerpo constructor principal (que también hace que la validación esté disponible cuando se usa copy
)
No puedes subclase
Es cierto, aunque todavía es posible que una clase de caso sea un descendiente. Un patrón común es crear una jerarquía de clases de rasgos, utilizando clases de casos como los nodos de hoja del árbol.
También vale la pena señalar el modificador sealed
. Cualquier subclase de un rasgo con este modificador debe declararse en el mismo archivo. Cuando se compara el patrón con las instancias del rasgo, el compilador puede advertirle si no ha verificado todas las posibles subclases concretas. Cuando se combina con clases de casos, esto puede ofrecerle un nivel de confianza muy alto en su código si se compila sin previo aviso.
Como una subclase de Producto, las clases de casos no pueden tener más de 22 parámetros
No hay una solución real, excepto para dejar de abusar de las clases con tantos params :)
También...
Otra restricción que se observa a veces es que Scala no admite (actualmente) los params perezosos (como los lazy val
, sino como parámetros). La solución a esto es usar un param nombre y asignarlo a un valor perezoso en el constructor. Desafortunadamente, los parámetros por nombre no se combinan con la coincidencia de patrones, lo que impide que la técnica se use con las clases de casos, ya que rompe el extractor generado por el compilador.
Esto es relevante si desea implementar estructuras de datos perezosos altamente funcionales, y con suerte se resolverá con la incorporación de parámetros perezosos a una versión futura de Scala.
Una gran desventaja: un caso de clases no puede extender una clase de caso. Esa es la restricción.
Otras ventajas que omitió, que se detallan a continuación: serialización / deserialización conforme, no es necesario utilizar la palabra clave "nueva" para crear.
Prefiero clases sin mayúsculas y minúsculas para objetos con estado variable, estado privado o sin estado (por ejemplo, la mayoría de los componentes únicos). Clases de casos para prácticamente todo lo demás.