Design Patterns: The Adapter Pattern

Adapting Dependencies to an Interface

The Adapter Pattern is one of the fundamental building blocks of software engineering. It allows two pieces of software to interact by adapting each of their public interfaces to the other’s. Used properly, this serves to keeps those two pieces of code independent and decoupled, which in turn promotes high code cohesion.

This is the first in a series on software design patterns. There’s a reason why I chose to start with this particular pattern: its simplicity and utility lend it not only to many different software implementation scenarios, but also to many different architectural paradigms.

The basic definition of the adapter pattern is that it allows two incompatible interfaces to work together. That is, it converts one or both of the interfaces in a particular code relationship to suit the needs of the other piece.

To understand this better, let’s take a look at an example. Let’s start with a basic service interface required by almost any web application: IUserRepository.

public interface IUserRepository
{
    Task<User> GetUserAsync(string username);
    Task CreateUserAsync(User newUser);
    Task UpdateUserAsync(User user);
    Task DeleteUserAsync(User user);
}

This interface has the responsibility of managing persisted user information in some backing data store. It’s a very simple interface that can be used in multiple areas of an application.

Perhaps the architects of this application have chosen to user MongoDB as the backing data store. The official MongoDB C# driver exposes a class called MongoCollection<T> as the primary mechanism for communicating directly with a MongoDB collection. As you can see from the API listing in the link, MongoCollection does not conform to the IUserRepository interface. Instead, it exposes a host of methods for interacting with MongoDB using its specific protocol.

But all is not lost! One can still use MongoCollection<T> to implement the requirements of IUserRepository. It just requires the implementation to delegate functionality:

public class MongoDBUserRepository : IUserRepository
{
    private readonly IMongoCollection<User> _userCollection;

    public MongoDBUserRepository(MongoDatabase mongoDatabase)
    {
        _userCollection = mongoDatabase.GetCollection<User>("users");
    }

    public async Task<User> GetUserAsync(string username)
        => await _userCollection
            .AsQueryable<User>()
            .SingleOrDefaultAsync(user => user.Username == username);

    public async Task CreateUserAsync(User newUser)
        => await _userCollection
            .InsertOneAsync(newUser, null, null);

    public async Task UpdateUserAsync(User user)
        => await _userCollection
            .ReplaceOneAsync(
                candidate => candidate.Username == user.Username,
                user,
                null,
                null);

    public async Task DeleteUserAsync(User user)
        => await _userCollection
            .DeleteOnAsync(
                candidate => candidate.Username == user.Username,
                null);
}

By delegating to the underlying IMongoCollection instance, the MongoDBUserRepository becomes an object adapter, delegating calls to the IUserRepository interface to specific methods on the IMongoCollection interface.

If inheritance is preferred, a class adapter can be created instead:

public class MongoDBUserRepository : MongoCollection, IUserRepository
{
    private readonly IMongoCollection<User> _userCollection;

    public MongoDBUserRepository(
        MongoDatabase database,
        string name,
        MongoCollectionSettings settings)
        : base(database, name, settings)
    {
    }

    public async Task<User> GetUserAsync(string username)
        => await this
            .AsQueryable<User>()
            .SingleOrDefaultAsync(user => user.Username == username);

    public async Task CreateUserAsync(User newUser)
        => await InsertOneAsync(newUser, null, null);

    public async Task UpdateUserAsync(User user)
        => await ReplaceOneAsync(
            candidate => candidate.Username == user.Username,
            user,
            null,
            null);

    public async Task DeleteUserAsync(User user)
        => await DeleteOnAsync(
            candidate => candidate.Username == user.Username,
            null);
}

Note here that instead of delegating to calls on an internal instance member variable, each method is just calling another method on this. Sometimes, inheritance may be preferred over encapsulation, but as one can see from the constructor this can make some aspects of code harder to write and maintain. It also produces an intermingled API on the adapter class, which some might view as “polluted”.

The power of this pattern comes in the fact that operations are delegated through an interface declared by the application to functions on an external dependency. This means that if the underlying data store is changed – say, to Couchbase or SQL – then the only thing that has to change is the implementation of the IUserRepository adapter for the new database. Any code using IUserRepository can be left unchanged, whereas any code directly using IMongoCollection would have to be refactored to use an appropriate client for the new database. This decouples the application from the particulars of its data store, allowing the two to vary independently.

Conclusion

The Adapter Pattern is a powerful, simple tool that can be used to make two different implementations and designs work together with minimal overhead. It can also decouple an application from the specific technologies required to implement its dependencies, allowing the dependencies to be readily swapped out with minimal impact on the rest of the application logic. Because of this, the Adapter Pattern is a critical component of Hexagonal Architecture.