DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,673 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for Adapter Pattern in C#
Kostas Kalafatis
Kostas Kalafatis

Posted on

Adapter Pattern in C#

The Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate.

The Adapter attempts to reconcile the differences between two otherwise incompatible interfaces or classes. In doing so, it wraps one of the classes in a layer that allows it to talk to the other.

You can find the example code of this post, on GitHub

Conceptualizing the Problem

Imagine that we're creating a stock market monitoring app. The app downloads the financial data from multiple endpoints in XML format processes them, and then displays them in pretty-looking charts and diagrams to the end user.

At some point, we decided to improve the app by integrating a 3rd-party visualization library. But there's a catch: the visualization library only works with data in JSON format.

How a project fails without adapter, because there are incompatible interfaces

We could change the library to work with XML. However, this might break some existing code that relies on the library. Moreover, while the library is open-source, getting a copy and changing the library's code to support XML is an exercise in futility (I have seen this practice more than once though...)

What we can do, is to create an adapter. An adapter is a special object that converts the interface of one object so that another object can communicate with it.

The adapter wraps one of the objects to hide the complexity of conversion happening under the hood. The wrapped object isn't even aware of the adapter. For example, we can wrap an object that operates in grams and celsius degrees with an adapter that converts all of the data to units such as pounds and Fahrenheit degrees.

Adapters can not only convert data into various formats but can also help objects with different interfaces collaborate.

Let's get back to our stock market app. To solve the problem of incompatible formats, we can create an XML-to-JSON adapter for every class of the analytics library that our code works with directly. Then we have to adjust our code to communicate with the library only via these adapters. When an adapter receives a call, it translates the incoming XML data into a JSON object and passes the call to the appropriate methods of a wrapped analytics object.

Adding an adapter to the current process allows communications between components

Structuring the Adapter Pattern

The adapter pattern comes in two flavours: the Object adapter and the Class adapter.

Object adapter

This implementation of the adapter pattern uses the object composition principle: the adapter implements the interface on one object and wraps the other.

In this implementation, the Object Adapter has four participants:

  • Client: The Client is a class containing the program's existing business logic. The client code doesn't get coupled to the concrete adapter class as long as it works with the adapter via the client interface. Thanks to this, we can introduce new types of adapters into the program without breaking the existing client code. This can be used when the interface of the service class gets changed or replaced. We can just create a new adapter class without changing the client code.
  • Client Interface: The Client Interface describes the protocol that other classes must follow to collaborate with the client code.
  • Service: The Service is some class (usually 3rd-party or legacy). The client can't use this class directly because it has an incompatible interface.
  • Adapter: The Adapter is a class that works with both the client and the service: it implements the client interface while wrapping the service object. The adapter receives calls from the client via the adapter interface and translates them into calls to the wrapped service object in a format it can understand.

Class diagram of the object adapter

Class adapter

This implementation uses inheritance: the adapter inherits interfaces from both objects at the same time. Note that this approach can only be implemented in languages that support multiple inheritance.

In this implementation, the Class Adapter doesn't wrap any object. It inherits the behaviours from both the client and the service, and the adaptation happens within the overridden methods. The resulting adapter can be used in place of an existing client class.

Class diagram of the class adapter

To demonstrate how the Adapter pattern works, we will create a meat-safe-cooking temperature database.

For this exampe, we have an old, legacy system which stores the temperature data. This legacy system will be represented by the MeatsDatabase and will be our Service participant. Such a system might look like the following:

namespace Adapter.Legacy
{
    public enum TemperatureType
    {
        Fahrenheit,
        Celsius
    }

    /// <summary>
    /// The legacy API must be converted to the new structure
    /// </summary>
    public class MeatsDatabase
    {
        public float GetSafeCookingTemperature(string meat)
        {
            return meat.ToLower() switch
            {
                "beef" or "pork" => 145f,
                "chicken" or "turkey" => 165f,
                _ => 165f,
            };
        }

        public int GetCaloriesPerOunce(string meat)
        {
            return meat.ToLower() switch
            {
                "beef" => 71,
                "pork" => 69,
                "chicken" => 66,
                "turkey" => 38,
                _ => 0,
            };
        }

