2012-08-22 17 views
7

Quiero crear un sistema de enity con algunas propiedades especiales, basadas en los rasgos de Scala.Conservación de las individualidades de rasgos al mezclarlas en

La idea principal es la siguiente: todos los componentes son rasgos que heredan de la rasgo común:

trait Component 
trait ComponentA extends Component 

veces, en el caso de una jerarquía más compleja y componentes interdependientes que puede conseguir como esto:

trait ComponentN extends ComponentM { 
    self: ComponentX with ComponentY => 

    var a = 1 
    var b = "hello" 
} 

y así sucesivamente. He llegado a la conclusión de que los datos relevantes para cada componente deben estar contenidos en sí mismos y no en algún almacenamiento dentro de un Entity o en otro lugar debido a la velocidad del acceso. Como nota al margen, esa es también la razón por la que todo es mutable, por lo que no hay necesidad de pensar en la inmutabilidad.

Entonces Entities se crean, mezcla de los rasgos:

class Entity 

class EntityANXY extends ComponentA 
    with ComponentN 
    with ComponentX 
    with ComponentY 

Aquí todo está bien, sin embargo tengo un requisito especial que no sé cómo cumplir con el código. El requisito es la siguiente:

cada rasgo debe proporcionar un método de codificación que facilita la recogida de los datos relacionado con el rasgo en una forma universal, por ejemplo, en una forma de un JSON o una Map como Map("a" -> "1", "b" -> "hello") y un método de decodificación (?) para traducir dicho mapa, si se recibe, de nuevo en las variables relacionadas con los rasgos. Además: 1) todos los métodos de codificación y descodificación de todos los rasgos mixtos se llaman en un grupo, en un orden arbitrario por los métodos encode y decode(Map) y 2) deben estar disponibles para ser llamados por separado especificando un rasgo escriba, o mejor, mediante un parámetro de cadena como decode("component-n", Map).

No es posible utilizar métodos con el mismo nombre, ya que se perderán debido a sombreado o anulación. Puedo pensar en una solución, donde todos los métodos se almacenan en un Map[String, Map[String, String] => Unit] para decodificar y Map[String,() => Map[String, String]] para codificar en cada entidad. Esto funcionaría, el nombre y la llamada estarían disponibles. Sin embargo, esto dará como resultado el almacenamiento de la misma información en cada entidad que es inaceptable.

También es posible almacenar estos mapas en un objeto complementario para que no esté duplicado en cualquier lugar y llamar al método encode y decode del objeto con un parámetro adicional que denota una instancia particular de la entidad.

El requisito puede parecer extraño, pero es necesario debido a la velocidad y modularidad requeridas. Todas estas soluciones son torpes y creo que hay una solución mejor e idiomática en Scala, o quizás me falta algún patrón arquitectónico importante aquí. Entonces, ¿hay un enfoque más simple y más idiomático que el que tiene el objeto compañero?

EDIT: Creo que la agregación en lugar de la herencia probablemente podría resolver estos problemas, pero a un costo de no poder llamar a los métodos directamente en una entidad.

ACTUALIZACIÓN: Explorando la forma bastante prometedora propuesta por Rex Kerr, me he topado con algo que obstaculiza.Aquí es el caso de prueba:

trait Component { 
    def encode: Map[String, String] 
    def decode(m: Map[String, String]) 
} 

abstract class Entity extends Component // so as to enforce the two methods 

trait ComponentA extends Component { 
    var a = 10 
    def encode: Map[String, String] = Map("a" -> a.toString) 
    def decode(m: Map[String, String]) { 
    println("ComponentA: decode " + m) 
    m.get("a").collect{case aa => a = aa.toInt} 
    } 
} 

trait ComponentB extends ComponentA { 
    var b = 100 
    override def encode: Map[String, String] = super.encode + ("b" -> b.toString) 
    override def decode (m: Map[String, String]) { 
    println("ComponentB: decoding " + m) 
    super.decode(m) 
    m.get("b").foreach{bb => b = bb.toInt} 
    } 
} 

trait ComponentC extends Component { 
    var c = "hey!" 
    def encode: Map[String, String] = Map("c" -> c) 
    def decode(m: Map[String, String]) { 
    println("ComponentC: decode " + m) 
    m.get("c").collect{case cc => c = cc} 
    } 
} 

trait ComponentD extends ComponentB with ComponentC { 
    var d = 11.6f 
    override def encode: Map[String, String] = super.encode + ("d" -> d.toString) 
    override def decode(m: Map[String, String]) { 
    println("ComponentD: decode " + m) 
    super.decode(m) 
    m.get("d").collect{case dd => d = dd.toFloat} 
    } 
} 

y finalmente

class EntityA extends ComponentA with ComponentB with ComponentC with ComponentD 

modo que

object Main { 
    def main(args: Array[String]) { 
    val ea = new EntityA 
    val map = Map("a" -> "1", "b" -> "3", "c" -> "what?", "d" -> "11.24") 
    println("BEFORE: " + ea.encode) 
    ea.decode(map) 
    println("AFTER: " + ea.encode) 
    } 
} 

lo que da:

BEFORE: Map(c -> hey!, d -> 11.6) 
ComponentD: decode Map(a -> 1, b -> 3, c -> what?, d -> 11.24) 
ComponentC: decode Map(a -> 1, b -> 3, c -> what?, d -> 11.24) 
AFTER: Map(c -> what?, d -> 11.24) 

