The Adapter Pattern

This Adapter pattern implementation allows for more flexibility than an equivalent, purely object-oriented implementation.


May 08, 2007
URL:http://www.drdobbs.com/architecture-and-design/the-adapter-pattern/199204099

Ronald is a Dutch software analyst who principally works with C++. He is currently the chief software architect for Optel Vision in Canada and can be reached at www.landheer-cieslak.com.


One of the appplications I work on was designed according to a Latino-Communist design pattern such as this: If any class A knows any other class B, it not only knows B, but also all its family, friends, and acquaintances and not only does it know them, but it knows their family, friends and acquaintances, and not only that, but it knows their internals and has a right -- nay, an obligation -- to play with those internals.

One of the problems with such an "approach" is that if there's a bug, you don't really know what's going on or what class is responsible: If you have four classes, Everybody, Somebody, Anybody, and Nobody, if Somebody has a bug, it could be Anybody 's fault but Nobody really knows, while Everybody shares responsibility.

Another problem is that when you have to add something to such an application, and you don't want to get your hands too dirty and you want to be able to unit-test your addition, you really have no choice but trying to hide a chopstick in a bowl of spaghetti. This is exactly the scenario I found myself in when I came up with the idea of the specific type of adapter I discuss here.

The Problem

Your mission, should you choose to accept it, is to take the core functionality of the application descibed above, encapsulate it in a single class we'll call Task, and construct an instance of that class. For this, you need to know three types, each of which is currently a template parameter of another class that you will partially replace, and which is known by the entire planet, where the planet in question is the application. Each of these types is different but related to each other type. You cannot simply take the definition of one particular type, rip it out of the application, and put it in your test case -- but you do have to write a test case.

For your class Task to work, you need a batch of data to work on, which you will visit with a visitor [1].

From the existing application, you can determine that the different types that can provide batches of data all have a function that can take a visitor as parameter, but those functions have different names, though they have similar signatures (most of them return void and take a reference to the visitor type as parameter, but the visitor type changes with each data batch type).

Visitors in the application all share the same signature and are derived from a common base class. That base class, however, is highly coupled with the entire rest of the application, which makes extracting it to use it in your test cases next to impossible. Also, you cannot expect the caller to provide you with an instance of the visitor, but it can provide you with its type.

You do not want the entire implementation of your Task class to be exposed, which means that though the code needs to be generic, you cannot exclusively rely on compile-time binding of your client's types in your implementation: Such approaches have already led to dramatic losses of productivity in the team that developed the application in the first place, and you don't want to add to that problem while solving another one.

The Solution: A Mixture of Compile-time and Run-time Programming

First, you need a way to pass a type without passing an instance of the type, and you need a way to store that type and be able to use it afterwards. You also need a way to encapsulate an interface contract by defining only the signatures of the functions you will call and making them depend on the types that will be passed to you.

Passing Types Without Passing Instances

In the different libraries you have at your disposal, there are two that are very interesting for passing types around: loki, which contains a Type2Type class and Boost.MPL, which contains boost::mpl::identity. The latter suits our purposes a bit better because we'll be using the Boost MPL later on, and Boost.MPL has a coherent way of presenting meta-functions [2].

So, to pass a type to a function foo without passing an object of that type, the following code will work just fine:

#include <boost/mpl/identity.hpp>
template < typename Type >
void foo(const boost::mpl::identity< Type > &)
{
	/* do something with Type */
}
int main()
{
	foo(boost::mpl::identity< int >());
	return 0;
}

Here, main passes the type int to the function foo. The nice thing about this method is that no object is actually created: The compiler will use the boost::mpl::identity instance only to determine the type to use, but will optimize-away its instance (usually even in the lowest optimization settings[3]).

Determining and Implementing a Common Interface and Interface Contract

Most of our data-batch types implement a function that accepts a visitor, takes a reference to it as a parameter, and returns void. The type of the data batch and of the visitor change, as do the names of the functions that accept those visitors, but the "signature" described above is a common denominator that we can translate into code:

void (DataBatchType::*accept_visitor_)(VisitorType &)

is a pointer to a function in a class called DataBatchType that takes a reference to an instance of something of type VisitorType. The pointer itself is called accept_visitor_[4].

This means that if we have both types, we can, in theory, create a pointer to something that we can call with a visitor as a parameter. One problem is, though, that some data-batch types don't implement quite the same signature; for example, one of the data-batch types we have to support looks like this:

struct DataBatch
{
	Visitor & visit(Visitor & visitor)
	{
		/* do something with the visitor */
		return visitor;
	}
};

A straightforward approach to a solution for this problem would be to create a type trait for the data-batch type, as follows:

template <>
struct DataBatchTraits< DataBatch >
{
	static void acceptVisitor(DataBatch * data_batch, Visitor & visitor)
	{
		data_batch->visit(visitor);
	}
};

which is one way of implementing compile-time polymorphism: Conceptually, trait classes are much like compile-time interfaces to a given class. Though you can't create an instance of a trait class without knowing the complete type of the class that implements its interface, you don't have to know that while writing the code that uses the interface, as long as you've determined the interface you need when writing that code. We've determined that we needed something that returned void and takes a reference to a visitor as parameter, so our traits class will provide just that, while wrapping the actual implementation, which only the compiler needs to know at the right time.

This means that once we've determined a common interface for all data-batch types, we retain a certain flexibility on the data-batch implementation side (which we would not have if we had a common base class [5].This is especially useful as we're integrating into exisiting code which painfully lacks coherency but is also painfully cohesive.

However, just using a trait is not quite enough: We need to remember that the Task class' implementation will not know the type of the data-batch, and will therefore not be able to create an instance of a trait class, specialized for the type of the data-batch. We therefore need to return to run-time polymorphism and implement an Adapter class that exposes the common interface the traits will also expose. That Adapter class will look like this:

struct DataBatchAdapter
{
	virtual ~DataBatchAdapter();
	virtual void visit() = 0;
};

To use this class, a derived class will need to be implemented that uses the trait class discussed above. Once we've come to this conclusion, the solution is self-evident:

namespace Details
{
	template < typename DataBatchType, typename VisitorType >
	struct DataBatchAdapter : public ::DataBatchAdapter
	{
		DataBatchAdapter(DataBatchType * data_batch)
			: data_batch_(data_batch)
		{ /* no-op */ }
		virtual void visit()
		{
			VisitorType visitor(VisitorTraits< VisitorType >::create(data_batch_));
			DataBatchTraits< DataBatchType >::acceptVisitor(data_batch_, visitor);
		}
		DataBatchType * data_batch_;
	};
}

The following code will, therefore, create an instance of the adapter and call the visit function on it:

DataBatch data_batch;
std::auto_ptr< DataBatchAdapter > adapter(new Details::DataBatchAdapter< DataBatch, Visitor >(&data_batch));
adapter->visit();

Note, by the way, that this code explicitly uses a pointer in order to keep the run-time polymorphism possible.

Storing a Type for Later Use

As I've already noted, the result of the typeid operator cannot be used for a dynamic_cast; for instance, the result of the typeid operator cannot be used for much of anything. It is therefore typically useless, and is certainly useless in the context we're working in here.

In the context of the Munchausen Pattern, the solution involved a type-list, which was possible because the types to be supported in the the Munchausen Pattern are known aforehand. That is not the case here, which means a type-list is not really an option.

While the type of the visitor is partially needed in the context of the adapter introduced above, it would be a very meager design choice to use an adapter For instance, our Task class will need to use the visitor independantly from the data-batch to extract information from it and, eventually, but outside the scope of this article, pass that information to another object. The most straightforward way to do this is, once more, to use an adapter which will, in this case, not store a pointer to an object of type VisitorType.

A skeleton of such a visitor adapter would look like this:

struct VisitorAdapter
{
	virtual ~VisitorAdapter();
};

namespace Details
{
	template < typename VisitorType >
	struct VisitorAdapter : public ::VisitorAdapter
	{
	};
}

to which the necessary functions can be added as needed.

An In-depth Look at this Adapter Pattern

The original Adapter pattern[6] is summarized, by its authors, as follows:

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces.

