scala dependency-injection cake-pattern

¿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.