質問

There are two ways of defining a method for two different classes inheriting the same trait in 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" }
役に立ちましたか?

解決 4

Starting in Scala 3, you have the possibility to use trait parameters (just like classes have parameters), which simplifies things quite a lot in this case:

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"

他のヒント

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

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

UPDATE:

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

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.

As this link explains:

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:

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 :)

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.

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.

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.

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.

Which one is better depends on your use case.

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top