scala 将方法放入 trait 或 case 类?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16482103/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-22 05:17:21  来源:igfitidea点击:

Put method in trait or in case class?

scalaobject-oriented-analysis

提问by Mika?l Mayer

There are two ways of defining a method for two different classes inheriting the same trait in Scala.

在 Scala 中,有两种方法可以为继承相同特征的两个不同类定义方法。

sealed trait Z { def minus: String }
case class A() extends Z { def minus = "a" }
case class B() extends Z { def minus = "b" }

The alternative is the following:

替代方案如下:

sealed trait Z { def minus: String = this match {
    case A() => "a"
    case B() => "b"
}
case class A() extends Z
case class B() extends Z

The first method repeats the method name, whereas the second method repeats the class name.

第一种方法重复方法名称,而第二种方法重复类名称。

I think that the first method is the best to use because the codes are separated. However, I found myself often using the second one for complicated methods, so that adding additional arguments can be done very easily for example like this:

我认为第一种方法最好用,因为代码是分开的。但是,我发现自己经常将第二种方法用于复杂的方法,因此可以非常轻松地添加其他参数,例如:

sealed trait Z {
  def minus(word: Boolean = false): String = this match {
    case A() => if(word) "ant" else "a"
    case B() => if(word) "boat" else "b"
}
case class A() extends Z
case class B() extends Z

What are other differences between those practices? Are there any bugs that are waiting for me if I choose the second approach?

这些做法之间还有哪些其他区别?如果我选择第二种方法,是否有任何错误在等着我?

EDIT:I was quoted the open/closed principle, but sometimes, I need to modify not only the output of the functions depending on new case classes, but also the input because of code refactoring. Is there a better pattern than the first one? If I want to add the previous mentioned functionality in the first example, this would yield the ugly code where the input is repeated:

编辑:我被引用了开放/封闭原则,但有时,我不仅需要根据新案例类修改函数的输出,还需要因为代码重构而修改输入。有没有比第一个更好的模式?如果我想在第一个示例中添加前面提到的功能,这将产生重复输入的丑陋代码:

sealed trait Z { def minus(word: Boolean): String  ; def minus = minus(false) }
case class A() extends Z { def minus(word: Boolean) = if(word) "ant" else "a" }
case class B() extends Z { def minus(word: Boolean) = if(word) "boat" else "b" }

采纳答案by Xavier Guihot

Note that with Dotty(foundation of Scala 3), you have the possibility to use trait parameters(just like classes have parameters), which simplifies things quite a lot in this case:

请注意,使用Dotty(foundation of Scala 3),您可以使用特征参数(就像类具有参数一样),这在这种情况下大大简化了事情:

trait Z(x: String) { def minus: String = x }
case class A() extends Z("a")
case class B() extends Z("b")
A().minus // "a"
B().minus // "b"

回答by Mik378

I would choose the first one.

我会选择第一个。

Why ? Merely to keep Open/Closed Principle.

为什么 ?仅仅为了保持开放/封闭原则

Indeed, if you want to add another subclass, let's say case class C, you'll have to modify supertrait/superclass to insert the new condition... ugly

事实上,如果你想添加另一个子类,比如说case class C,你必须修改 supertrait/superclass 来插入新的条件......丑陋

Your scenario has a similar in Java with template/strategypattern against conditional.

您的场景在 Java 中有类似的模板/策略模式针对条件

UPDATE:

更新:

In your last scenario, you can't avoid the "duplication" of input. Indeed, parameter type in Scala isn't inferable.

在您的最后一个场景中,您无法避免输入的“重复”。事实上,Scala 中的参数类型是不可推断的。

It still better to have cohesive methods than blending the whole inside one method presenting as many parameters as the method union expects.

拥有内聚方法比将整体混合在一个方法中更好,该方法提供与方法联合期望的一样多的参数。

Just Imagine ten conditions in your supertrait method. What if you change inadvertently the behavior of one of each? Each change would be risked and supertrait unit tests should always run each time you modify it ...

想象一下你的超特质方法中的十个条件。如果您无意中改变了其中一个的行为怎么办?每次更改都会冒风险,并且每次修改时都应始终运行超特征单元测试......

Moreover changing inadvertently an input parameter (not a BEHAVIOR) is not "dangerous" at all. Why? because compiler would tell you that a parameter/parameter type isn't relevant any more. And if you want to change it and do the same for every subclasses...ask to your IDE, it loves refactoring things like this in one click.

此外,无意中更改输入参数(不是 BEHAVIOR)根本不是“危险的”。为什么?因为编译器会告诉您参数/参数类型不再相关。如果你想改变它并对每个子类做同样的事情......询问你的 IDE,它喜欢一键重构这样的东西。

As this linkexplains:

正如此链接所解释的:

Why open-closed principle matters:

为什么开闭原则很重要:

No unit testing required.
No need to understand the sourcecode from an important and huge class.
Since the drawing code is moved to the concrete subclasses, it's a reduced risk to affect old functionallity when new functionality is added.

不需要单元测试。
无需了解重要而庞大的类的源代码。
由于绘图代码已移至具体子类,因此在添加新功能时影响旧功能的风险降低了。

UPDATE 2:

更新 2:

Here a sample avoiding inputs duplication fitting your expectation:

这是一个避免输入重复的示例,符合您的期望:

sealed trait Z { 
     def minus(word: Boolean): String = if(word) whenWord else whenNotWord
     def whenWord: String
     def whenNotWord: String             
  }

case class A() extends Z { def whenWord = "ant"; def whenNotWord = "a"}

Thanks type inference :)

谢谢类型推断:)

