Type Safe Decorators

In OOP there exists a design pattern for extending functionality of methods via a form of composition, this pattern is commonly referred to as the Decorator Pattern. You can find this design pattern in your dusty copy of the gang of four. In case you don't have a copy handy here's the wikipedia definition.

In object-oriented programming, the decorator pattern (also known as Wrapper, an alternative naming shared with the Adapter pattern) is a design pattern that allows behavior to be added to an individual object, either statically or dynamically, without affecting the behavior of other objects from the same class.  

To model this design pattern in Scala with a user service that we would want to decorate with caching and logging. We would first define a common interface as an abstract trait. We would then implement the base user service such that it implements the abstract trait and all the decorators such that they also implement the abstract trait. You would then compose them together via dependency injection. This would look something like this:

trait UserService {

  def getUser(username: String): Option[User]

}

object UserService {

  def apply(): UserService = {
    val dbService      = new DatabaseUserService(new Database())
    val cacheService   = new CacheUserService(new Cache(), dbService)
    new LoggingUserService(new Logger(), cacheService)
  }

}

class DatabaseUserService(db: Database) extends UserService {

  def getUser(username: String): Option[User] = db.get(s"user:$username")

}

class CacheUserService(cache: Cache, service: UserService) extends UserService {

  def getUser(username: String): Option[User] = {
    cache.get(username) orElse service.getUser(username)
  }

}

class LoggingUserService(logger: Logger, service: UserService) extends UserService {

  def getUser(username: String): Option[User] = {
    logger.info(s"Attempting to get user: $username")
    val user = service.getUser(username)

    if (user.isDefined) {
      logger.info(s"Successfully got user: $username")
    } else {
      logger.info(s"No such user: $username")
    }

    user
  }

}

In the decorator pattern, the composition happens at the value level when we inject the obect to decorate into the decorator. Scala provides a means for us to do this at the type level through a feature known as Stackable Traits. For the most part Stackable Traits are exactly what they sound like, that's traits that allow you to compose methods by stacking them on top of each other.

Stackable methods in scala must be identifed with the keywords abstract override before the def keyword. Yes you read that right and it's not a typo. It seems like a weird use of the abstract keyword, but it is correct. This combination of keywords give you access to a super pointer in your method which reference the next class or trait in the stack.

Modeling our previous implementation of the decorator pattern using Stackable Traits instead would look something like this

trait UserService {

  def getUser(username: String): Option[User]

}

object UserService {

  def apply(): UserService = {
    new DatabaseUserService extends RiakDatabaseService
                            with CacheUserService
                            with MemcacheCacheService
                            with LogginUserService
                            with LogglyLoggerService {}
  }

}

trait DatabaseUserService extends UserService with DatabaseService {

  def getUser(username: String): Option[User] = db.get(s"user:$username")

}

class CacheUserService extends UserService with CacheService {

  abstract override def getUser(username: String): Option[User] = {
    cache.get(username) orElse super.getUser(username)
  }

}

class LoggingUserService extends UserService with LoggingService {

  abstract override def getUser(username: String): Option[User] = {
    logger.info(s"Attempting to get user: $username")
    val user = super.getUser(username)

    if (user.isDefined) {
      logger.info(s"Successfully got user: $username")
    } else {
      logger.info(s"No such user: $username")
    }

    user
  }

}

Now we'll know if our decorators are implemented properly if our program type checks. In the example above were also leveraging existential types via the Cake Pattern in lieu of constructor injection.

The stacked traits are executed from right to left such that in our example, the LoggingUserService will run first and it's invocation of super.getUser() will pass control to the CacheUserService that when it invokes it's super.getUser() will pass control to DatabaseUserservice