在 Scala 中使用自类型时如何保持单一职责?

How to keep single responsibility when using self-type in Scala?

使用自类型进行依赖注入,导致暴露其他特征的public方法,这打破了单一职责原则。让我来举例说明

trait Output {
  def output(str: String): Unit
}

trait ConsoleOutput extends Output {
  override def output(str: String): Unit = println(str)
}

class Sample {
  self: Output =>

  def doSomething() = {
    // I do something stupid here!
    output("Output goes here!")
  }
}

val obj = new Sample with ConsoleOutput
obj.output("Hey there")

我的 Sample class 取决于 Output 特征,当然我想在我的 Sample [=24= 中使用 Output 特征方法].但是对于上面的代码示例,我的 Sample class 公开了 output 方法,该方法不是来自其功能,并且打破了 Sample.

的单一责任

如何避免它并继续使用自类型和蛋糕模式?

自我类型在这里并不重要。从另一个 class 继承会公开那个 class 的 public 方法,而不管任何自身类型。所以任何继承自 class 和 public 方法都可以说是打破了单一责任原则。

如果 trait 旨在用于依赖项注入,那么它应该使其方法 protected 不被暴露。

trait Output {
  protected def output(str: String): Unit
}

trait ConsoleOutput extends Output {
  protected override def output(str: String): Unit = println(str)
}

对接受的答案发表评论

接受的答案声称 "Responsibility of providing the output still lies with the component implementing Output"。这是不正确的,并且显示了类型和实现之间的混淆。

一个对象的行为是由它的类型指定的,而不是它的实现(Liskov 替换原则)。类型是告诉用户对象可以做什么的契约。因此,指定职责的是 type,而不是 implementation

类型 Sample with ConsoleOutput 具有来自 Object 类型的 output 方法和来自 Sample 类型的 doSomething 方法。因此它有责任提供这两种方法的实现。 output 的实现在 ConsoleOuput 中的事实与类型无关,因此与负责它的人无关。

Sample with ConsoleOutput 对象可以很容易地覆盖 output 的实现,在这种情况下,它显然负责该方法,而不是 ConsoleOutputSample with ConsoleOutput 选择不更改 output 的实现并不意味着它不对此负责。当实现改变时,对象的职责不会改变。

Single Responsibility Principle

的解释

这个原则是软件工程五个 SOLID 原则中的第一个。 正如维基百科解释的那样,"The single responsibility principle [] states that every module or class should have responsibility over a single part of the functionality provided by the software, and that responsibility should be entirely encapsulated by the class."

换句话说,不要这样做:

class MultipleResponsibilities {
   def computePi(places: Int): List[Int]
   def countVowels(text: String): Int
}

但改为这样做:

class PiComputer {
  def computePi(places: Int): List[Int]
}

class VowelCounter {
   def countVowels(text: String): Int
}

computePicountVowels 是程序功能的不同部分,因此它们应该封装在不同的 class 中。

第三个 SOLID 原则是 Liskov Substitution Principle,它表示对象的功能应该完全取决于类型,不应受实现的影响。您应该能够更改实现并仍然以相同的方式使用对象并获得相同的结果。

由于对象的功能完全由对象的类型定义,因此对象的职责也完全由类型定义。更改实现不会更改职责。

提供输出的责任 仍然属于实施 Output 的组件。事实上,您的 class 提供对它的访问与类似的内容没有什么不同:

  class Foo(val out: Output)
  new Foo(new ConsoleOutput{}).out.output

当然,您可以在此处将 out 设为私有,但如果您不希望从外部访问它,也可以在 ConsoleOutput 中保护 .output

(在另一个答案中对您的评论的回答是,如果您也想使用它 "stand-alone",那么您将其子class,并使 output public 在子class).