What is Adapter?

The Adapter pattern is probably the most used design pattern in software engineering. It allows objects/classes of different abstracts/signatures/blueprints to collaborate.

I will guide you through a practical scenario that will make it easier to understand the Adapter pattern.

Let’s start with a scenario: Hack the API

Suppose you are creating an OpenAI API client. It connects to the OpenAI client with a secret key. But you want to hack the client to communicate with the Hugging Face API instead. How do we do that? Let’s create one using the Adapter pattern.

Target

At first, let’s see what a minimal client code may look like:

// Target
type IOpenAIClient interface {
	ChatOnce(str string) string
}
type OpenAIClient struct {
	OPEN_AI_SECRET string
}

func (c *OpenAIClient) ChatOnce(str string) string {
	return "Open AI: " + str
}

func NewOpenAIClient() IOpenAIClient {
	c := &OpenAIClient{OPEN_AI_SECRET: os.Getenv("OPEN_AI_SECRET")}
	return c
}

We can initiate a client by calling NewOpenAIClient() which returns a concrete of IOpenAIClient. Here’s a basic example of how we can create an OpenAI client:

func main() {
	c := NewOpenAPIClient()
	res := c.ChatOnce("hello!")
	fmt.Println(res) // Open AI: hello!
}

Adaptee

Let’s say we want to incorporate HuggingFace API instead of OpenAI API using the same class. In this case, HuggingFace API is an Adaptee and the implementation looks like this:

// Adaptee
type HuggingFaceClient struct{}

func (c *HuggingFaceClient) Invoke(str string) string {
	return "HuggingFace: " + str
}
func NewHuggingFaceClient() HuggingFaceClient {
	return HuggingFaceClient{}
}

Adapter

Now, if we want to replace the OpenAI client with HuggingFace client, we need a switching mechanism, a shared signature, an adapter.

// Hacking OpenAI client to use HuggingFace client
type HackClient struct {
	Adaptee HuggingFaceClient
}
func (c *HackClient) ChatOnce(str string) string {
	return c.Adaptee.Invoke(str)
}

Integrating the Adapter

package main

import (
	"fmt"
	"os"
)

// Target
type IOpenAIClient interface {
	ChatOnce(str string) string
}
type OpenAIClient struct {
	OPEN_AI_SECRET string
}

func (c *OpenAIClient) ChatOnce(str string) string {
	return "Open AI: " + str
}

func NewOpenAIClient() IOpenAIClient {
	c := &OpenAIClient{OPEN_AI_SECRET: os.Getenv("OPEN_AI_SECRET")}
	return c
}

// Adaptee
type HuggingFaceClient struct{}

func (c *HuggingFaceClient) Invoke(str string) string {
	return "HuggingFace: " + str
}
func NewHuggingFaceClient() HuggingFaceClient {
	return HuggingFaceClient{}
}

// Hacking OpenAI client to use Hugging Face API
type HackClient struct {
	Adaptee HuggingFaceClient
}

func (c *HackClient) ChatOnce(str string) string {
	return c.Adaptee.Invoke(str)
}

func main() {
	c := NewOpenAIClient()
	res := c.ChatOnce("hello!")
	fmt.Println(res) // Open AI: hello!

	hfClient := NewHuggingFaceClient()
	c = &HackClient{hfClient}
	res = c.ChatOnce("hello!")
	fmt.Println(res) // HuggingFace: hello!
}

Why use the Adapter pattern?

Adapter plays well with the open-closed principle: “Code should be open for extension, closed for modification”. Here’s from my personal experience on how I use an Adapter pattern in Golang. I will note down a few of my use cases:

  1. Create data abstraction layer, a layers just below the database connectors.
  2. WebSocket, WebRTC adapters to use on top of other frameworks (Gin, Echo).
  3. Create standard reusable middlewares that supports other http-routers.
  4. Implement different adapters around the same functionality. E.g. I have developed a notification system where I widely used Adapters- EmailNotifier, SMSNotifier, SlackNotifier etc. part of Notifier class having the function Notify(). It was for refactoring purpose, and it made the codebase much more readable and collaboration-friendly.

Caveats

Design patterns are some efficient/readable ways to approach a solution. If we try to fit design patterns forcefully, we might end up designing something complicated and unnecessary technical debt. Adapter pattern if used when not required will require you to write additional classes which are unnecessary and may bring additional complexities.

Thank you

Thank you guys for reading! I would love to hear your thoughts about my blog posts I have started writing recently. Please drop an email at [email protected] if you would like to share any feedback or suggestions. Peace!