19 votos

¿Por qué utilizar el patrón cake de scala en lugar de campos abstractos?

He estado leyendo sobre cómo hacer Inyección de Dependencia en scala a través del patrón de tarta . Creo que lo entiendo, pero debo haberme perdido algo, porque sigo sin verle el sentido. ¿Por qué es preferible declarar las dependencias a través de tipos propios en lugar de campos abstractos?

Teniendo en cuenta el ejemplo de Programación de Scala TwitterClientComponent declara dependencias como esta utilizando el patrón de la tarta:

//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 esto mejor que declarar las dependencias como campos abstractos, como se indica 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 el momento de la instanciación, que es cuando el DI ocurre realmente (según entiendo), me cuesta ver las ventajas de cake, especialmente si se tiene en cuenta el tecleo extra que hay que hacer para las declaraciones de cake (enclosing trait)

    //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 lo mismo!

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 se me debe escapar algo sobre la superioridad de la tarta. Sin embargo, por el momento no puedo ver lo que ofrece sobre la declaración de dependencias de cualquier otra manera (constructor, campos abstractos).

5voto

Derek Wyatt Puntos 2070

Piensa en lo que ocurre si TwitterService utiliza TwitterLocalCache . Sería mucho más fácil si TwitterService se ha auto-tipificado a TwitterLocalCache porque TwitterService no tiene acceso al val localCache que has declarado. El patrón Cake (y la autotipificación) nos permite inyectar de una manera mucho más universal y flexible (entre otras cosas, claro).

4voto

CheatEx Puntos 988

Los traits con anotaciones de tipo propio son mucho más componibles que las antiguas judías con inyección de campos, que probablemente tenías en mente en tu segundo fragmento.

Veamos cómo se instala este rasgo:

val productionTwitter = new TwitterClientComponent with TwitterUI with FSTwitterCache with TwitterConnection

Si necesitas probar este rasgo, probablemente escribas:

val testTwitter = new TwitterClientComponent with TwitterUI with FSTwitterCache with MockConnection

Hmm, una pequeña violación de DRY. Vamos a mejorar.

trait TwitterSetup extends TwitterClientComponent with TwitterUI with FSTwitterCache
val productionTwitter = new TwitterSetup with TwitterConnection
val testTwitter = new TwitterSetup with MockConnection

Además, si tienes una dependencia entre servicios en tu componente (digamos que UI depende de TwitterService) serán resueltos automáticamente por el compilador.

0voto

Paolo Falabella Puntos 10514

No estaba seguro de cómo funcionaría el cableado real, así que he adaptado el sencillo ejemplo de la entrada del blog que enlazaste utilizando propiedades abstractas como sugeriste.

// =======================  
// 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 sencillo sí funciona (por lo que la técnica que sugieres sí es utilizable).

Sin embargo, el mismo blog muestra al menos otros tres métodos para lograr el mismo resultado; creo que la elección es sobre todo acerca de la legibilidad y la preferencia personal. En el caso de la técnica que sugieres IMHO la clase Warmer comunica mal su intención de tener dependencias inyectadas. Además, para cablear las dependencias, tuve que crear dos traits más (PotSensorMixin y HeaterMixin), pero tal vez tú tenías en mente una forma mejor de hacerlo.

0voto

Przemek Pokrywka Puntos 925

En este ejemplo creo que no hay gran diferencia. Los autotipos pueden aportar potencialmente 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, simplemente declaras la dependencia de un ThreadPool. Los autotipos (tal y como se utilizan en el patrón Cake) para mí son sólo una forma de declarar varios miembros abstractos a la vez, dándoles un nombre conveniente.

Iteramos.com

Iteramos es una comunidad de desarrolladores que busca expandir el conocimiento de la programación mas allá del inglés.
Tenemos una gran cantidad de contenido, y también puedes hacer tus propias preguntas o resolver las de los demás.

Powered by:

X