The adapter design pattern

In many cases, we have to make applications work by combining different components together. However, quite often, we have a problem where the component interfaces are incompatible with each other. Similarly with using public or any libraries, which we cannot modify ourselves—it is quite rare that someone else's views will be exactly the same as ours in our current settings. This is where adapters help. Their purpose is to help incompatible interfaces work together without modifying their source code.

We will be showing how adapters work using a class diagram and an example in the next few subsections.

Class diagram

For the adapter class diagram, let's imagine that we want to switch to using a new logging library in our application. The library we are trying to use has a log method that takes the message and the severity of the log. However, throughout our whole application, we expect to have the info, debug, warning and error methods that only take the message and automatically set the right severity. Of course, we cannot edit the original library code, so we have to use the adapter pattern. The following figure shows the class diagram that represents the adapter design pattern:

Class diagram

In the preceding diagram, we can see our adapter (AppLogger) extend and also use an instance of Logger as a field. While implementing the methods, we then simply call the log method with different parameters. This is the general adapter implementation and we will see the code for it in the next subsection. There are some cases where extending might not be possible and we will show how Scala can deal with this. Also, we will show some advanced usage of the language features to achieve the adapter pattern.

Code example

First of all, let's see the code for our Logger that we assume that we cannot change:

class Logger {
  def log(message: String, severity: String): Unit = {
    System.out.println(s"${severity.toUpperCase}: $message")
  }
}

We've tried to keep it as simple as possible in order to not distract the reader from the main purpose of this book. Next, we could either just write a class that extends Logger or we could provide an interface for abstraction. Let's take the second approach:

trait Log {
  def info(message: String)
  def debug(message: String)
  def warning(message: String)
  def error(message: String)
}

Finally, we can create our AppLogger:

class AppLogger extends Logger with Log {
  override def info(message: String): Unit = log(message, "info")

  override def warning(message: String): Unit = log(message, "warning")

  override def error(message: String): Unit = log(message, "error")

  override def debug(message: String): Unit = log(message, "debug")
}

We can then use it in the following program:

object AdapterExample {
  def main(args: Array[String]): Unit = {
    val logger = new AppLogger
    logger.info("This is an info message.")
    logger.debug("Debug something here.")
    logger.error("Show an error message.")
    logger.warning("About to finish.")
    logger.info("Bye!")
  }
}

As expected, our output will look like the following:

Code example

Note

You can see that we haven't implemented the class diagram exactly as shown. We don't need the Logger instance as a field of our class, because our class is an instance of Logger already and we have access to its methods anyway.

This is how we implement and use the basic adapter design pattern. However, there are cases where the class we want to adapt is declared as final and we are unable to extend it. We will show how to handle this in the next subsection.

The adapter design pattern with final classes

If we declare our original logger as final, we will see that our code will not compile. There is a different way to use the adapter pattern in this case. Here is the code:

class FinalAppLogger extends Log {
  private val logger = new FinalLogger
  
  override def info(message: String): Unit = logger.log(message, "info")

  override def warning(message: String): Unit = logger.log(message, "warning")

  override def error(message: String): Unit = logger.log(message, "error")

  override def debug(message: String): Unit = logger.log(message, "debug")
}

In this case, we simply wrap the final logger inside a class and then use it to call the log method with different parameters. The usage is absolutely the same as before. This could have a variation where the logger is passed as a constructor parameter as well. This is useful in cases where creating the logger requires some extra parameterization during creation.

The adapter design pattern the Scala way

As we have already mentioned multiple times, Scala is a rich programming language. Because of this fact, we can use implicit classes to achieve what the adapter pattern does. We will be using the same FinalLogger that we had in the previous example.

Implicit classes provide implicit conversions in places where possible. In order for the implicit conversions to work, we need to have the implicits imported and that's why they are often defined in objects or package objects. For this example, we will use a package object. Here is the code:

package object adapter {

  implicit class FinalAppLoggerImplicit(logger: FinalLogger) extends Log {
    
    override def info(message: String): Unit = logger.log(message, "info")

    override def warning(message: String): Unit = logger.log(message, "warning")

    override def error(message: String): Unit = logger.log(message, "error")

    override def debug(message: String): Unit = logger.log(message, "debug")
  }
}

This is a package object for the package where our logger examples are defined. It will automatically convert a FinalLogger instance to our implicit class. The following code snippet shows an example usage of our logger:

object AdapterImplicitExample {
  def main(args: Array[String]): Unit = {
    val logger: Log = new FinalLogger
    logger.info("This is an info message.")
    logger.debug("Debug something here.")
    logger.error("Show an error message.")
    logger.warning("About to finish.")
    logger.info("Bye!")
  }
}

The final output will be exactly the same as our first example.

What is it good for?

The adapter design pattern is useful in cases after the code is designed and written. It allows to make, otherwise incompatible, interfaces work together. It is also pretty straightforward to implement and use.

What is it not so good for?

There is a problem with the last implementation mentioned in the preceding section. It is the fact that we will have to always import our package or normal object when using the logger. Also, implicit classes and conversions sometimes make the code much harder to read and understand. Implicit classes have some limitations, as described here: http://docs.scala-lang.org/overviews/core/implicit-classes.html.

As we already mentioned, the adapter design pattern is useful when we have code that we cannot change. If we are able to fix our source code, then this might be a better decision because using adapters throughout our program will make it difficult to maintain and hard to understand.

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

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