Los componentes A y B no están influenciados, siendo cortado por la resolución de herencia norte. Entonces este enfoque solo es aplicable en ciertos casos de jerarquía. En este caso, vemos que el ComponentD ha sombreado todo lo demás. Cualquier comentario es bienvenido

ACTUALIZACIÓN 2: coloco el comentario que responde a este problema aquí, para una mejor referencia:.. "Scala linealiza todos los rasgos Debe haber un supertrait de todo lo que terminará la cadena en su caso, que significa que C y A todavía deben llamar al super, y Component debe ser el que termine la cadena con una operación nula ". - Rex Kerr

+0

¿Por qué no utilizar algún tipo de solución genérica para implementar la codificación, en lugar de codificar manualmente todos los métodos de codificar a sí mismo? –

+0

@RobinGreen puede mostrar un ejemplo de una solución de este tipo? Estoy casi seguro de que implica mucha reflexión o mucho más fundición adicional (aunque automática) que la hecha a mano. Hay una gran cantidad de buenas soluciones genéricas, pero todas cuestan velocidad. Además, no estoy seguro de si esto me ayudará con mi objetivo principal en esta tarea: crear una solución delgada para manejar la decodificación de codificación individual. Aunque si conoces algunos ejemplos interesantes, por favor comparte, tal vez los he echado de menos y me servirán? – noncom

+0

¿No se encontrará con un montón de problemas al mezclar dos componentes diferentes que definen una var con el mismo nombre, p.'trait A extends Component {var name =" "}' y 'trait B extends Component {var name =" "}'? No entiendo por qué no usas el patrón de pastel más idiomático. De esta forma, los enfrentamientos de nombres serán más raros y se evitarán más fácilmente, ¿sí? –

Respuesta

5

Travis tenía una respuesta esencialmente correcta; no estoy seguro de por qué lo eliminó. Pero, de todos modos, puedes hacer esto sin demasiado dolor, siempre y cuando estés dispuesto a hacer que tu método de codificación tome un parámetro adicional, y que cuando decodifiques estés contento de simplemente establecer variables mutables, no crear un nuevo objeto. (El apilamiento de rasgos complejo efectivamente en el tiempo de ejecución varía de difícil a imposible.)

La observación básica es que cuando encadena rasgos juntos, define una jerarquía de llamadas de superclase. Si cada una de estas llamadas se encarga de los datos en ese rasgo, estaría configurado, siempre que pueda encontrar una manera de recuperar todos esos datos. Entonces

trait T { 
    def encodeMe(s: Seq[String]): Seq[String] = Seq() 
    def encode = encodeMe(Seq()) 
} 
trait A extends T { 
    override def encodeMe(s: Seq[String]) = super.encodeMe(s) :+ "A" 
} 
trait B extends T { 
    override def encodeMe(s: Seq[String]) = super.encodeMe(s) :+ "B" 
} 

¿Funciona?

scala> val a = new A with B 
a: java.lang.Object with A with B = [email protected] 

scala> a.encode 
res8: Seq[String] = List(A, B) 

scala> val b = new B with A 
b: java.lang.Object with B with A = [email protected] 

scala> b.encode 
res9: Seq[String] = List(B, A) 

Indeed! No solo funciona, sino que obtienes el pedido de forma gratuita.

Ahora necesitamos una forma de establecer variables basadas en esta codificación. Aquí, seguimos el mismo patrón: tomamos algunas sugerencias y simplemente subimos la supercadena con ella. Si tiene muchos rasgos apilados, es posible que desee analizar previamente el texto en un mapa o filtrar las partes aplicables al rasgo actual. Si no, simplemente pasa todo a súper, y luego ponte después.

trait T { 
    var t = 0 
    def decode(m: Map[String,Int]) { m.get("t").foreach{ ti => t = ti } } 
} 
trait C extends T { 
    var c = 1 
    override def decode(m: Map[String,Int]) { 
    super.decode(m); m.get("c").foreach{ ci => c = ci } 
    } 
} 
trait D extends T { 
    var d = 1 
    override def decode(m: Map[String,Int]) { 
    super.decode(m); m.get("d").foreach{ di => d = di } 
    } 
} 

Y esto también funciona igual que uno esperaría:

scala> val c = new C with D 
c: java.lang.Object with C with D = [email protected] 

scala> val d = new D with C 
d: java.lang.Object with D with C = [email protected] 

scala> c.decode(Map("c"->4,"d"->2,"t"->5)) 

scala> "%d %d %d".format(c.t,c.c,c.d) 
res1: String = 5 4 2 
+1

Eliminé la respuesta porque todo el asunto de "anulación abstracta" en realidad no estaba haciendo nada en este caso (lo que obtengo al responder las preguntas primero en el Mañana). La tuya es mejor, de todos modos. –

+0

¡Un enfoque interesante! Nunca pensé de esa manera ... realmente ofrece una manera bastante ordenada y comparativamente barata de hacer esto. Pero para expandir la teoría, ¿cómo piensas qué pasaría en el caso de una jerarquía de paternidad no lineal? Por ejemplo, ¿qué pasa si hay un rasgo que tiene otro rasgo ya que es padre? Este es un caso interesante, para ver dónde no puedo obtener lo que necesito con este modelo de herencia (creo). Permítanos explorar más, por favor vea la actualización de la pregunta original. – noncom

+2

@noncom - Scala _linearizes_ todos los rasgos. Debe haber un súper retrato de todo lo que terminará la cadena. En su caso, eso significa que C y A aún deberían llamar súper, y 'Componente 'debería ser el que termine la cadena con un no-op. –