我想描述一种在模块化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