        public double GetProteinPerOunce(string meat)
        {
            return meat.ToLower() switch
            {
                "beef" => 7.33f,
                "pork" => 7.67f,
                "chicken" => 8.57f,
                "turkey" => 8.5f,
                _ => 0f,
            };
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Now let's create a Meats class

namespace Adapter
{
    /// <summary>
    /// The new Meats class, which represents details
    /// about a particular kind of meat.
    /// </summary>
    public class Meats
    {
        protected string MeatName;
        protected double SafeCookingTemperatureFahrenheit;
        protected double SafeCookingTemperatureCelsius;
        protected double CaloriesPerOunce;
        protected double CaloriesPerGram;
        protected double ProteinPerOunce;
        protected double ProteinPerGram;

        public Meats(string meatName) 
        { 
            this.MeatName = meatName;
        }

        public virtual void LoadData()
        {
            Console.WriteLine($"\nMeat: {MeatName} ------");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The problem is that we cannot modify the legacy API, which is the MeatDatabase class. Here's where our Adapter participant comes into play: we need another class that inherits from Meat but maintains a reference to the API such that the API's data can be loaded into an instance of the Meat class:

using Adapter.Legacy;

namespace Adapter
{
    /// <summary>
    /// The Adapter class, which wraps the Meats class and
    /// initializes that class's values.
    /// </summary>
    public class MeatDetails : Meats
    {
        private MeatsDatabase meatsDatabase;

        public MeatDetails(string name) : base(name)
        {
        }

        public override void LoadData()
        {
            meatsDatabase = new MeatsDatabase();
            SafeCookingTemperatureFahrenheit = meatsDatabase.GetSafeCookingTemperature(MeatName);
            SafeCookingTemperatureCelsius = FahrenheitToCelsius(SafeCookingTemperatureFahrenheit);
            CaloriesPerOunce = meatsDatabase.GetCaloriesPerOunce(MeatName);
            CaloriesPerGram = PoundsToGrams(CaloriesPerOunce);
            ProteinPerOunce = meatsDatabase.GetProteinPerOunce(MeatName);
            ProteinPerGram = PoundsToGrams(ProteinPerOunce);

            base.LoadData();
            Console.WriteLine($" Safe Cooking Temperature (Fahrenheit): {SafeCookingTemperatureFahrenheit}");
            Console.WriteLine($" Safe Cooking Temperature (Celcius): {SafeCookingTemperatureCelsius}");
            Console.WriteLine($" Calories per Ounce: {CaloriesPerOunce}");
            Console.WriteLine($" Calories per Gram: {CaloriesPerGram}");
            Console.WriteLine($" Protein per Ounce: {ProteinPerOunce}");
            Console.WriteLine($" Protein per Gram: {ProteinPerGram}");
        }

        private double FahrenheitToCelsius(double fahrenheit)
        {
            return (fahrenheit - 32) * 0.55555;
        }

        private double PoundsToGrams(double pounds)
        {
            return pounds * 0.0283 / 1000;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally, in our Main() method, we can now show the difference between using the legacy class by itself and using the Adapter class:

using Adapter;

// Non-adapted
Meats unknown = new Meats("Beef");
unknown.LoadData();

// Adapted
MeatDetails beef = new MeatDetails("Beef");
beef.LoadData();

MeatDetails chicken = new MeatDetails("Chicken");
chicken.LoadData();

MeatDetails turkey = new MeatDetails("Turkey");
turkey.LoadData();
Enter fullscreen mode Exit fullscreen mode

The output of our application will be the following:

Output of the adapter demo

Pros and Cons of Adapter Pattern

βœ” We can separate the interface or data conversion code from the primary business logic of the program, thus satisfying the Single Responsibility Principle ❌ The overall complexity of the code increases because we need to introduce a set of new interfaces and classes. Sometimes it’s simpler just to change the service class so that it matches the rest of your code.
βœ” We can introduce new types of adapters into the program without breaking the existing client code, as long as they work with the adapters through the client interface, thus satisfying the Open/Closed Principle

Relations with Other Patterns

  • The Bridge is usually designed up-front, letting us develop parts of an application independently of each other. On the other hand, rhe Adapter is commonly used with an existing application to make some otherwise-incompatible classes work together nicely.
  • The Adapter changes the interface of an existing object, while the Decorator enhances an object without changing its interface. In addition, rhe Decorator supports recursive composition, which isn't possible when we use the Adapter.
  • The Adapter provides a different interface to the wrapped object, the Proxy provides it with the same interface, and the Decorator provides it with an enhanced interface.
  • Bridge, State, Strategy and to some degree Adapter have very similar structures. Indeed, all of these patterns are based on composition, which is delegating work to other objects. However, they all solve different problems. A pattern isn't just a recipe for structuring our code in a specific way.

Final Thoughts

In this article, we have discussed what is the Adapter pattern, when to use it and what are the pros and cons of using this design pattern. We then examined what is a class adapter and what is an object adapter and how the Adapter pattern relates to other classic design patterns.

It's worth noting that the Adapter pattern, along with the rest of the design patterns presented by the Gang of Four, is not a panacea or a be-all-end-all solution when designing an application. Once again it's up to the engineers to consider when to use a specific pattern. After all these patterns are useful when used as a precision tool, not a sledgehammer.

Top comments (0)

🌚 Browsing with dark mode makes you a better developer.

It's a scientific fact.