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.
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:
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.
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:
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.
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.
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.
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.
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.
3.144.255.87