【问题标题】:Mixins vs composition in scalaMixins 与 Scala 中的组合
【发布时间】:2011-03-26 06:08:18
【问题描述】:

在 Java 世界中(更准确地说,如果您没有多重继承/混合),经验法则非常简单:“优先考虑对象组合而不是类继承”。

如果您还考虑使用 mixins,我想知道它是否/如何改变,尤其是在 scala 中?
mixin 是否被认为是一种多重继承或更多类组合的方式?
是否还有“优先对象组合优于类组合”(或相反)指南?

我见过很多例子,当对象组合也可以完成工作时,人们使用(或滥用)mixin,但我并不总是确定哪个更好。在我看来,你可以用它们实现非常相似的东西,但也有一些区别,一些例子:

  • 可见性 - 使用 mixins,一切都成为公共 api 的一部分,而组合则不是这样。
  • 冗长 - 在大多数情况下,mixin 不那么冗长并且更易于使用,但并非总是如此(例如,如果您还在复杂的层次结构中使用 self 类型)

我知道简短的回答是“视情况而定”,但可能有一些典型的情况是这样或那样更好。

到目前为止我可以提出的一些指导方针示例(假设我有两个特征 A 和 B,并且 A 想使用 B 的一些方法):

  • 如果您想使用 B 中的方法扩展 A 的 API,则使用 mixins,否则为组合。但是,如果我创建的类/实例不是公共 API 的一部分,则无济于事。
  • 如果你想使用一些需要混合的模式(例如Stackable Trait Pattern),那么这是一个简单的决定。
  • 如果你有循环依赖,那么使用 self 类型的 mixin 会有所帮助。 (我尽量避免这种情况,但这并不总是那么容易)
  • 如果您想要一些动态的、运行时决定如何进行组合然后是对象组合。

在许多情况下,mixin 似乎更容易(和/或不那么冗长),但我很确定它们也有一些陷阱,比如两篇 artima 文章中描述的“上帝类”和其他:part 1,@ 987654323@(顺便说一句,在我看来,大多数其他问题与 scala 无关/不那么严重)。

你还有更多这样的提示吗?

【问题讨论】:

    标签: scala composition mixins


    【解决方案1】:

    如果您只在类定义中混入抽象特征,然后在对象实例化时混入相应的具体特征,那么在 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 
    

    这个结构有几个值得推荐的地方。首先,它可以防止类爆炸,因为需要不同的特征功能组合。其次,它允许轻松测试,因为可以创建和混合“无所事事”的具体特征,类似于模拟对象。最后,我们完全隐藏了所使用的锁定特性,甚至锁定仍在继续,对我们服务的消费者来说。

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

    【讨论】:

    • 构造this => Logging是什么意思?它无法编译。
    • 这是一个自我类型的注解,我总是忘记它的语法。已编辑。在这种情况下,这意味着任何扩展 MyService 的对象也必须扩展 Locking
    【解决方案2】:

    您未提及的其他差异:

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

    (Programming Scala)

    如果您发现某个特定 trait 最常被用作其他类的父级,因此子类表现为父级 trait,那么请考虑将 trait 定义为一个类,以使这种逻辑关系更加清晰。
    (我们说 behaves as,而不是 is a,因为前者是基于 Liskov 替换原则的更精确的继承定义 - 例如,参见 [Martin2003] .)

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

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

    因此advice, still from Programming Scala:

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

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

    最后一部分,关于对象的初始状态,通常有助于在给定概念的类(和类组合)和特征(和混合)之间做出决定.

    【讨论】:

    • 感谢您的回答,但我认为这更多的是关于特性与类。我在那个话题上感觉更舒服 :-) 当我使用“类组合”这个词时,我可能不够精确。我的意思是在 Stackable Trait Pattern 文章中描述的,“这个模式在结构上类似于装饰器模式,除了它涉及用于类组合而不是对象组合的装饰”,尽管它混合了类和特性。
    • @Sandor:我明白了。我正在解决您问题的基本部分,因为我对这种区别还不完全满意,但您应该很快就会得到更准确的答案。
    • "mixins (trait) 没有构造函数参数。" 有趣的是,Dotty(成为 Scala 3)支持 trait parameters。因此,您提到的 Programming Scala 的经验法则可能会发展。
    • @Jubobs 谢谢。 8 年后,这条规则可能会演变,我并不感到惊讶。
    猜你喜欢
    • 2021-01-03
    • 2010-10-29
    • 1970-01-01
    • 2014-03-06
    • 2021-12-02
    • 2016-07-22
    • 1970-01-01
    • 2017-02-16
    • 2021-11-09
    相关资源
    最近更新 更多