ACTUALIZACIÓN 2016/02/25:
Si bien la respuesta que escribí a continuación sigue siendo suficiente, también vale la pena hacer referencia a otra respuesta relacionada con esto con respecto al objeto complementario de la clase de caso. Es decir, cómo se reproduce exactamente el objeto acompañante implícito generado por el compilador que ocurre cuando solo se define la clase de caso en sí. Para mí, resultó ser contrario a la intuición.
Resumen:
Puede alterar el valor de un parámetro de clase de caso antes de que se almacene en la clase de caso de manera bastante simple mientras sigue siendo un ADT (tipo de datos abstracto) válido (ated). Si bien la solución fue relativamente simple, descubrir los detalles fue un poco más desafiante.
Detalles:
si desea asegurarse de que solo se puedan instanciar instancias válidas de su clase de caso, lo cual es una suposición esencial detrás de un ADT (tipo de datos abstracto), hay una serie de cosas que debe hacer.
Por ejemplo, un copy
método generado por el compilador se proporciona de forma predeterminada en una clase de caso. Por lo tanto, incluso si tuvo mucho cuidado de asegurarse de que solo se crearan instancias a través del apply
método del objeto complementario explícito que garantizaba que solo podrían contener valores en mayúsculas, el siguiente código produciría una instancia de clase de caso con un valor en minúscula:
val a1 = A("Hi There")
val a2 = a1.copy(s = "gotcha")
Además, las clases de casos se implementan java.io.Serializable
. Esto significa que su cuidadosa estrategia de tener solo instancias en mayúsculas se puede alterar con un simple editor de texto y deserialización.
Por lo tanto, para todas las diversas formas en que se puede usar su clase de caso (con benevolencia y / o maldad), estas son las acciones que debe tomar:
- Para su objeto complementario explícito:
- Créelo usando exactamente el mismo nombre que su clase de caso
- Esto tiene acceso a las partes privadas de la clase de caso.
- Cree un
apply
método con exactamente la misma firma que el constructor principal para su clase de caso
- Esto se compilará con éxito una vez que se complete el paso 2.1
- Proporcionar una implementación obteniendo una instancia de la clase de caso usando el
new
operador y proporcionando una implementación vacía{}
- Esto ahora instanciará la clase de caso estrictamente en sus términos
- Se
{}
debe proporcionar la implementación vacía porque se declara la clase de caso abstract
(consulte el paso 2.1)
- Para su clase de caso:
- Declararlo
abstract
- Evita que el compilador de Scala genere un
apply
método en el objeto complementario que es lo que estaba causando el error de compilación "el método se define dos veces ..." (paso 1.2 anterior)
- Marque el constructor principal como
private[A]
- El constructor principal ahora solo está disponible para la clase de caso en sí y para su objeto complementario (el que definimos anteriormente en el paso 1.1)
- Crea un
readResolve
método
- Proporcione una implementación utilizando el método de aplicación (paso 1.2 anterior)
- Crea un
copy
método
- Defínalo para que tenga exactamente la misma firma que el constructor principal de la clase de caso
- Para cada parámetro, añadir un valor predeterminado utilizando el mismo nombre de parámetro (ex:
s: String = s
)
- Proporcione una implementación utilizando el método de aplicación (paso 1.2 a continuación)
Aquí está su código modificado con las acciones anteriores:
object A {
def apply(s: String, i: Int): A =
new A(s.toUpperCase, i) {}
}
abstract case class A private[A] (s: String, i: Int) {
private def readResolve(): Object =
A.apply(s, i)
def copy(s: String = s, i: Int = i): A =
A.apply(s, i)
}
Y aquí está su código después de implementar el requisito (sugerido en la respuesta de @ollekullberg) y también identificar el lugar ideal para colocar cualquier tipo de almacenamiento en caché:
object A {
def apply(s: String, i: Int): A = {
require(s.forall(_.isUpper), s"Bad String: $s")
new A(s, i) {}
}
}
abstract case class A private[A] (s: String, i: Int) {
private def readResolve(): Object =
A.apply(s, i)
def copy(s: String = s, i: Int = i): A =
A.apply(s, i)
}
Y esta versión es más segura / robusta si este código se usará a través de la interoperabilidad de Java (oculta la clase de caso como una implementación y crea una clase final que evita derivaciones):
object A {
private[A] abstract case class AImpl private[A] (s: String, i: Int)
def apply(s: String, i: Int): A = {
require(s.forall(_.isUpper), s"Bad String: $s")
new A(s, i)
}
}
final class A private[A] (s: String, i: Int) extends A.AImpl(s, i) {
private def readResolve(): Object =
A.apply(s, i)
def copy(s: String = s, i: Int = i): A =
A.apply(s, i)
}
Si bien esto responde directamente a su pregunta, hay incluso más formas de expandir esta vía en torno a las clases de casos más allá del almacenamiento en caché de instancias. Para las necesidades de mi propio proyecto, he creado una solución aún más amplia que he documentado en CodeReview (un sitio hermano de StackOverflow). Si termina revisándolo, usando o aprovechando mi solución, considere dejarme comentarios, sugerencias o preguntas y, dentro de lo razonable, haré todo lo posible para responder dentro de un día.