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

208 Views Asked by At

Using self-type for dependency injections, cause to expose public method of other traits ,which break the single responsibility principal. Let's me talk with example

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

My Sample class dependes on Output trait and of course I would like to use Output trait methods in my Sample class. But with above code example my Sample class expose output method which doesn't come from its functionality and break single responsibility of Sample.

How can I avoid it and keep using self-type and cake-pattern?

2

There are 2 best solutions below

3
Dima On BEST ANSWER

Responsibility of providing the output still lies with the component implementing Output. The fact that your class provides access to it is no different from something like:

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

Sure, you can make out private here, but you could also have .output protected in ConsoleOutput if you don't want it accessible from outside as well.

(The answer to your comment in the other answer is that if you also want to use it "stand-alone", then you subclass it, and make output public in the subclass).

2
Tim On

The self type is not really relevant here. Inheriting from another class exposes the public methods of that class regardless of any self type. So any inheritance from a class with public methods can be said to break the single responsibility principle.

If the trait is intended to be use for dependency injection then it should make its methods protected so that they are not exposed.

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

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

Comment on the accepted answer

The accepted answer claims that the "Responsibility of providing the output still lies with the component implementing Output". This is incorrect, and shows confusion between a type and an implementation.

The behaviour of an object is specified by its type, not its implementation (Liskov substitution principle). The type is the contract that tells the user what the object can do. Therefore it is the type that specifies the responsibilities, not the implementation.

The type Sample with ConsoleOutput has the output method from the Object type and the doSomething method from the Sample type. Therefore it has the responsibility of providing an implementation of both of those methods. The fact that the implementation of output is in ConsoleOuput is irrelevant to the type and is therefore irrelevant to who is responsible for it.

The Sample with ConsoleOutput object could easily override the implementation of output in which case it would clearly be responsible for that method, not ConsoleOutput. The fact that Sample with ConsoleOutput chooses not to change the implementation of output does not mean that it is not responsible for it. The responsibilities of an object do not change when the implementation changes.

Explanation of the Single Responsibility Principle

This principle is the first of the five SOLID principles of software engineering. As Wikipedia explains, "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."

In other words, don't do this:

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

but do this instead:

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

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

computePi and countVowels are different parts of the functionality of the program and therefore they they should be encapsulated in different classes.

The third SOLID principle is the Liskov Substitution Principle which says that the functionality of an object should depend solely on the type and should not be affected by the implementation. You should be able to change the implementation and still use the object in the same way with the same results.

Since the functionality of an object is fully defined by the type of an object, the responsibilities of an object are also fully defined by the type. Changing the implementation does not change the responsibilities.