The implementation as proposed in its original form is quite different from what I present here, however, in that in its original form, the adapter is derived from the adaptee and exposes an interface defined for the adapter, while calling functions on the adaptee accordingly. This means that common implementations of the Adapter pattern impose writing a new, independant, class for each class to be adapted and, worse in our case, tightly coupling that class with the class to be adapted[7].

The Adapter pattern I present here is the same pattern and serves the same purpose, but the implementation as presented here requires only the unification of the interface as a set of traits classes (which could be very much smaller than the number of classes to adapt) and a single, templatized, derived class which is specialized with the type to adapt.

The Role and Implementation of the Traits Classes

As used here, the traits classes define a unified interface for both the visitors and the data-batches, and adapt to the interfaces of the existing classes. As such, they are the "adapter" classes at a compile-time level.

As stated above, it is possible to re-group traits classes according to certain traits of each class. For instance, the traits class itself can be derived from a variable set of classes that is determined by the actual traits of each class. For example, if some of our data-batch classes have a visit function that takes a pointer to the visitor, rather than a reference, that could be implemented as follows:

template  < typename T >
struct accepts_pointer : boost::false_type
{};
namespace Details
{
	template < typename DataBatchType, typename VisitorType, typename AcceptsPointer >
	struct DataBatchTraits_visit
	{
		static void visit(DataBatchType * data_batch)
		{
			VisitorType visitor(VisitorTraits< VisitorType >::create());
			data_batch->acceptVisitor(visitor);
		}
	};

	template < typename DataBatchType, typename VisitorType >
	struct DataBatchTraits_visit< DataBatchType, VisitorType, boost::true_type >
	{
		static void visit(DataBatchType * data_batch)
		{
			VisitorType visitor(VisitorTraits< VisitorType >::create());
			data_batch->acceptVisitor(&visitor);
		}
	};
}

template < typename DataBatchType, typename VisitorType >
struct DataBatchTraits : Details::DataBatchTraits_visit< DataBatchType, VisitorType, typename accepts_pointer< DataBatchType >::type >
{
};

in which case the meta-function accepts_pointer determines which version of DataBatchTraits_visit is used. By default, accepts_pointer "returns" false (or rather: boost::false_type), but a specialization of accepts_pointer like this:

template <>
struct accepts_pointer< DataBatchType2 > : boost::true_type
{};

indicating that DataBatchType2 implements an acceptVisitor function that takes a pointer to the visitor rather than a reference, would change the traits for DataBatchType2 accordingly. Hence, only one class of traits is necessary, which derives from any number of little traits classes according to any number of compile-time flags, implemented as compile-time meta-functions.

The Role and Implementation of the Adapter's Base Class

While the traits classes implement a compile-time uniformization of the interface, you can't store an instance of a traits class without knowing the type it is a trait of. This is, in a sense, the same problem as briefly described above, but it elucidates the role of the adapter's base class rather clearly: The adapter's base class exposes the unified interface of the adaptees as a set of abstract functions, which are to be implemented by derived classes which, in turn, are generated with the types to be adapted.

The adapter's base class can, however, also serve a second and perhaps more important role: The role of defining a unified interface contract for all adaptees: by defining a set of pre-and post-conditions which are check directly by the base-class, the base class can make sure that the adaptees really behave as expected and become a very powerful debugging tool once you have to integrate your solution in the final application [8].

Let's take a look at an over-simplified adapter class:

class Adapter
{
public :
	virtual ~Adapter();
	virtual int get25() const;

protected :
	virtual int get25_() const = 0;
};

The implementation of the base class' get2 function simply calls get25_ and checks its post-conditions:

int Adapter::get25() const
{
	int retval(get25_());
	assert(retval == 25);	// post-condition: we expect get25 to return 25
	return retval;
}

This enforces a contract that we'll assume to be established: We expect get25 to return 25. This implementation documents that assumption and, if not compiled with NDEBUG, enforces it with a run-time assertion.

As we can't expect derived classes to always be specializations of our own, generic, implementation, we can't implement this check in the derived class and hope it will always be used, so we don't. Hence, our derived class looks like this:

namespace Details
{
	template < typename AdapteeType >
	class Adapter : public ::Adapter
	{
	public :
		Adapter(AdapteeType * adaptee);
	protected :
		int get25_() const;

