Self types

One of the features of good code is the separation of concerns. Developers should aim to make classes and their methods responsible for one and only one thing. This helps in testing, maintaining, and simply understanding code better. Remember: simple is always better.

However, it is inevitable that when writing real software, we will need instances of some classes within other ones in order to achieve certain functionalities. In other words, once our building blocks are nicely separated, they would have dependencies in order to perform their functionality. What we are talking about here really boils down to dependency injection. Self types provide a way to handle these dependencies in an elegant way. In this section, we will see how to use them and what they are good for.

Using self types

Self types allow us to easily separate code in our applications, and then require it from other places. Everything gets clearer with an example, so let's have a look at one. Let's assume that we want to be able to persist information into a database:

trait Persister[T] {
  def persist(data: T)
}

The persist method will do some transformations on the data and then insert it in our database. Of course, our code is well written, so the database implementations are separated. We have the following for our database:

trait Database[T] {
  def save(data: T)
}

trait MemoryDatabase[T] extends Database[T] {
  val db: mutable.MutableList[T] = mutable.MutableList.empty 

  override def save(data: T): Unit = {
    System.out.println("Saving to in memory database.")
    db.+=:(data)
  }
}

trait FileDatabase[T] extends Database[T] {
  override def save(data: T): Unit = {
    System.out.println("Saving to file.")
  }
}

We have a base trait and then some concrete database implementations. So how do we pass our database to Persister? It should be able to call the save method defined in the database. Our possibilities include the following:

  • Extend Database in Persister. This would, however, also make Persister an instance of Database, and we don't want this. We will show why later.
  • Have a variable for Database in Persister and use it.
  • Use self types.

We are trying to see how self types work here, so let's use this approach. Our Persister interface will change to the following:

trait Persister[T] {
  this: Database[T] =>
    def persist(data: T): Unit = {
      System.out.println("Calling persist.")
      save(data)
    }
}

Now we have access to the methods in database and can call the save method inside Persister.

Tip

Naming the self type

In the preceding code, we included our self type using the following statement: this: Database[T] =>. This allows us to access the methods of our included types directly as if they were methods of the trait that includes them. Another way of doing the same here is by writing self: Database[T] => instead. There are many examples out there that use the latter approach, which is useful to avoid confusion if we need to refer to this in some nested trait or class definitions. Calling the methods of the injected dependencies using this approach, however, would require the developer to use self. in order to gain access to the required methods.

The self type requires any class that mixes Persister in, to also mix Database in. Otherwise, our compilation will fail. Let's create classes to persist to memory and database:

class FilePersister[T] extends Persister[T] with FileDatabase[T]
class MemoryPersister[T] extends Persister[T] with MemoryDatabase[T]

Finally, we can use them in our application:

object PersisterExample {
  def main(args: Array[String]): Unit = {
    val fileStringPersister = new FilePersister[String]
    val memoryIntPersister = new MemoryPersister[Int]
    
    fileStringPersister.persist("Something")
    fileStringPersister.persist("Something else")
    
    memoryIntPersister.persist(100)
    memoryIntPersister.persist(123)
  }
}

Here is the output of our program:

Calling persist.
Saving to file.
Calling persist.
Saving to file.
Calling persist.
Saving to in memory database.
Calling persist.
Saving to in memory database.

What self types do is different than inheritance. They require the presence of some code, and thus allow us to split a functionality nicely. This can make a huge difference in maintaining, refactoring, and understanding a program.

Requiring multiple components

In real applications, we might require more than one component using a self type. Let's show this in our example with a History trait that could potentially keep track of changes to roll back at some point. Ours will just do printing:

trait History {
  def add(): Unit = {
    System.out.println("Action added to history.")
  }
}

We need to use this in our Persister trait, and it will look like this:

trait Persister[T] {
  this: Database[T] with History =>
    def persist(data: T): Unit = {
      System.out.println("Calling persist.")
      save(data)
      add()
    }
}

Using the with keyword, we can add as many requirements as we like. However, if we just leave our code changes there, it will not compile. The reason for this is that we now must mix History in every class that uses Persister:

class FilePersister[T] extends Persister[T] with FileDatabase[T] with History
class MemoryPersister[T] extends Persister[T] with MemoryDatabase[T] with History

And that's it. If we now run our code, we will see this:

Calling persist.
Saving to file.
Action added to history.
Calling persist.
Saving to file.
Action added to history.
Calling persist.
Saving to in memory database.
Action added to history.
Calling persist.
Saving to in memory database.
Action added to history.

Conflicting components

In the preceding example, we had a requirement for the History trait, which has an add()method. What would happen if the methods in different components have the same signatures and they clash? Let's try this:

trait Mystery {
  def add(): Unit = {
    System.out.println("Mystery added!")
  }
}