回答by cmbaxter

Personally, I'd stay away from the second approach. Each time you add a new sub class of Z you have to touch the shared minus method, potentially putting at risk the behavior tied to the existing implementations. With the first approach adding a new subclass has no potential side effect on the existing structures. There might be a little of the Open/Closed Principle in here and your second approach might violate it.

就个人而言,我会远离第二种方法。每次添加 Z 的新子类时,您都必须接触共享的减法方法,这可能会使与现有实现相关的行为面临风险。使用第一种方法添加新的子类对现有结构没有潜在的副作用。这里可能有一点开放/封闭原则,你的第二种方法可能会违反它。

回答by petrn

Open/Closed principle can be violated with both approaches. They are orthogonal to each other. The first one allows to easily add new type and implement required methods, it breaks Open/Closed principle if you need to add new method into hierarchy or refactor method signatures to the point that it breaks any client code. It is after all reason why default methods were added to Java8 interfaces so that old API can be extended without requiring client code to adapt. This approach is typical for OOP.

这两种方法都可能违反开放/封闭原则。它们彼此正交。第一个允许轻松添加新类型并实现所需的方法,如果您需要将新方法添加到层次结构或重构方法签名以破坏任何客户端代码,它会破坏开放/关闭原则。归根结底,这就是为什么将默认方法添加到 Java8 接口的原因,以便可以扩展旧 API,而无需客户端代码进行调整。这种方法是 OOP 的典型做法。

The second approach is more typical for FP. In this case it is easy to add methods but it is hard to add new type (it breaks O/C here). It is good approach for closed hierarchies, typical example are Algebraic Data Types (ADT). Standardized protocol which is not meant to be extended by clients could be a candidate.

第二种方法对于 FP 更为典型。在这种情况下,添加方法很容易,但添加新类型很难(这里打破了 O/C)。这是封闭层次结构的好方法,典型的例子是代数数据类型(ADT)。不打算由客户端扩展的标准化协议可能是候选协议。

Languages struggle to allow to design API which would have both benefits - easy to add types as well as adding methods. This problem is called Expression Problem. Scala provides Typeclass pattern to solve this problem which allows to add functionality to existing types in ad-hoc and selective manner.

语言努力允许设计具有两个好处的 API - 易于添加类型以及添加方法。这个问题称为表达式问题。Scala 提供了 Typeclass 模式来解决这个问题,它允许以临时和选择性的方式向现有类型添加功能。

Which one is better depends on your use case.

哪个更好取决于您的用例。