Scala中的Mixins与组合

81
在Java世界中(更准确地说,如果您没有多重继承/混合),经验法则非常简单:“优先使用对象组合而不是类继承”。我想知道如果您考虑了mixins,特别是在Scala中,它是否会发生变化?mixins是否被认为是多重继承的一种方式,还是更多的类组合?是否也有“优先使用对象组合而不是类组合”(或反之亦然)的指导方针?
我看到过很多人在可以使用对象组合的情况下使用(或滥用)mixins,并且我并不总是确定哪个更好。在我看来,您可以通过它们实现相似的事情,但也存在一些差异,例如:
可见性-使用mixins时,所有内容都成为公共API的一部分,而这在组合中并非如此。
冗长-在大多数情况下,mixins较少冗长且使用起来更容易,但并非始终如此(例如,如果您还在复杂的层次结构中使用自身类型)
我知道简短的答案是“取决于”,但可能有一些典型的情况,其中这个或那个更好。以下是我目前能想到的一些指南示例(假设我有两个traits A和B,A想要使用来自B的某些方法):
  • 如果您想使用B中的方法来扩展A的API,则使用mixins,否则使用组合。但是,如果我创建的类/实例不是公共API的一部分,则无济于事。
  • 如果您想使用需要mixins的某些模式(例如Stackable Trait Pattern),那么这是一个简单的决定。
  • 如果存在循环依赖关系,则mixins与自我类型可以帮助解决问题。(我尽量避免这种情况,但并不总是容易)
  • 如果您想要一些动态的运行时决策如何进行组合,则使用对象组合。

在许多情况下,mixins似乎更容易(和/或更简洁),但我非常确定它们也有一些缺陷,例如“上帝类”和其他在两篇artima文章中描述的问题:part 1part 2(顺便说一句,对于Scala来说,大多数其他问题似乎与之不相关/不太严重)。

您有更多类似的提示吗?

2个回答

41

如果您在Scala中将抽象特质混入到类定义中,然后在对象实例化时混入相应的具体特质,就可以避免很多人在使用混入时遇到的问题。例如:

trait Locking{
   // abstract locking trait, many possible definitions
   protected def lock(body: =>A):A
}

class MyService{
   this:Locking =>
}

//For this time, we'll use a java.util.concurrent lock
val myService:MyService = new MyService with JDK15Locking 

这个构造有几个值得推荐的地方。首先,它避免了由于需要不同组合的 trait 功能而导致类数量大量爆炸的问题。其次,它允许轻松测试,因为可以创建并混入“什么都不做”的具体 trait,类似于模拟对象。最后,我们已经完全隐藏了使用的锁定 trait,甚至隐藏了从我们服务的消费者那里进行加锁操作的过程。

既然我们已经克服了 mix-in 的大部分缺点,我们仍然需要在 mix-in 和组合之间做出权衡。对于我自己来说,我通常根据一个假设的委托对象是否完全被包含对象封装或者它是否有可能共享并拥有自己的生命周期来做决策。锁定提供了一个完全封装的委托对象的很好的例子。如果您的类使用锁对象来管理对其内部状态的并发访问,那么该锁完全由包含对象控制,它及其操作都不会作为类的公共接口广告。对于像这样完全封装的功能,我选择 mix-in。对于像数据源这样的共享功能,使用组合。


this => Logging 这个结构的意思是什么?它无法编译。 - HRJ
3
这是一个我经常忘记语法的自类型注释。在这种情况下,它意味着任何扩展了MyService的对象也必须扩展Locking。 - Dave Griffith

11

你没有提到的其他区别:

  • 特质类没有任何独立存在:

(Scala编程)

如果你发现一个特定的特质最常用于其他类的父类,以便子类表现为父特质,则考虑将该特质定义为类,以使这种逻辑关系更加清晰。
(我们说“表现为”,而不是“是”,因为前者是基于Liskov替代原则的继承的更精确的定义-例如,请参见[Martin2003]。)

[Martin2003]: Robert C. Martin, 敏捷软件开发:原则、模式和实践,Prentice-Hall,2003

  • 混入(trait)没有构造函数参数。

因此,仍然来自Scala编程的建议

避免在特质中使用无法初始化为合适默认值的具体字段。
改用抽象字段或将特质转换为带有构造函数的类
当然,无状态特质在初始化方面没有任何问题。

这是良好的面向对象设计的一般原则,即从构建过程完成的那一刻开始,实例始终处于已知的有效状态。

最后一部分关于对象的初始状态常常有助于决定给定概念的类(和类组合)与特质(和混入)之间的选择。


谢谢你的回答,但我认为这更多是关于特质与类的问题。在这个话题上,我感觉更加自在 :-) 当我使用“类组合”这个术语时,可能我没有表达得够准确。我的意思就像在 Stackable Trait Pattern 文章中所描述的那样,“这种模式在结构上类似于装饰器模式,只不过它涉及到类组合而不是对象组合”,尽管它混合了类和特质。 - Sandor Murakozi
@Sandor: 我理解你的意思。我回答了你问题的基本部分,因为我还没有完全理解这个区别,但你很快会得到更准确的答案。 - VonC
“Mixins(trait)没有构造函数参数。” 有趣的是,即将成为Scala 3的Dotty支持trait参数。因此,您提到的《Programming Scala》中的经验法则可能会发展演变。 - jub0bs
1
@Jubobs 谢谢。8年后,我并不惊讶这个规则可能会发展。 - VonC

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