AGGIORNAMENTO 25/02/2016:
Sebbene la risposta che ho scritto di seguito rimanga sufficiente, vale anche la pena fare riferimento a un'altra risposta correlata a questo riguardo all'oggetto associato della classe case. Vale a dire, come si riproduce esattamente l'oggetto associato implicito generato dal compilatore che si verifica quando si definisce solo la classe case stessa. Per me si è rivelato controintuitivo.
Riepilogo:
è possibile modificare il valore di un parametro della classe case prima che venga memorizzato nella classe case in modo abbastanza semplice pur rimanendo un ADT (Abstract Data Type) valido (ated). Sebbene la soluzione fosse relativamente semplice, scoprire i dettagli è stato un po 'più impegnativo.
Dettagli:
se vuoi assicurarti che solo istanze valide della tua classe case possano essere istanziate, che è un presupposto essenziale dietro un ADT (Abstract Data Type), ci sono una serie di cose che devi fare.
Ad esempio, un copy
metodo generato dal compilatore viene fornito per impostazione predefinita su una classe case. Quindi, anche se fossi molto attento a assicurarti che solo le istanze siano state create tramite il apply
metodo dell'oggetto associato esplicito che garantiva che potevano contenere solo valori maiuscoli, il codice seguente produrrebbe un'istanza di classe case con un valore minuscolo:
val a1 = A("Hi There")
val a2 = a1.copy(s = "gotcha")
Inoltre, implementano le classi case java.io.Serializable
. Ciò significa che la tua attenta strategia di avere solo le maiuscole può essere sovvertita con un semplice editor di testo e deserializzazione.
Quindi, per tutti i vari modi in cui la classe del tuo caso può essere utilizzata (in modo benevolo e / o malevolo), ecco le azioni che devi intraprendere:
- Per il tuo oggetto compagno esplicito:
- Crealo usando esattamente lo stesso nome della classe del tuo caso
- Questo ha accesso alle parti private della classe del caso
- Crea un
apply
metodo con esattamente la stessa firma del costruttore principale per la tua classe case
- Questo verrà compilato correttamente una volta completato il passaggio 2.1
- Fornire un'implementazione ottenendo un'istanza della classe case utilizzando l'
new
operatore e fornendo un'implementazione vuota{}
- Questo ora istanzerà la classe del caso rigorosamente secondo le tue condizioni
- L'implementazione vuota
{}
deve essere fornita perché la classe case è dichiarata abstract
(vedere il passaggio 2.1)
- Per la tua classe di casi:
- Dichiaralo
abstract
- Impedisce al compilatore Scala di generare un
apply
metodo nell'oggetto associato che è la causa dell'errore di compilazione "il metodo è definito due volte ..." (passaggio 1.2 sopra)
- Contrassegna il costruttore principale come
private[A]
- Il costruttore primario è ora disponibile solo per la classe case stessa e per il suo oggetto associato (quello che abbiamo definito sopra nel passaggio 1.1)
- Crea un
readResolve
metodo
- Fornire un'implementazione utilizzando il metodo apply (passaggio 1.2 sopra)
- Crea un
copy
metodo
- Definitelo in modo che abbia esattamente la stessa firma del costruttore principale della classe case
- Per ogni parametro, aggiungere un valore predefinito utilizzando lo stesso nome del parametro (es:
s: String = s
)
- Fornire un'implementazione utilizzando il metodo apply (passaggio 1.2 di seguito)
Ecco il tuo codice modificato con le azioni precedenti:
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)
}
Ed ecco il tuo codice dopo aver implementato il require (suggerito nella risposta @ollekullberg) e aver anche identificato il posto ideale dove mettere qualsiasi tipo di caching:
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)
}
E questa versione è più sicura / robusta se questo codice verrà utilizzato tramite l'interoperabilità Java (nasconde la classe case come implementazione e crea una classe finale che impedisce le derivazioni):
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)
}
Sebbene questo risponda direttamente alla tua domanda, ci sono ancora più modi per espandere questo percorso attorno alle classi di casi oltre alla memorizzazione nella cache delle istanze. Per le mie esigenze di progetto, ho creato una soluzione ancora più ampia che ho documentato su CodeReview (un sito gemello StackOverflow). Se finisci per esaminarlo, utilizzare o sfruttare la mia soluzione, ti preghiamo di lasciarmi feedback, suggerimenti o domande e, entro limiti ragionevoli, farò del mio meglio per rispondere entro un giorno.