May 31, 2011

Design Patterns Simplified: The Adapter Pattern

Even though you may never have heard of the Adapter pattern or may never have seen it formally defined, chances are that you've used it in your coding practice countless times. Adapters are often referred to as "wrappers" and are used when the interface of an existing class must be adapted to match the interface of a target class. Not sure where to get started with the Adapter pattern? Well, then let's start with an example, shall we?

Author's Note: If you've already studied design patterns a little bit, you know that there are actually two adapter patterns: the Object Adapter Pattern and the Class Adapter Pattern. However, the Class Adapter Pattern is really only useful in languages that truly support multiple inheritance. Since that's a short list of languages, we're going to focus on the Object Adapter Pattern in this tutorial. However, feel free to browse some of the titles in the Further Reading section if you want to learn more about the Class Adapter Pattern. For the remainder of this tutorial, assume that "adapter pattern" refers to the Object Adapter Pattern only.

It All Starts with Headphones
I love my Beats Solo headphones. They're great for when I'm listening to my favorite music on my iPhone or listening to iTunes at work.
Monster Beats Solo High Definition On-Ear Headphones with ControlTalk (White)However, my cans don't work well with my m-audio recording interface because it expects a 1/4" connector and my Beats Solo headphones are only equipped with a 3.5mm connector. What's an audiophile to do? There are several options:

  1. I could buy another pair of headphones that have the expected 1/4" connector, however it would be difficult to justify the cost of a new pair of studio quality headphones for such a specific use case.
  2. I could modify my Beats Solo headphones by removing the 3.5mm connector and replacing it with a 1/4" connector, however doing so would make my Beats Solo headphones unusable on my devices that expect a 3.5mm connector, and is prone to error (the sound quality may suffer, or worse, my headphones may not even work after the mod!).
  3. Luckily, a third option exists: I can simply purchase an adapter at my local electronics store that will take my headphones' 3.5mm output as its input, while providing the expected 1/4" connector to my m-audio interface. When I no longer need a 1/4" connector I can simply remove the adapter and I can once again use my headphones with all of the devices I own that expect a 3.5mm connector.
Obviously, the third option is by far the most reasonable one.

The Adapter Pattern in Software Design
The motivation for using the Adapter Design Pattern is virtually the same as the motivation for using an adapter on my headphones. If we have a class that expects a certain interface and another class that provides the functionality we want but doesn't match the necessary interface, then we want to use an adapter class that will provide the expected interface while leveraging the functionality that we already have.

Here's a UML class diagram showing what the adapter pattern looks like:

In the example above, Target is an abstract class that provides the interface that Client expects to use. ConcreteTargetA and ConcreteTargetB are concrete implementations of Target. TargetBAdaptee is a pre-existing concrete class that provides some or all of the functionality promised by ConcreteTargetB, but doesn't provide the interface that Client expects. Rather than recreate the functionality contained in TargetBAdaptee within ConcreteTargetB, we can create an instance of TargetBAdaptee in ConcreteTargetB, and then leverage the states and behaviors of the former within the implementation of the latter, which provides the necessary interface to Client.

Got it? No? Okay, let's recreate the UML diagram above but use the objects contained in our real-world example:

In the diagram above, my m-audio studio recording interface (Client) expects a certain connector type from any headphones (Target) connected to it. SonyMDR (ConcreteTargetA) provides both the functionality and type of connector (interface) that my m-audio interface expects. My Beats Solo headphones (TargetBAdaptee) provide the necessary functionality, but not the connector type, that my m-audio interface expects. However, my 1/4" TRS Adapter (ConcreteTargetB) can provide the connector type that my m-audio interface expects while also using the functionality provided by my Beats Solo headphones.

Got it now? Good.

Adapter Pattern Rationale
The first question that is likely on your mind right now is: "Why use the adapter pattern at all?"

After all, you could simply alter the interface of your target class to make it compatible with the interface that your would-be adaptee provides, and then alter portions of code outside the target class hierarchy to use the new interface. This may seem somewhat attractive at first, but is really intractable in large systems, and violates the Open-Closed Principle.

In many cases, especially when using open-source libraries, it's tempting to simply alter the implementation of a class so that its interface meets your needs, rather than create another class to wrap its implementation within the required interface. However, altering the implementation or interface of a class that already exists is in violation of the Open-Closed Principle, and makes for very tightly-coupled software design. Following this approach rather than using the adapter pattern would be like cutting the existing connector off your headphones and attaching a different connector.

Alternatively, some novice programmers take to copying and pasting code from an existing class into their concrete implementations, however this is actually more work than adapting an existing class, can result in unresolved dependencies, and will undoubtedly require very careful testing. Taking this measure would (roughly) be the equivalent of buying a new pair of headphones rather than using the ones you already have.

The adapter pattern is an ideal choice because it allows you to add existing functionality to your system without altering the targeted interface, client code, or the implementation of the existing functionality. Therefore, while it may seem like more effort at first to create an adapter class, it actually requires less effort than any other proposed option when you consider all facets of the development process.

Adapter Pattern Use Cases
Like any other design pattern, the adapter pattern is not applicable in all cases. Here is the typical use case:

  • You have a requirement to provide (or maintain) a particular interface to client objects (e.g.: Headphones).
  • There is a class that already exists that provides the required implementation, but does not provide the required interface (e.g.: Beats Solo).
If your design requirements do not include the above, then the adapter pattern is likely not the right choice for your solution.

I hope you've enjoyed this introduction and overview of the Adapter Design Pattern, which is part of the Design Patterns Simplified series. If you like what you've read so far, feel free to subscribe to my feed in the sidebar on the right. If you have questions, suggestions, or errata, please feel free to comment below or send me an email at brian [at] brian [hyphen] driscoll [dot] com.

Further Reading

No comments: