为什么要使用Scala的蛋糕模式而不是抽象字段?

25

我一直在阅读使用蛋糕模式在scala中进行依赖注入的内容。我认为我已经理解了它,但可能错过了某些东西,因为我仍然看不出其中的重点!为什么通过自身类型声明依赖关系比仅使用抽象字段更可取呢?

Programming Scala中给出的示例中,TwitterClientComponent使用蛋糕模式声明依赖关系如下:

//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)
      }
    }
  }
}

这种方式比下面将依赖项声明为抽象字段的方式更好在哪里?

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)
    }
  }
}

就实例化时间而言,也就是 DI 实际发生的时候(据我所知),我很难看出 Cake 的优势,尤其是考虑到为了 Cake 声明需要额外键盘输入的内容(包含 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
    }

现在再来一次,使用抽象字段,大致相同!:

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
        }

我相信自己一定错过了关于蛋糕优越性的某些东西!然而,目前我看不出它提供了什么比以任何其他方式(构造函数、抽象字段)声明依赖性更好的东西。


请注意,您所提到的书名应为“Programming Scala”,而非同样存在的“Programming in Scala”。 - agilesteel
我认为 service 是来自于 TwitterServiceComponent,但我不确定我们如何知道它被命名为 service - Kevin Meredith
4个回答

8

使用带有self-type注释的特质比使用旧式的带有字段注入的bean更加可组合,你在第二个片段中可能考虑过这种方式。

让我们看一下如何实例化这个特质:

val productionTwitter = new TwitterClientComponent with TwitterUI with FSTwitterCache with TwitterConnection

如果您需要测试这个特性,您可能会写出以下代码:
val testTwitter = new TwitterClientComponent with TwitterUI with FSTwitterCache with MockConnection

嗯,有点DRY (Don't Repeat Yourself) 违规。让我们改进一下。

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

此外,如果你的组件中有服务之间的依赖关系(例如,UI 依赖于 TwitterService),编译器会自动解析这些依赖关系。


2
那与构造函数注入有何不同?您可以为连接创建一个接口并拥有两个实现。 - Edmondo
我猜想的是,你可以为某些依赖关系命名特定的组合。这样你就可以在某些依赖关系的组合上进行抽象。对吧?但我想知道还有没有其他方面需要考虑? - jhegedus

7

想象一下如果TwitterService使用TwitterLocalCache会发生什么。如果TwitterService自我类型为TwitterLocalCache,那将会更容易,因为TwitterService无法访问您声明的val localCache。蛋糕模式(以及自我类型)允许我们以更普遍和灵活的方式进行注入(当然还有其他方面)。


1
是的,没错。但在你的例子中,通过使用抽象字段只能避免一个比使用cake更多的字段赋值。我想你的意思是写成'TwitterService无法访问val localCache'。如果是这样,再次使用抽象字段,TwitterService也将声明一个'val localCache: TwitterLocalCache',它也需要在实例化时进行赋值。当然,它可以是与TwitterClient实例相同的实例,也可以是另一个实例,具体取决于您的要求。 - Noel Kennedy
啊,是的,抱歉...我得修复一下。但我不认为你真正看到了问题所在。是的,TwitterService可以声明一个localCache值,但它可能没有将其设为抽象的,甚至可能将其设为受保护的(即可能是私有的),因此TwitterClient必须拥有自己的引用,就像TwitterService一样。唉...我们可以继续讨论下去,但归根结底,标准的“Java注入”概念让我感到非常失望,而自我类型化则是统一和简单的。 - Derek Wyatt
我不确定这真的是一个问题,如果你只是将抽象的实现移动到特质中,那么依赖链就没有问题了。 - Chris DaMour

1

我不确定实际的布线如何工作,所以我采用了您建议的抽象属性,并改编了您链接博客文章中的简单示例。

// =======================  
// 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 

在这个简单的例子中,它确实有效(所以你建议的技术确实可用)。

然而,同一篇博客展示了至少另外三种实现相同结果的方法;我认为选择主要取决于可读性和个人偏好。在你建议的技术中,我认为Warmer类表达其依赖注入意图不够清晰。此外,为了连接依赖项,我不得不创建两个更多的traits(PotSensorMixin和HeaterMixin),但也许你有更好的方法来完成它。


没错,这就是一般的想法。你不需要那两个额外的特性。你可以这样写:val warmer = new Warmer { val sensor = new PotSensor; val onOff = new Heater }。这个实例化是“依赖注入”的点。 - Noel Kennedy
@Noel:你对额外特性的看法是正确的。我原以为你想保留混入注入的概念(但在这种情况下,我同意这没有太多意义)。所以基本上这是Java中“构造函数注入与设置器注入”的Scala版本,您有更多选择... - Paolo Falabella

1
在这个例子中,我认为没有太大的区别。自类型在trait声明多个抽象值时可能会带来更多的清晰度,例如:
trait ThreadPool {
  val minThreads: Int
  val maxThreads: Int
}

那么,与依赖于多个抽象值不同,您只需声明对线程池的依赖。

对我来说,自身类型(如Cake模式中所使用的)只是一种同时声明多个抽象成员并为它们提供方便名称的方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接