	private :
		AdapteeType * adaptee_;
	};

	template < typename AdapteeType >
	Adapter< AdapteeType >::Adapter(AdapteeType * adaptee)
		: adaptee_(adaptee)
	{ /* no-op */ }

	template < typename AdapteeType >
	int Adapter< AdapteeType >::get25_() const
	{
		return AdapteeTraits< AdapteeType >::get25(adaptee_);
	}
}

If we now take a look at the rest of the implementation of a little test case (from which the code above was taken), we can see that this approach really does work. Here's the traits class:

namespace Details
{
	template < typename AdapteeType >
	struct AdapteeTraits
	{
		static int get25(const AdapteeType * adaptee)
		{
			return adaptee->get25();
		}
	};
}

and the adaptee:

struct Adaptee
{
	int get25() const
	{
		return 26;
	}
};

which means the following code will fail, as expected:

int main()
{
	Adaptee adaptee;
	std::auto_ptr>l Adapter > adapter(new Details::Adapter< Adaptee >(&adaptee));
	return adapter->get25();
}

Conclusion

The Adapter pattern implementation I present here allows for far more flexibility than an equivalent, purely object-oriented implementation. With the proper use of compile-time meta-functions that describe certain traits of adapted types, traits classes to encapsulate those traits, an Adapter base class that defines and enforces an interface with an interface contract and derived classes generated according to the adapted types, we can successfully integrate unit-testable software in an application that has become too cohesive to unit-test, and is not coherent enough to allow for "normal" assumptions about the functioning of the code to be made.

Within the context this solution was found in, the solution is superior to both the object-oriented approach to the Adapter pattern and to a C++ Veneer implementation, which has some, but not all, of the advantages of the solution presented here.

The Task class, described above, is now a simple matter of writing a few of these adapters and the glue to make them stick together.

Notes

[1] The problem, as presented here, is a bit simplified: The actual problem I had to solve had four objects and three types which needed to be passed to the task, each of which could have different types.

[2] See C++ Template Metaprogramming: Concepts, Tools, and Techniques from Boost and Beyond by David Abrahams and Aleksey Gurtovoy for more information.

[3] Both GCC-3.4.4 and MSVC8 perform this optimization.

[4] This is basically the way virtual function calls work, so a way of implementing an abstract interface would be doing the same thing the compiler does: creating a table of "virtual" functions and calling them according to the one that was asked for. Due to C++'s type-safety and a host of conceptual design problems, this is not the way we'll go, though.

[5] For instance, when using a common base class to determine an interface, all derived classes have to implement the base class' interface exactly as presented by the base class, whereas when using a traits class, the exact implementation and signature of the implementation is left to the implementer, and the traits class itself can simply serve as glue-code.

[6] Design Patterns: Elements of Reusable Object-Oriented Software, by Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides.

[7] A Veneer class is much like a classic Adapter in the sense that it also derives from the adaptee and implements a unified interface, and also has the advantage of the Adapter pattern as implemented here. The major difference, though, is that to use Veneers efficiently, you still have to know aforehand what you will be adapting, or fall back to using traits in the same way we d here. Using traits within a Veneer gets a bit more complicated, though, as you're no longer working on a separate class instance. Also, the adapters presented here have a base class that is not a template class and can therefore be stored without knowing the type of the adaptee -- something that can only be accomplished with Veneers when using multiple inheritance, which is something we would like to avoid for now. Veneers are still very interesting in other cases, though, and should definitely not be thrown out of your toolbox because you have this shiny new tool to work with!

[8] As you'll remember, the final application is a bowl of spaghetti of which you can't really expect any coherent behavior. Implementing and enforcing an interface contract top check whether what assumptions you have about its behavior is really what actually happens can save you a lot of trouble in the long run, as it will help pinpoint the location of any bugs within the application, rather than within your adapter.

The Source Code

The source code of every example above is available from from Dr. Dobb's andfrom me.

Only an MSVC8 solution file with its set of project files is provided for compilation, but it should be pretty straightforward to make a little Make file to compile on some other system, and I'll be happy to provide a GNU Makefile for such a purpose.

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.