Understanding and Implementing the Adapter Pattern in C# and C++

Understanding and Implementing the Adapter Pattern in C# and C++


In this article, we will explore the Adapter pattern, understand when it's useful, and demonstrate a rudimentary implementation of it using both C# and C++.

Background

Nearly six years ago, at the start of my career, one of my clients asked me to "write an adapter." At the time, I had no knowledge of the "Adapter Pattern," but I took the request literally and began my work. I completed the task, and later realized that the Adapter pattern was simple to understand and implement.

So, what exactly is an adapter? Imagine you have a source providing data in one format, but your target system expects it in another format. In this case, you can insert a module between the two components to perform the necessary conversion. The module performing this task is the adapter. I recall using an adapter to power my 16-bit gaming console (which required 9 volts) from a 220-volt power supply.


In the context of software, the Adapter pattern works similarly but applies to classes and objects. If a class exposes a set of functions that don’t match the interface expected by the client, an adapter can be placed in between. The advantage is that the client doesn't need to change every time the underlying object changes. You only need to write a new adapter, and the client code will work seamlessly.

What is the Adapter Pattern?

The "Gang of Four" (GoF) defines the Adapter pattern as follows: "Convert the interface of a class into another interface that the clients expect. The Adapter lets classes work together that couldn't otherwise due to incompatible interfaces."

Let’s break this down with a simple class diagram:


  • Target: Defines the interface that the client uses.
  • Adapter: Adapts the Adaptee’s interface and exposes the Target interface to the client.
  • Adaptee: The object whose interface needs to be adapted.
  • Client: Uses the Adaptee’s functionality via the Adapter interface (i.e., Target).

A good example of when the Adapter pattern is useful is in an MP3 player application. Imagine that the app is built using a library like DirectX for playback. Later, there is a need to switch to XNA. Rather than rewriting the entire application to replace DirectX-specific calls with XNA-specific ones, you can simply write an adapter for XNA. This way, the client code remains unchanged.

Code Implementation

Let’s now implement a simple application to demonstrate the Adapter pattern. We will create two dummy libraries that expose different interfaces and write an adapter to bridge them.

C# Implementation of Libraries (Adaptee):

// Library One
class LibraryOne
{
    public void ThisIsHowOneDoesIt()
    {
        Console.WriteLine("Using Library ONE to perform the action");
    }
}

// Library Two
class LibraryTwo
{
    public string ThisIsHowTwoDoesIt()
    {
        return "Using Library TWO to perform the action";
    }
}        

C++ Implementation of Libraries (Adaptee):

// LibraryOne
class LibraryOne
{
public:
    void ThisIsHowOneDoesIt()
    {
        std::cout << "Using Library ONE to perform the action\n";
    }
};

// LibraryTwo
class LibraryTwo
{
public:
    std::string ThisIsHowTwoDoesIt()
    {
        return "Using Library TWO to perform the action\n";
    }
};        

Adapter Interface (Target):

C#:

interface IAdapter
{
    void Do();        
}        

C++:

class IAdapter
{
public:
    virtual void Do() = 0;
};        

Concrete Adapter Classes:

C# Implementation of Adapters:

// Adapter for Library One
class AdapterOne : IAdapter
{
    private LibraryOne one;

    public AdapterOne()
    {
        one = new LibraryOne();
    }

    public void Do()
    {
        one.ThisIsHowOneDoesIt();
    }
}

// Adapter for Library Two
class AdapterTwo : IAdapter
{
    private LibraryTwo two;

    public AdapterTwo()
    {
        two = new LibraryTwo();
    }

    public void Do()
    {
        Console.WriteLine(two.ThisIsHowTwoDoesIt());
    }
}        

C++ Implementation of Adapters:

// Adapter for Library One
class AdapterOne : public IAdapter
{
public:
    void Do()
    {
        LibraryOne one;
        one.ThisIsHowOneDoesIt();
    }
};

// Adapter for Library Two
class AdapterTwo : public IAdapter
{
public:
    void Do()
    {
        LibraryTwo two;
        std::cout << two.ThisIsHowTwoDoesIt();
    }
};        

Now that we have our adapter interfaces and concrete adapters, let's see how the client can use the adapters to interact with both libraries.

C# Client Implementation:

static void Main(string[] args)
{
    IAdapter adapter = null;

    Console.WriteLine("Enter which library you want to use to perform the operation (1 or 2):");
    int x = Console.Read();

    if (x == '1')
    {
        // Use Library One
        adapter = new AdapterOne();
    }
    else if (x == '2')
    {
        // Use Library Two
        adapter = new AdapterTwo();
    }

    // Perform the operation
    adapter.Do();
}        

C++ Client Implementation:

int main()
{
    IAdapter *adapter = nullptr;

    std::cout << "Enter which library you want to use to perform the operation (1 or 2): ";
    int x;
    std::cin >> x;

    if (x == 1)
    {
        // Use Library One
        adapter = new AdapterOne();
    }
    else if (x == 2)
    {
        // Use Library Two
        adapter = new AdapterTwo();
    }

    // Perform the operation
    adapter->Do();

    delete adapter;
    return 0;
}        

In both cases, the client uses the same interface (IAdapter) to interact with different libraries. This is the core benefit of the Adapter pattern: it enables code to work with classes that have incompatible interfaces by introducing an adapter that ensures the client can continue to work seamlessly without changing its code.


Conclusion

The Adapter pattern is a simple yet powerful design pattern that is incredibly useful when working with different interfaces. Instead of modifying the client code or the underlying objects, an adapter allows you to create a consistent interface for the client while adapting the implementation of the underlying systems. The .NET Framework uses this pattern internally, particularly when interacting with legacy COM components.

By using the Adapter pattern, you ensure that your code remains flexible and maintainable, even as the underlying systems change. Whether you're working in C# or C++, this pattern can save you time and effort in the long run.

#AdapterPattern #CSharp #CppProgramming #DesignPatterns

要查看或添加评论,请登录

Rahul Singh的更多文章

社区洞察

其他会员也浏览了