Jump to content

Adapter

From Knowledge Base

Adapter

The Adapter Pattern (Structural) allows the interface of an existing class to be used as another interface. It is often used to make existing classes work with others without modifying their source code.

The primary purpose of the Adapter Pattern is to achieve compatibility between different interfaces. It is particularly useful when integrating new features or systems with legacy code, as it allows communication between the new and old system components without the need to refactor or rewrite existing code.

Benefits

  • Interoperability: It allows two unrelated or incompatible interfaces to work together.
  • Reusability: Existing classes can be reused even if their interfaces do not match the ones needed for a particular task.
  • Decoupling: It helps in decoupling the client classes from the classes of the system being adapted. This means changes to the system classes do not directly impact the client code.

Use Cases

  • Legacy Integration: When new applications need to be integrated with legacy systems.
  • Third-party Libraries: When using third-party libraries or APIs that have different interface conventions.
  • Multiple Sources: When the application needs to interact with multiple sources having different interfaces.

How It Works

The Adapter Pattern involves a client, a target interface, an adapter, and an adaptee:

  • Client: The client class contains the current business logic of the application.
  • Target Interface: This is the expected interface the client wants to use.
  • Adapter: The adapter class implements the target interface and contains a reference to an object of the adaptee class.
  • Adaptee: An existing class that needs adapting to fit the target interface.

The client uses the adapter by calling methods on the target interface. The adapter translates these calls to calls on the adaptee's interface, which does the actual work.

Implementation Example

Adaptee Classes

Create the Data Entity and Repository classes. These operations are specific to the SQLite database as indicated by the use of SQLiteConnection (requires the `System.Data.SQLite` package, which can be installed via NuGet).

Data Entity class

namespace AdapterPattern {
    public class Data {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public Data(Guid id, string name) {
            Id = id;
            Name = name;
        }
    }
}

Repository class

using System.Data.SQLite;

namespace AdapterPattern {
    public class Repository : IRepository {
        private string connectionString;
        public Repository(string dbFilePath) {
            connectionString = $"Data Source={dbFilePath};Version=3;";
            CreateMyTable();
        }

        public void InsertData(Data myData) {
            using (SQLiteConnection connection = new SQLiteConnection(connectionString)) {
                connection.Open();
                using (SQLiteCommand command = new SQLiteCommand(connection)) {
                    command.CommandText = "INSERT INTO MyTable (Id, Name) VALUES (@id, @name)";
                    command.Parameters.AddWithValue("@id", myData.Id.ToString());
                    command.Parameters.AddWithValue("@name", myData.Name);
                    command.ExecuteNonQuery();
                }
            }
        }

        public List<Data> GetAllData() {
            List<Data> data = new List<Data>();
            using (SQLiteConnection connection = new SQLiteConnection(connectionString)) {
                connection.Open();
                using (SQLiteCommand command = new SQLiteCommand("SELECT Id, Name FROM MyTable", connection)) {
                    using (SQLiteDataReader reader = command.ExecuteReader()) {
                        while (reader.Read()) {
                            data.Add(new Data(
                                Guid.Parse(reader["Id"].ToString()),
                                reader["Name"].ToString()
                            ));
                        }
                    }
                }
            }
            return data;
        }
        
        private void CreateMyTable() {
            using (SQLiteConnection connection = new SQLiteConnection(connectionString)) {
                connection.Open();
                using (SQLiteCommand command = new SQLiteCommand(connection)) {
                    command.CommandText = "CREATE TABLE IF NOT EXISTS MyTable (Id TEXT PRIMARY KEY, Name TEXT)";
                    command.ExecuteNonQuery();
                }
            }
        }
    }
}

Target Interface

Next we create the target interface, that defines a set of operations that the client code expects to use. The adapter implements this interface and acts as an intermediary, allowing the client code to work with a class that doesn’t originally adhere to the interface. It “adapts” the non-compatible class to the expected interface, enabling the client to use it seamlessly. The IRepository interface represents the operations that the application needs to perform.

namespace AdapterPattern {   
    public interface IExternalDataSource {
        List<Data> FetchExternalData();
    }
}

Client/External Application

We introduce a new class that requires adaptation.

namespace AdapterPattern {
    public class ExternalService {
        private IExternalDataSource _externalDataSource;

        public ExternalService(IExternalDataSource externalDataSource) {
            _externalDataSource = externalDataSource;
        }

        public void ProcessExternalData() {
            var externalData = _externalDataSource.FetchExternalData();
            Console.WriteLine("Processing external data:");
            foreach (var data in externalData) {
                Console.WriteLine($"External Service Processing: {data.Name}");
            }
        }
    }
}

Adapter Class

The adapter class will implement the IExternalDataSource interface and wrap the Repository class. It will translate the IRepository operations into operations that the Repository class can understand.

namespace AdapterPattern {
    class ExternalServiceAdapter : IExternalDataSource {
        private Repository _repository;

        public ExternalServiceAdapter(string dbFilePath) {
            _repository = new Repository(dbFilePath);
        }

        public List<Data> FetchExternalData() {
            return _repository.GetAllData();
        }
    }
}

Usage

The code below demonstrates the Adapter Pattern’s role in enabling the Repository class to work with the ExternalService class, which relies on an interface, IExternalDataSource, for data integration.

namespace AdapterPattern {
    internal class Program {
        static void Main(string[] args) {
            string dbFilePath = "adapter_example_database";

            IRepository dataRepository = new Repository(dbFilePath);
            dataRepository.InsertData(new Data(Guid.NewGuid(), "Example Data 1"));
            dataRepository.InsertData(new Data(Guid.NewGuid(), "Example Data 2"));

            IExternalDataSource externalDataSource = new ExternalServiceAdapter(dbFilePath);
            var externalService = new ExternalService(externalDataSource);
            externalService.ProcessExternalData();
        }
    }
}

Result:

Processing external data:
External Service Processing: Example Data 1
External Service Processing: Example Data 2

Class Diagram

Practical Usage in C#

In C# development within the .NET ecosystem, the Adapter pattern is instrumental in enabling communication between systems and components that would otherwise be incompatible:

- Integrating Legacy Systems: As systems evolve, especially with the migration from Full .NET Framework to .NET Standard and .NET 6+ for Linux compatibility, the Adapter pattern can be used to ensure that new components can work with legacy systems without altering the existing codebase.

- Facilitating Communication Between Services: In a microservices architecture, where services communicate over HTTP REST-based APIs or via a service bus, the Adapter pattern can be used to adapt the data exchange between different services, ensuring that they can interact seamlessly despite having different data formats or protocols.