Design Patterns: Adapter - using GoLang

Jayaprasanna Roddam - Nov 6 - - Dev Community

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

  1. Target: The interface that the client expects to work with.
  2. Adaptee: The existing class that has a different or incompatible interface.
  3. Adapter: A wrapper that implements the Target interface, and internally calls the Adaptee’s methods, translating data if needed.

How It Works

  1. The Client: Makes a call to the Target interface expecting data in a certain format.
  2. The Adapter: Implements the Target interface and, when called by the Client, internally calls the Adaptee’s methods.
  3. The Adaptee: Provides data or functionality, but in a format or structure different from what the Client expects.
  4. 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)
}
Enter fullscreen mode Exit fullscreen mode

Explanation of the Example

  1. Target Interface (ProductProvider): Defines the GetProduct() method, which the client expects and which returns a Product.
  2. Adaptee (LegacyProductSource): Represents the existing legacy system. It provides data in the LegacyProduct format, which has different field names (ProductName and ProductCost).
  3. Adapter (ProductAdapter): Implements the ProductProvider interface and contains a reference to the LegacyProductSource. In the GetProduct() method, it fetches data from LegacyProductSource, converts LegacyProduct to Product, and returns it.
  4. Client (main function): Uses the adapter to get the product details in the expected Product 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.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .