Airbrake Blog

Structural Design Patterns: Adapter

Written by Frances Banks | May 26, 2017 6:00:09 PM

Today we've finally made it to the Structural pattern section of our Guide to Software Design Patterns series, and to celebrate we'll be exploring the adapter design pattern (while possibly eating some delicious cake as well). Structural patterns are used to configure how entities interact with and are related to one another. Thus, the adapter pattern allows us to make two normally incompatible entities compatible with one another.

In this article we'll examine the adapter pattern in a bit more detail, illustrating some examples in both the real world as well as functional C# code. Let's get to it!

In the Real World

Whether we realize it or not, most of us use adapters (or converters) all the time. The act of talking on your phone uses an analog-to-digital converter to convert the analog signal of your voice as picked up by a microphone into a digital signal. And, of course, the reverse happens when you're hearing the voice of the person on the other end.

Speaking of phones, we all probably have at least one or two devices with a USB port to plug into a computer or a charger. There are now so many different types of USB connections out there that many adapters exist to allow for some semblance of compatibility between them.

Anyone who's an avid gamer like myself might also have tried to find a way to use gamepads or controllers on a console they weren't originally designed for. In many cases these experiments lead to third-party adapters to get the job done. While these adapters may not be very pretty or reliable, they can save you fortune when compared to buying an entirely new controller for $50 a pop.

Many more examples of adapters exist, but the basic idea should be clear: An adapter provides us with a way to integrate two objects that are normally incompatible with one another.

How It Works In Code

To illustrate an example of the adapter design pattern in code we'll continue with the gaming example above, where we have an XBox console but we want to use a PlayStation controller with it. This requires some sort of adapter that will convert the signals from the PlayStation controller into the appropriate signals that our XBox expects. With that in mind let's start with the full code example, then we'll examine it in more detail piece by piece.

using Utility;

namespace Adapter
{
class Program
{
static void Main(string[] args)
{
// Create new XBox console.
var xbox = new XBox();
// Play with default controller.
xbox.Play();
// Create new PlayStationController.
var playStationController = new PlayStationController();
// Create controller adapter for PlayStation controller.
var adapter = new ControllerAdapter(playStationController);
// Play with adapted controller.
xbox.Play(adapter);

Logging.Log("-----------------");

// Create new PlayStation console.
var playstation = new PlayStation();
// Play with default controller.
playstation.Play();
// Create new XBoxController.
var xboxController = new XBoxController();
// Create controller adapter for XBox controller.
var adapter2 = new ControllerAdapter(xboxController);
// Play with adapted controller.
playstation.Play(adapter2);
}
}

interface IController { }

class Controller : IController { }

class PlayStationController : Controller { }

class XBoxController : Controller { }

/// <summary>
/// Used to adapt incompatible controllers within console calls.
/// </summary>
class ControllerAdapter
{
public Controller Controller { get; set; }

public ControllerAdapter(Controller controller)
{
// Assign controller to adapter.
Controller = controller;
Logging.Log($"Using adapter on {controller.GetType().Name}.");
}
}

interface IConsole
{
void Play();
}

class Console : IConsole
{
private Controller _Controller;

/// <summary>
/// Controller field with custom set method to output controller being activated.
/// </summary>
protected Controller Controller
{
get { return _Controller; }
set
{
_Controller = value;
Logging.Log($"Plugging {Controller.GetType().Name} into {this.GetType().Name} console.");
}
}

public Console() { }

/// <summary>
/// Invoke Play call using default controller.
/// </summary>
public void Play()
{
Logging.Log($"Playing with {Controller.GetType().Name} on {this.GetType().Name} console.");
}

/// <summary>
/// Invoke Play with associated adapter controller.
/// </summary>
/// <param name="adapter"></param>
public void Play(ControllerAdapter adapter)
{
Controller = adapter.Controller;
Logging.Log($"Playing with {Controller.GetType().Name} on {this.GetType().Name} console.");
}
}

/// <summary>
/// Basic XBox console.
/// </summary>
class XBox : Console
{
public XBox() {
// Associate new XBoxController as default.
Controller = new XBoxController();
}
}

/// <summary>
/// Basic PlayStation console.
/// </summary>
class PlayStation : Console
{
public PlayStation()
{
// Associate new PlayStationController as default.
Controller = new PlayStationController();
}
}

}

At the most basic level we start with some controller classes to represent the unique controller types for both XBox and PlayStation. These inherit from the Controller class, which itself inherits from IController. For this example we don't actually need any fields, properties, or methods in these entities, so they're all empty and just represent the rudimentary objects that we'll use elsewhere in the example. Obviously, in the real world we could add additional logic to these controllers if needed:

interface IController { }

class Controller : IController { }

class PlayStationController : Controller { }

class XBoxController : Controller { }

Next we need our adapter. The ControllerAdapter class is relatively simple, but its purpose is not to be complex. Instead, it merely acts as an adapter (often called a wrapper in many programming contexts) so we can use it to associate a Controller object with a Console. As before, the logic is not very fleshed out here, but we could add additional logic within the ControllerAdapter method (or elsewhere in the class) that check which particular type of Controller object is passed in and make any necessary adaptation adjustments. Here we're just opting to output a simple message indicating that the passed controller has been adapted.

/// <summary>
/// Used to adapt incompatible controllers within console calls.
/// </summary>
class ControllerAdapter
{
public Controller Controller { get; set; }

public ControllerAdapter(Controller controller)
{
// Assign controller to adapter.
Controller = controller;
Logging.Log($"Using adapter on {controller.GetType().Name}.");
}
}

