Put method in trait or in case class?
Asked Answered
N

4

15

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" }
Negligence answered 10/5, 2013 at 12:18 Comment(1)
Let me speculate that adding any non-trivial method to the case class is unsound from OO perspective. Case class instance exposes all of its internals, and good objects are not. So it is not a "real" object but primitive alike, like string or tuple.Dogwatch
R
1

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"
Refractor answered 21/5, 2019 at 20:2 Comment(2)
Very interesting. Is there a way we can expose the variable x, e.g. trait Z(val x: String) ?Inclining
@MikaëlMayer you can indeed expose the trait variable: trait Z(val x: String) { def minus: String = x } to access it: A().x. You can also add several parameters to the trait and choose which to use in the "word" condition: trait Z(x: String, y: String) { def minus(word: Boolean = false): String = if (word) y else x } - case class A() extends Z("a", "c") - A().minus(true) which returns "c".Refractor
H
8

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

Hesitant answered 10/5, 2013 at 12:22 Comment(6)
and if you want to add a new argument to the function, you need to modify it in every subfunction, even if it is not necessary. Isn't that ugly as well?Inclining
@Mikaël Mayer In your last scenario(update), we well see that B behavior doesn't need at all the emphasizeA. Thus I can return this question to your first comment: what is better? Cohesive function, quickly understandable and short, or imposing all behaviors from subclass to fit a specific signature, even they do not need them ...Hesitant
Ok, I'll refine my question.Inclining
Maybe if there is always one argument only, which itself is a class, I can have the first approach to let me add new functionalities very easily without rewriting the arguments.Inclining
@Mikaël Mayer Re-Updated ;)Hesitant
@Mikaël Mayer Repeating argument when dealing with inheritance has NEVER been a bad thing. It's the essence of interface contract and refined subclasses.Hesitant
D
2

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.

Danella answered 10/5, 2013 at 12:25 Comment(0)
A
2

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.

Ailina answered 26/1, 2017 at 16:14 Comment(1)
Hello thanks for this insight, can you show an example on how to use Typeclass patterns?Inclining
R
1

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"
Refractor answered 21/5, 2019 at 20:2 Comment(2)
Very interesting. Is there a way we can expose the variable x, e.g. trait Z(val x: String) ?Inclining
@MikaëlMayer you can indeed expose the trait variable: trait Z(val x: String) { def minus: String = x } to access it: A().x. You can also add several parameters to the trait and choose which to use in the "word" condition: trait Z(x: String, y: String) { def minus(word: Boolean = false): String = if (word) y else x } - case class A() extends Z("a", "c") - A().minus(true) which returns "c".Refractor

© 2022 - 2024 — McMap. All rights reserved.