¿Por qué usar el patrón de tortas de scala en lugar de campos abstractos?
dependency-injection cake-pattern (4)
He estado leyendo acerca de cómo hacer Dependency Injection en Scala a través del patrón de tortas . ¡Creo que lo entiendo, pero debo haberme perdido algo porque aún no puedo ver el punto! ¿Por qué es preferible declarar dependencias a través de los propios tipos en lugar de solo campos abstractos?
Dado el ejemplo en Programming Scala, TwitterClientComponent
declara dependencias como esta usando el patrón cake:
//other trait declarations elided for clarity
...
trait TwitterClientComponent {
self: TwitterClientUIComponent with
TwitterLocalCacheComponent with
TwitterServiceComponent =>
val client: TwitterClient
class TwitterClient(val user: TwitterUserProfile) extends Tweeter {
def tweet(msg: String) = {
val twt = new Tweet(user, msg, new Date)
if (service.sendTweet(twt)) {
localCache.saveTweet(twt)
ui.showTweet(twt)
}
}
}
}
¿Cómo es mejor que declarar las dependencias como campos abstractos como a continuación?
trait TwitterClient(val user: TwitterUserProfile) extends Tweeter {
//abstract fields instead of cake pattern self types
val service: TwitterService
val localCache: TwitterLocalCache
val ui: TwitterClientUI
def tweet(msg: String) = {
val twt = new Tweet(user, msg, new Date)
if (service.sendTweet(twt)) {
localCache.saveTweet(twt)
ui.showTweet(twt)
}
}
}
En cuanto al tiempo de creación de instancias, que es cuando realmente ocurre la DI (tal como lo entiendo), estoy luchando para ver las ventajas del pastel, especialmente cuando se considera la necesidad de escribir el teclado adicional para las declaraciones del pastel (rasgo adjunto)
//Please note, I have stripped out some implementation details from the
//referenced example to clarify the injection of implemented dependencies
//Cake dependencies injected:
trait TextClient
extends TwitterClientComponent
with TwitterClientUIComponent
with TwitterLocalCacheComponent
with TwitterServiceComponent {
// Dependency from TwitterClientComponent:
val client = new TwitterClient
// Dependency from TwitterClientUIComponent:
val ui = new TwitterClientUI
// Dependency from TwitterLocalCacheComponent:
val localCache = new TwitterLocalCache
// Dependency from TwitterServiceComponent
val service = new TwitterService
}
¡Ahora de nuevo con campos abstractos, más o menos iguales !:
trait TextClient {
//first of all no need to mixin the components
// Dependency on TwitterClient:
val client = new TwitterClient
// Dependency on TwitterClientUI:
val ui = new TwitterClientUI
// Dependency on TwitterLocalCache:
val localCache = new TwitterLocalCache
// Dependency on TwitterService
val service = new TwitterService
}
¡Estoy seguro de que me falta algo sobre la superioridad de la torta! Sin embargo, por el momento no puedo ver lo que ofrece sobre la declaración de dependencias de ninguna otra manera (constructor, campos abstractos).
Los rasgos con anotación de tipo propio son mucho más composable que los beans de viejo estilo con inyección de campo, que probablemente tengas en mente en tu segundo fragmento.
Veamos cómo instalarás este rasgo:
val productionTwitter = new TwitterClientComponent with TwitterUI with FSTwitterCache with TwitterConnection
Si necesita probar este rasgo, probablemente escriba:
val testTwitter = new TwitterClientComponent with TwitterUI with FSTwitterCache with MockConnection
Hmm, una pequeña violación DRY. Mejoremos.
trait TwitterSetup extends TwitterClientComponent with TwitterUI with FSTwitterCache
val productionTwitter = new TwitterSetup with TwitterConnection
val testTwitter = new TwitterSetup with MockConnection
Además, si tiene una dependencia entre servicios en su componente (digamos que UI depende de TwitterService) el compilador los resolverá automáticamente.
No estaba seguro de cómo funcionaría el cableado real, así que he adaptado el ejemplo simple en la entrada del blog al que vinculó para usar propiedades abstractas como sugirió.
// =======================
// service interfaces
trait OnOffDevice {
def on: Unit
def off: Unit
}
trait SensorDevice {
def isCoffeePresent: Boolean
}
// =======================
// service implementations
class Heater extends OnOffDevice {
def on = println("heater.on")
def off = println("heater.off")
}
class PotSensor extends SensorDevice {
def isCoffeePresent = true
}
// =======================
// service declaring two dependencies that it wants injected
// via abstract fields
abstract class Warmer() {
val sensor: SensorDevice
val onOff: OnOffDevice
def trigger = {
if (sensor.isCoffeePresent) onOff.on
else onOff.off
}
}
trait PotSensorMixin {
val sensor = new PotSensor
}
trait HeaterMixin {
val onOff = new Heater
}
val warmer = new Warmer with PotSensorMixin with HeaterMixin
warmer.trigger
en este caso simple funciona (por lo que la técnica que sugiere es útil).
Sin embargo, el mismo blog muestra al menos otros tres métodos para lograr el mismo resultado; Creo que la elección se basa principalmente en la legibilidad y las preferencias personales. En el caso de la técnica que sugiere en mi humilde opinión, la clase Warmer comunica mal su intención de tener dependencias inyectadas. También para conectar las dependencias, tuve que crear dos rasgos más (PotSensorMixin y HeaterMixin), pero tal vez tenías una mejor manera de hacerlo.
Piensa en lo que sucede si TwitterService
usa TwitterLocalCache
. Sería mucho más fácil si TwitterService
se TwitterLocalCache
a TwitterLocalCache
porque TwitterService
no tiene acceso a la val localCache
que ha declarado. El patrón Cake (y auto-tipado) nos permite inyectar de una manera mucho más universal y flexible (entre otras cosas, por supuesto).
En este ejemplo, creo que no hay gran diferencia. Los self-types pueden aportar más claridad en los casos en que un rasgo declara varios valores abstractos, como
trait ThreadPool {
val minThreads: Int
val maxThreads: Int
}
Entonces, en lugar de depender de varios valores abstractos, declara la dependencia de un ThreadPool. Los auto-tipos (como se usan en el patrón Cake) para mí son solo una forma de declarar varios miembros abstractos a la vez, dándoles un nombre conveniente.