2012-09-04 6 views
12

Tal vez un experto en Scala con un buen sentido del estilo y la elegancia me puede ayudar a encontrar una manera más agradable de estructurar el siguiente código, que tiene un problema de "expulsión" del constructor.Diseño modular de Scala: ¿cómo evito un modelo repetitivo "push-out"?

partimos de una clase base simple:

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) 

} 

Para efectos de tipo de comprobación en una aplicación compleja, requiero una subclase sin ningún estado adicional:

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

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

} 

Tal como está Cuando agrego dos barras, solo obtengo 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 siguiente respuesta en el REPL:

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

¿Cómo llego a dos bares se suman para formar otro bar (en lugar de un Foo), de una manera elegante, sin tener que copiar y pegar método add de Foo, como a continuación?

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) 

} 

tengo muchos de estos bares (todos esencialmente copias de Foo, pero muy importante para la comprobación de tipos), una solución de cortar y pegar de libre pagará dividendos.

Gracias!

Respuesta

12

Intento evitar la herencia tanto como sea posible. Entonces aquí hay un enfoque alternativo.

La clase Bar tiene exactamente el mismo constructor que Foo y ambos son apátridas. Si desea tener varios subtipos, solo para transmitir cualquier información adicional, puede usar un parámetro genérico como una "etiqueta". Por ejemplo:

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) 

Ahora add es seguro. Luego puede usar una clase de tipo para obtener toString y mostrar Kind.

+0

Me gusta el "parámetro como etiqueta" idea, para la seguridad de tipos. ¡Gracias! –

5

Ampliando la respuesta de @ paradigmático, si desea ser capaz de soportar las operaciones que son específicos de cada Bar (por ejemplo, diferentes toString), puede ir un paso más allá y hacer Kind una clase de tipos.

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") 

Es igual que un tipo seguro como antes:

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) 

Para cualquier propiedad que desea ser dependiente de la etiqueta, los declaran de manera abstracta en Kind y proporcionan las implementaciones de los objetos implícitos.

+0

Interesante. ¿Cuál es la ventaja de su enfoque "implícito" al declarar un método 'def name = "Foo"' dentro de Foo y hacer que Bar y Biz lo anulen? –

+1

No es realmente una ventaja, solo estaba tratando de demostrar que puede lograr exactamente eso incluso en un entorno sin herencia, similar al propuesto en la respuesta aceptada. – Philippe

2

El enfoque con la parametrización de tipo tiene una limitación en el sentido de que no le permite extender la funcionalidad de una manera natural que es herencia. Por lo tanto, un enfoque alternativo podría ser un método de reemplazo, capaz de fábrica (que no es más que un delegado para el constructor):

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")) 
Cuestiones relacionadas