2012-09-04 9 views
12

Forse un esperto di Scala con un buon senso dello stile e dell'eleganza può aiutarmi a capire un modo migliore per strutturare il seguente codice, che ha un problema di "push-out" del costruttore.Design modulare di Scala: come evitare un boiler "push-out" del costruttore?

Iniziamo con una semplice classe di base:

class Foo(val i: Int, val d: Double, val s: String) { 

    def add(f: Foo) = new Foo(i + f.i, d + f.d, s + f.s) 
    override def toString = "Foo(%d,%f,%s)".format(i,d,s) 

} 

Per scopi di tipo-controllo in un'applicazione complessa, ho bisogno di una sotto-classe senza alcuna condizione aggiuntiva:

class Bar(i: Int, d: Double, s: String) extends Foo(i,d,s) { 

    override def toString = "Bar(%d,%f,%s)".format(i,d,s) 

} 

Così com'è , quando aggiungo due barre, ottengo solo un Foo:

val x = new Bar(1,2.3,"x") 
val y = new Bar(4,5.6,"y") 
val xy = x.add(y) 

con la seguente risposta nel REPL:

x : Bar = Bar(1,2.300000,x) 
y : Bar = Bar(4,5.600000,y) 
xy : Foo = Foo(5,7.900000,xy) 

Come posso ottenere due barre di aggiungere insieme per formare un altro bar (piuttosto che un Foo), in modo elegante, senza dover copiare e incollare metodo add di Foo, come di seguito?

class Bar(i: Int, d: Double, s: String) extends Foo(i,d,s) { 

    // ugly copy-and-paste from Foo: 
    def add(b: Bar) = new Bar(i + b.i, d + b.d, s + b.s) 
    override def toString = "Bar(%d,%f,%s)".format(i,d,s) 

} 

Ho molti di questi bar (tutti essenzialmente copie di Foo, ma molto importante per il controllo di tipo), una soluzione cut-and-paste-libera pagherà dividendi.

Grazie!

risposta

12

Cerco di evitare l'eredità il più possibile. Quindi ecco un approccio alternativo.

Classe Bar ha esattamente lo stesso costruttore di Foo ed entrambi sono senza stato. Se vuoi avere più sottotipi, solo per trasmettere ulteriori informazioni, puoi usare un parametro generico come "etichetta". Ad esempio:

trait Kind 
trait Bar extends Kind 

class Foo[T<:Kind](val i: Int, val d: Double, val s: String) { 
    def add(f: Foo[T]) = new Foo[T](i + f.i, d + f.d, s + f.s) 
    override def toString = "Foo(%d,%f,%s)".format(i,d,s) 
} 


scala> val b1 = new Foo[Bar](2,3.0,"hello") 
b1: Foo[Bar] = Foo(2,3.000000,hello) 

scala> val b2 = new Foo[Bar](3,1.0," world") 
b2: Foo[Bar] = Foo(3,1.000000, world) 

scala> b1 add b2 
res1: Foo[Bar] = Foo(5,4.000000,hello world) 

Ora il tipo add è sicuro. È quindi possibile utilizzare una classe di tipo per ottenere toString per visualizzare Kind.

+0

Mi piace il "parametro come etichetta" idea, per la sicurezza di tipo. Grazie! –

5

Ampliando @ risposta di paradigmatico, se si vuole essere in grado di supportare le operazioni che sono specifici per ogni Bar (ad esempio diversa toString), si può fare un passo avanti e fare un Kind typeclass.

trait Kind[T] { def name : String } 
trait Bar 
implicit object BarHasKind extends Kind[Bar] { val name = "Bar" } 

class Foo[T : Kind](val i : Int, val d : Double, val s : String) { 
    def add(f : Foo[T]) = new Foo[T](i + f.i, d + f.d, s + f.s) 
    override def toString = implicitly[Kind[T]].name + "(%d,%f,%s)".format(i,d,s) 
} 

scala> val b1 = new Foo[Bar](2, 3.0, "hello") 
b1: Foo[Bar] = Bar(2,3.000000,hello) 

trait Biz 
implicit object BizHasKind extends Kind[Biz] { val name = "Biz" } 

scala> val b2 = new Foo[Biz](1, 1.0, "One") 

E 'altrettanto tipo sicuro come prima:

scala> b1 add b2 
<console>:16: error: type mismatch; 
    found : Foo[Biz] 
    required: Foo[Bar] 

scala> b2 add b2 
resN: Foo[Biz] = Biz(2,2.000000,OneOne) 

Per ogni proprietà che si desidera essere dipendente dal tag, dichiararle astrattamente in Kind e forniscono le implementazioni negli oggetti impliciti.

+0

Interessante. Qual è il vantaggio del tuo approccio "implicito" rispetto alla dichiarazione di un metodo "def name =" Foo "" all'interno di Foo, e se Bar e Biz lo sovrascrivono? –

+1

Non è davvero un vantaggio, stavo solo cercando di dimostrare che è possibile ottenere esattamente questo anche in un'ambientazione senza eredità, simile a quella proposta nella risposta accettata. – Philippe

2

L'approccio con parametrizzazione del tipo ha un limite in quanto non consente di estendere la funzionalità in modo naturale che è l'ereditarietà. Quindi, un approccio alternativo potrebbe essere un metodo di sostituzione-in grado di fabbrica (che è semplicemente un delegato al costruttore):

class Foo(val i: Int, val d: Double, val s: String) { 

    protected def create(i: Int, d: Double, s: String) = new Foo(i, d, s) 

    def add[A <: Foo](f: A) = create(i + f.i, d + f.d, s + f.s) 

    override def toString = "Foo(%d,%f,%s)".format(i,d,s) 
} 

class Bar(i: Int, d: Double, s: String) extends Foo(i,d,s) { 

    protected override def create(i: Int, d: Double, s: String) = new Bar(i, d, s) 

    override def toString = "Bar(%d,%f,%s)".format(i,d,s) 

    // additional methods... 

} 

println(new Foo(10, 10.0, "10") add new Bar(10, 10.0, "10")) 
println(new Bar(10, 10.0, "10") add new Foo(10, 10.0, "10")) 
Problemi correlati