The Adapter Pattern is a structural design pattern that enables incompatible interfaces to work together. Think of it as a translator that adapts the interface of one class to match what another class expects. This pattern is especially useful when integrating systems or libraries with different interfaces without modifying the original code.
Core Idea
Imagine you have a client expecting data in a specific format or using certain methods, but the data source you want to use has a different format or method name. Rather than changing the client or the data source, you create an adapter that "translates" between them.
Key Components
- Target: The interface that the client expects to work with.
- Adaptee: The existing class that has a different or incompatible interface.
- Adapter: A wrapper that implements the Target interface, and internally calls the Adaptee’s methods, translating data if needed.
How It Works
- The Client: Makes a call to the Target interface expecting data in a certain format.
- The Adapter: Implements the Target interface and, when called by the Client, internally calls the Adaptee’s methods.
- The Adaptee: Provides data or functionality, but in a format or structure different from what the Client expects.
- Conversion (if necessary): The Adapter transforms the Adaptee’s output into a format that the Client understands, providing seamless compatibility.
When to Use the Adapter Pattern
- When you have existing classes with incompatible interfaces and need them to work together.
- When you want to reuse a class that provides useful functionality but has a different interface than what your system requires.
- When integrating with a third-party library or a legacy system.
Example Scenario
Imagine a modern application that retrieves product details from a legacy system. The modern application expects the product data to be in a Product
format, but the legacy system returns it in a LegacyProduct
format. We can use the Adapter Pattern to convert LegacyProduct
to Product
without modifying either the client or the legacy system.
Code Example
package main
import "fmt"
// Target Interface: The client expects a ProductProvider interface.
type ProductProvider interface {
GetProduct() Product
}
// Expected Data Structure: The modern Product format.
type Product struct {
Name string
Price float64
}
// Adaptee Class: This represents the legacy data source.
type LegacyProduct struct {
ProductName string
ProductCost float64
}
// LegacyProductSource simulates the legacy system that provides data in the LegacyProduct format.
type LegacyProductSource struct{}
func (l *LegacyProductSource) FetchProduct() LegacyProduct {
return LegacyProduct{
ProductName: "Vintage Clock",
ProductCost: 99.99,
}
}
// Adapter: Adapts LegacyProductSource to match the ProductProvider interface.
type ProductAdapter struct {
legacySource *LegacyProductSource
}
// NewProductAdapter is a constructor for ProductAdapter.
func NewProductAdapter(source *LegacyProductSource) *ProductAdapter {
return &ProductAdapter{legacySource: source}
}
// GetProduct adapts LegacyProduct to Product.
func (a *ProductAdapter) GetProduct() Product {
legacyProduct := a.legacySource.FetchProduct()
// Convert LegacyProduct to Product
return Product{
Name: legacyProduct.ProductName,
Price: legacyProduct.ProductCost,
}
}
func main() {
// Create the legacy source and wrap it in an adapter.
legacySource := &LegacyProductSource{}
adapter := NewProductAdapter(legacySource)
// Client retrieves the product using the adapter.
product := adapter.GetProduct()
fmt.Printf("Product: %s, Price: %.2f\n", product.Name, product.Price)
}
Explanation of the Example
-
Target Interface (
ProductProvider
): Defines theGetProduct()
method, which the client expects and which returns aProduct
. -
Adaptee (
LegacyProductSource
): Represents the existing legacy system. It provides data in theLegacyProduct
format, which has different field names (ProductName
andProductCost
). -
Adapter (
ProductAdapter
): Implements theProductProvider
interface and contains a reference to theLegacyProductSource
. In theGetProduct()
method, it fetches data fromLegacyProductSource
, convertsLegacyProduct
toProduct
, and returns it. -
Client (
main
function): Uses the adapter to get the product details in the expectedProduct
format, unaware of the underlying legacy structure.
Benefits of the Adapter Pattern
- Compatibility: Allows incompatible classes to work together without modifying their code.
- Reusability: Existing classes (e.g., legacy systems) can be reused with new systems.
- Flexibility: Adapters can be swapped out as needed to work with different data sources.
Summary
The Adapter Pattern is highly valuable when integrating different systems with incompatible interfaces. By using a wrapper (adapter) that "translates" between interfaces, you enable seamless compatibility and keep client code clean and straightforward. This pattern is especially helpful when working with legacy code, third-party libraries, or APIs that you cannot modify directly.