2012-06-28 15 views
31

¿Cómo creo un objeto configurable correctamente funcional en Scala? He visto el video de Tony Morris en la mónada Reader y todavía no puedo conectar los puntos.Datos de configuración en Scala - ¿Debería usar la mónada Reader?

Tengo una lista no modificable de Client objetos:

class Client(name : String, age : Int){ /* etc */} 

object Client{ 
    //Horrible! 
    val clients = List(Client("Bob", 20), Client("Cindy", 30)) 
} 

Quiero Client.clients que se determine en tiempo de ejecución, con la libertad de poder leerlo desde un archivo de propiedades o desde una base de datos. En el mundo Java Me defino una interfaz, poner en práctica los dos tipos de fuente, y el uso de DI para asignar una variable de clase:

trait ConfigSource { 
    def clients : List[Client] 
} 

object ConfigFileSource extends ConfigSource { 
    override def clients = buildClientsFromProperties(Properties("clients.properties")) 
    //...etc, read properties files 
} 

object DatabaseSource extends ConfigSource { /* etc */ } 

object Client { 
    @Resource("configuration_source") 
    private var config : ConfigSource = _ //Inject it at runtime 

    val clients = config.clients 
} 

Esto parece una solución bastante limpio para mí (no es una gran cantidad de código, intención clara), pero que var hace saltar (otoh, no me parece muy problemático, ya que sé que va a inyectar una vez y solamente una vez).

¿Cómo sería la mónada Reader en esta situación y, explícame como si fuera 5, cuáles son sus ventajas?

+1

'val's * puede * modificarse mediante reflexión, por lo que es posible que su biblioteca de inyección de dependencia pueda" inyectar un val " – gerferra

+2

@gerferra ¿cuál es el punto de val modificado por reflexión, si tenemos var? –

+0

¿por qué no hacer 'Client' una clase con un argumento, por lo que la configuración se puede pasar a instancias de' Cliente'? –

Respuesta

45

Comencemos con una diferencia simple y superficial entre su enfoque y el enfoque Reader, que es que ya no es necesario esperar en config en ninguna parte. Digamos que definir los siguientes conceptos sinónimo de tipo vagamente inteligente:

type Configured[A] = ConfigSource => A 

Ahora bien, si alguna vez necesito un ConfigSource para alguna función, por ejemplo una función que obtiene el n 'th cliente en la lista, puede declarar que funcionan como "configurado":

def nthClient(n: Int): Configured[Client] = { 
    config => config.clients(n) 
} 

Así que estamos esencialmente tirando de un config de la nada, cada vez que necesita uno! Huele a inyección de dependencia, ¿verdad? Ahora digamos que queremos que las edades de los primeros, segundos y terceros clientes en la lista (suponiendo que existan):

def ages: Configured[(Int, Int, Int)] = 
    for { 
    a0 <- nthClient(0) 
    a1 <- nthClient(1) 
    a2 <- nthClient(2) 
    } yield (a0.age, a1.age, a2.age) 

Para esto, por supuesto, necesita una definición adecuada de map y flatMap. No entraré en eso aquí, sino que simplemente diré que Scalaz (o Rúnar's awesome NEScala talk, o Tony's que ya ha visto) le ofrece todo lo que necesita.

El punto importante aquí es que la dependencia ConfigSource y su llamada inyección están en su mayoría ocultas. La única "pista" que podemos ver aquí es que ages es del tipo Configured[(Int, Int, Int)] en lugar de simplemente (Int, Int, Int). No necesitamos hacer referencia explícita a config en ningún lado.

Como acotación al margen, esta es la forma en que casi siempre gusta pensar en mónadas: que ocultan su efecto así que no es contaminar el flujo de su código, mientras que declarar explícitamente el efecto en la firma de tipo.En otras palabras, no necesita repetirse demasiado: usted dice "hey, esta función se ocupa del efecto X" en el tipo de devolución de la función, y no se meta más con eso.

En este ejemplo, por supuesto, el efecto es leer desde un entorno fijo. Otro efecto monádico con el que puede estar familiarizado es el manejo de errores: podemos decir que Option oculta la lógica de manejo de errores mientras que hace explícita la posibilidad de errores en el tipo de su método. O, más o menos lo contrario de lo que se lee, la mónada Writer oculta lo que estamos escribiendo al hacer explícita su presencia en el sistema de tipos.

Ahora, por fin, del mismo modo que normalmente necesitamos para arrancar un marco DI (en algún lugar fuera de nuestro flujo normal de control, como en un archivo XML), también es necesario para arrancar esta curiosa mónada. Seguramente vamos a tener algún punto de entrada lógico a nuestro código, tales como:

def run: Configured[Unit] = // ... 

Termina siendo bastante simple: desde Configured[A] es sólo un sinónimo de tipo para la función ConfigSource => A, sólo se puede aplicar la función a su "entorno":

run(ConfigFileSource) 
// or 
run(DatabaseSource) 

Ta-da! Por lo tanto, al contrastar con el enfoque DI tradicional al estilo Java, no tenemos ninguna "magia" que ocurra aquí. La única magia, por así decirlo, está encapsulada en la definición de nuestro tipo Configured y la forma en que se comporta como una mónada. Lo que es más importante, el tipo de sistema nos mantiene honestos sobre qué inyección de dependencia de "reino" está ocurriendo en: cualquier cosa con el tipo Configured[...] está en el mundo DI, y cualquier cosa sin él no lo es. Simplemente no obtenemos esto en la DI de la vieja escuela, donde todo es potencialmente administrado por la magia, por lo que no se sabe qué partes de su código son seguras para reutilizar fuera de un marco DI (por ejemplo, dentro las pruebas de su unidad, o en algún otro proyecto por completo).

actualización

: me escribió un blog post que explica Reader con mayor detalle.

+0

Me acabo de ocurrir, también debería decir: ** no se preocupe por hacer un "objeto configurable". ** Un objeto configurable, en realidad, es algo con parámetros de constructor. ¿De dónde vienen esos parámetros? La persona que llama del constructor, por supuesto, que a su vez (si lo he convencido para que lo intente), consígalos del lector (en este caso, el entorno 'Configurado [...]'). Se trata de funciones que llaman a otras funciones, no de las agallas de sus objetos. – mergeconflict

+0

Hmm ...Entonces, en última instancia, todavía tenemos que refactorizar todas nuestras firmas fn para que devuelvan 'Configured [PriorReturnedType]' hasta el fn donde elegimos 'run (ConfigFileSource)' or 'run (DatabaseSource)'? ¿Por qué es superior a pasar el 'ConfigSource' como una arg? Y no entiendo cómo "No tenemos ninguna" magia "que ocurra aquí". Todavía tenemos que elegir 'ejecutar (ConfigFileSource)' o 'run (DatabaseSource)' por una línea de comando arg o una variable de entorno o la "magia" de DI, ¿no? –

+1

re: "Así que en última instancia todavía tenemos que refactorizar todas nuestras firmas fn ..." - Algunas de ellas, no todas. Todo lo que dependa de la configuración global necesitará una firma 'Configurado', pero sus funciones puras no. De nuevo, mantener esos dos mundos claramente identificables es algo bueno. – mergeconflict

Cuestiones relacionadas