Next we have the IConsole interface which specifies a single Play() method:

interface IConsole
{
void Play();
}

Our Console class then inherits from IConsole. While not necessary, in this case we wanted to add some extra functionality to the Controller.set() method call by producing an output message that indicates which type of Controller is being plugged into the current console, so we used a private _Controller field. It's also particularly important to note that we have set the access modifier of the Console.Controller property to protected. This ensures that it cannot be publicly accessed by other entities (or outside its own scope), but that the Controller property can still be used within the Console class along with any inherited classes as well.

As with our controller classes the constructor of Console doesn't need to do anything to illustrate the adapter pattern, so we leave it blank.

Lastly, we have the Play() methods. The first Play() definition assumes no arguments are passed and outputs a message indicating that "play" is executed using the currently assigned Controller on the current Console. On the other hand, Play(ControllerAdapter adapter) is used when we want to play a game on our console using an incompatible controller. In this case, the only way to do so is to pass a ControllerAdapter instance to the Play() method, which then assigns the Controller and outputs the same indication message as the previous Play() definition:

class Console : IConsole
{
private Controller _Controller;

/// <summary>
/// Controller field with custom set method to output controller being activated.
/// </summary>
protected Controller Controller
{
get { return _Controller; }
set
{
_Controller = value;
Logging.Log($"Plugging {Controller.GetType().Name} into {this.GetType().Name} console.");
}
}

public Console() { }

/// <summary>
/// Invoke Play call using default controller.
/// </summary>
public void Play()
{
Logging.Log($"Playing with {Controller.GetType().Name} on {this.GetType().Name} console.");
}

/// <summary>
/// Invoke Play with associated adapter controller.
/// </summary>
/// <param name="adapter"></param>
public void Play(ControllerAdapter adapter)
{
Controller = adapter.Controller;
Logging.Log($"Playing with {Controller.GetType().Name} on {this.GetType().Name} console.");
}
}

The final two class definitions are for our unique console classes, XBox and PlayStation. These inherit from Console and therefore don't need to define much themselves, except that we assume each console will use its own respective Controllerclass by default, so we set that property during construction:

/// <summary>
/// Basic XBox console.
/// </summary>
class XBox : Console
{
public XBox() {
// Associate new XBoxController as default.
Controller = new XBoxController();
}
}

/// <summary>
/// Basic PlayStation console.
/// </summary>
class PlayStation : Console
{
public PlayStation()
{
// Associate new PlayStationController as default.
Controller = new PlayStationController();
}
}

That's all the setup complete, so now we have three basic types of entities: Controllers, consoles, and an adapter. Each controller is compatible with its matching console type (e.g. XBox console with XBoxController), but because we didn't want any way to directly access the Controller property for our Console class instances, the default controller assignment is handled during initialization of the XBox and PlayStation classes.

Here's our code to see this adapter pattern in action:

class Program
{
static void Main(string[] args)
{
// Create new XBox console.
var xbox = new XBox();
// Play with default controller.
xbox.Play();
// Create new PlayStationController.
var playStationController = new PlayStationController();
// Create controller adapter for PlayStation controller.
var adapter = new ControllerAdapter(playStationController);
// Play with adapted controller.
xbox.Play(adapter);

Logging.Log("-----------------");

// Create new PlayStation console.
var playstation = new PlayStation();
// Play with default controller.
playstation.Play();
// Create new XBoxController.
var xboxController = new XBoxController();
// Create controller adapter for XBox controller.
var adapter2 = new ControllerAdapter(xboxController);
// Play with adapted controller.
playstation.Play(adapter2);
}
}

We start by creating a new instance of our XBox console then call xbox.Play() to start playing using the default controller. These first two lines produce the following output, telling us we've plugged in the (default) XBoxController into our console and then we're using the XBoxController within our Play() method call:

Plugging XBoxController into XBox console.
Playing with XBoxController on XBox console.

However, we want to use a PlayStation controller instead, so we start by creating a new instance of PlayStationController. Now we need our adapter. We create a new instance of ControllerAdapter, which expects a Controller class to be passed as the only argument. Finally, we invoke the xbox.Play(ControllerAdapter adapter) method call by passing in our new adapter, which is set to use a PlayStation controller. As a result, our output shows that the adapter was used on the new PlayStationController instance, then it was plugged into our XBox console, before we finally were able to begin playing with it:

Using adapter on PlayStationController.
Plugging PlayStationController into XBox console.
Playing with PlayStationController on XBox console.

This is the crux of the entire adapter pattern: We were able to take a PlayStation controller and adapt it to be used with our XBox console. Moreover, we did so without any direct access to the Controller property of our XBox : Console class, and instead simply used a ControllerAdapter instance to perform the necessary compatibility changes behind the scenes.

Just to illustrate that this works both ways, the final few lines of code reverse everything to try using an XBoxController via an adapter with our PlayStation console:

Logging.Log("-----------------");

// Create new PlayStation console.
var playstation = new PlayStation();
// Play with default controller.
playstation.Play();
// Create new XBoxController.
var xboxController = new XBoxController();
// Create controller adapter for XBox controller.
var adapter2 = new ControllerAdapter(xboxController);
// Play with adapted controller.
playstation.Play(adapter2);

This works just fine illustrating that, once implemented, the adapter pattern can easily be expanded as much as necessary to meet your own project requirements:

-----------------
Plugging PlayStationController into PlayStation console.
Playing with PlayStationController on PlayStation console.
Using adapter on XBoxController.
Plugging XBoxController into PlayStation console.
Playing with XBoxController on PlayStation console.