We can now use this in our Persister trait:

trait Persister[T] {
  this: Database[T] with History with Mystery =>
    def persist(data: T): Unit = {
      System.out.println("Calling persist.")
      save(data)
      add()
    }
}

Of course, we will change all the classes that mix Persister in:

class FilePersister[T] extends Persister[T] with FileDatabase[T] with History with Mystery
class MemoryPersister[T] extends Persister[T] with MemoryDatabase[T] with History with Mystery

If we try to compile our application, we will see that it results in a failure with the following messages:

Error:(47, 7) class FilePersister inherits conflicting members:

  method add in trait History of type ()Unit  and

  method add in trait Mystery of type ()Unit

(Note: this can be resolved by declaring an override in class FilePersister.)

class FilePersister[T] extends Persister[T] with FileDatabase[T] with History with Mystery

      ^

Error:(48, 7) class MemoryPersister inherits conflicting members:

  method add in trait History of type ()Unit  and

  method add in trait Mystery of type ()Unit

(Note: this can be resolved by declaring an override in class MemoryPersister.)

class MemoryPersister[T] extends Persister[T] with MemoryDatabase[T] with History with Mystery

      ^

Luckily, the error messages also contain information that tells us how we can fix the problem. This is absolutely the same case that we saw earlier while using traits, and we can provide the following fix:

class FilePersister[T] extends Persister[T] with FileDatabase[T] with History with Mystery {
  override def add(): Unit ={
    super[History].add()
  }
}
class MemoryPersister[T] extends Persister[T] with MemoryDatabase[T] with History with Mystery {
  override def add(): Unit ={
    super[Mystery].add()
  }
}

After running the example, we will see the following output:

Calling persist.
Saving to file.
Action added to history.
Calling persist.
Saving to file.
Action added to history.
Calling persist.
Saving to in memory database.
Mystery added!
Calling persist.
Saving to in memory database.
Mystery added!

Self types and the cake design pattern

What we saw in our preceding examples was a pure example of dependency injection. We required one component to be available in another one through self types.

Note

Self types are often used for dependency injection. They are the main part of the Cake design pattern, which we will become familiar with later in this book.

The cake design pattern relies completely on self types. It encourages engineers to write small and simple components, which declare and use their dependencies. After all the components in an application are programmed, they can be instantiated inside a common component registry and made available to the actual application. One of the nice advantages of the cake design pattern is that it actually checks during compile time whether all the dependencies would be satisfied.

We will dedicate a complete section on the cake design pattern later in this book, where we will provide more details about how the pattern can actually be wired up, what advantages and drawbacks it has, and so on.

Self types versus inheritance

In the previous section, we said that we don't want to use inheritance in order to get access to the Database methods. Why is it? If we had made Persister extend Database, this would mean that it would become a database itself (is-a relationship). However, this is not correct. It uses a database in order to achieve its functionality.

Inheritance exposes a subclass to the implementation details of its parent. This, however, is not always desired. According to the authors of Design Patterns: Elements of Reusable Object-Oriented Software, developers should favor object composition over class inheritance.

Inheritance leaks functionality

If we use inheritance, we would also leak functionality to subclasses that we do not want. Let's see the following code:

trait DB {
  def connect(): Unit = {
    System.out.println("Connected.")
  }
  
  def dropDatabase(): Unit = {
    System.out.println("Dropping!")
  }
  
  def close(): Unit = {
    System.out.println("Closed.")
  }
}

trait UserDB extends DB {
  def createUser(username: String): Unit = {
    connect()
    try {
      System.out.println(s"Creating a user: $username")
    } finally {
      close()
    }
  }

  def getUser(username: String): Unit = {
    connect()
    try {
      System.out.println(s"Getting a user: $username")
    } finally {
      close()
    }
  }
}

trait UserService extends UserDB {
  def bad(): Unit = {
    dropDatabase()
  }
}

This could be a real-life scenario. Because this is how inheritance works, we would get access to dropDatabase in UserService. This is something we do not want, and we can fix it using self types. The DB trait stays the same. Everything else changes to the following:

trait UserDB {
  this: DB =>

  def createUser(username: String): Unit = {
    connect()
    try {
      System.out.println(s"Creating a user: $username")
    } finally {
      close()
    }
  }

  def getUser(username: String): Unit = {
    connect()
    try {
      System.out.println(s"Getting a user: $username")
    } finally {
      close()
    }
  }
}

trait UserService {
  this: UserDB =>
  
  // does not compile
  // def bad(): Unit = {
  // dropDatabase()
  //}
}

As the comments in the code show, in this last version of the code, we do not have access to the DB trait methods. We can only call the methods of the type we require, and this is exactly what we wanted to achieve.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.72.245