使用Scala特性作为模块或“ Thin Cake”模式

我想描述一种在模块化Scala项目中成功使用的纯Scala模块化方法。

但是,让我们从如何进行依赖注入开始(另请参见我的其他 博客 )。 每个类可以具有构造函数参数形式的依赖项,例如:

class WheatField
class Mill(wheatField: wheatField)
class CowPasture
class DiaryFarm(cowPasture: CowPasture)
class Bakery(mill: Mill, dairyFarm: DairyFarm)

在“世界尽头”中,有一个主类运行应用程序,并在其中创建整个对象图:

object BakeMeCake extends App {
     // creating the object graph
     lazy val wheatField = new WheatField()
     lazy val mill = new Mill(wheatField)     
     lazy val cowPasture = new CowPasture()
     lazy val diaryFarm = new DiaryFarm(cowPasture)
     lazy val bakery = new Bakery(mill, dairyFarm)

     // using the object graph
     val cake = bakery.bakeCake()
     me.eat(cake)
}

接线可以手动完成,或使用MacWire进行

请注意,我们可以使用Scala构造进行范围界定: lazy val对应于单例对象(在构造的对象图中), def于依赖范围的对象(将为每种用法创建一个新实例)。

薄蛋糕图案

如果对象图(同时又是主类)变大怎么办? 答案很简单:我们必须将其分解为“模块”。 每个模块都是Scala trait ,并且包含对象图的某些部分。

例如:

trait CropModule {
     lazy val wheatField = new WheatField()
     lazy val mill = new Mill(wheatField)     
} 

trait LivestockModule {
     lazy val cowPasture = new CowPasture()
     lazy val diaryFarm = new DiaryFarm(cowPasture)
}

然后,主要对象成为特质的组合。 蛋糕模式也正是这种情况。 但是,这里我们仅使用其中一个元素,因此使用“ Think Cake”模式名称。

object BakeMeCake extends CropModule with LivestockModule {
     lazy val bakery = new Bakery(mill, dairyFarm)

     val cake = bakery.bakeCake()
     me.eat(cake) 
}

如果您曾经使用过Google Guice ,则可能会看到相似之处:trait-modules直接对应于Guice模块。 但是,这里我们获得了额外的类型安全性和编译时检查,以确保满足所有类的依赖性要求。

当然,模块特征不仅可以包含新的对象实例化,但是您必须谨慎,不要在其中放置过多的逻辑–在某些时候,您可能需要提取一个类。 也包含在模块中的典型代码是例如新的actor创建代码和设置缓存。

依存关系

如果我们的特征模块具有模块间依赖性,该怎么办? 我们有两种方法可以解决该问题。

首先是抽象成员。 如果模块中需要一个类的实例,我们可以简单地将其定义为trait-module的抽象成员。 然后,必须在其他模块中实现此抽象成员,最后再与我们的模块组成该模块。 在这里使用一致的命名约定会有所帮助。 所有抽象依赖项都在某个时刻定义的事实由编译器检查。

第二种方法是通过继承进行组合。 如果我们想从三个较小的模块中创建一个较大的模块,则可以简单地扩展其他模块特性,由于继承的工作方式,我们可以使用在那里定义的所有对象。

将这两种方法放在一起,例如:

// composition via inheritance: bakery depends on crop and livestock modules
trait BakeryModule extends CropModule with LivestockModule {
     lazy val bakery = new Bakery(mill, dairyFarm)
}   

// abstract member: we need a bakery
trait CafeModule {
     lazy val espressoMachine = new EspressoMachine()
     lazy val cafe = new Cafe(bakery, espressoMachine)

     def bakery: Bakery
}

// the abstract bakery member is implemented in another module
object CafeApp extends CafeModule with BakeryModule {
     cafe.orderCoffeeAndCroissant()
}

多种实现

进一步讲这个想法,在某些情况下,我们可能具有trait-module-interfaces和几个trait-module-implementation。 该接口将仅包含抽象成员,而实现将关联适当的类。 如果其他模块仅依赖于trait-module-interface,那么当我们完成最后的组合时,我们可以使用任何实现。

但是,这并不完美。 在编写代码时,必须静态地知道实现-我们无法动态决定我们要使用的实现。 如果我们只想为一个特征接口动态选择一个实现,那不是问题–我们可以使用一个简单的“ if”。 但是,每增加一种组合,我们要处理的案件就会成倍增加。 例如:

trait MillModule {
     def mill: Mill
}

trait CornMillModule extends MillModule { 
     lazy val cornField = new CornField()
     lazy val mill = new CornMill(cornField)
}

trait WheatMillModule extends MillModule { 
     lazy val wheatField = new WheatField()
     lazy val mill = new WheatMill(wheatField)
}

val modules = if (config.cornPreferred) {
     new BakeryModule with CornMillModule
} else {
     new BakeryModule with WheatMillModule
}

会更好吗?

当然! 总有一些要改进的地方:)。 已经提到了问题之一–您无法选择动态使用哪个特征模块(运行时配置)。

可以改进的另一个领域是特征模块和包之间的关系。 一个好的方法是每个包(或每个包树)有一个特征模块。 这样,你在逻辑组代码实现的一些功能在单一封装中,并指定如何形成的实现类应该性状模块中使用。 但是为什么要同时定义包和特征模块呢? 也许它们可以以某种方式合并在一起? 我在Veripacks项目中一直在探索增加软件包的作用。

限制某些已定义对象的可见性也可能很好。 遵循“每个包一个公共类”的规则,这里我们可能有“每个特征模块一个公共对象”。 但是,如果我们从较小的特征模块中创建较大的特征模块,则较大的模块将无法限制其组成的模块中对象的可见性。 实际上,较小的模块将必须知道其可见性的最大范围,并使用适当的private [package name]修饰符(假设较大的模块位于父程序包中)。

加起来

总体而言,我们发现该解决方案是一种简单,清晰的结构化代码和创建对象图的方法。 它仅使用本机Scala构造,不依赖任何框架或库,并提供编译时检查以确保所有内容均已正确定义。

祝您好胃口!


翻译自: https://www.javacodegeeks.com/2014/03/using-scala-traits-as-modules-or-the-thin-cake-pattern.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值