Skip to Content
Course content

233: Adapter Pattern in Go

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time when I'm reviewing PRs: developers treat the Adapter pattern as if it's just a "wrapper" used for convenience or to hide a messy function. They'll wrap a three-line function in another function and call it an Adapter. But that's not what this pattern is for. If you're just trying to make a function call shorter or "cleaner," you're thinking of a helper function or perhaps a Facade.

Adapters aren't just "wrappers" for convenience

The biggest misconception is that the Adapter pattern is about simplifying code. It isn't. It's about compatibility. When you're using the Adapter pattern, you aren't trying to make a piece of code "easier" to use; you're trying to make a piece of code that has one interface work with a system that expects a completely different interface.

Imagine you have a system that processes payments using a PaymentProcessor interface. Your entire codebase depends on this interface. Now, your boss tells you to integrate a new, third-party payment vendor. You can't change the vendor's code (it's a compiled library), and you don't want to rewrite your entire application to accommodate their specific method signatures. If you just "wrap" their function for convenience, you're still stuck with a type mismatch. You need a bridge.

Bridging the gap between incompatible interfaces

In Go, the Adapter pattern relies heavily on our implicit interface implementation. The goal is to create a new type (the Adapter) that satisfies the interface your application expects, while internally delegating the actual work to the incompatible third-party object.

Let's look at a concrete example. Suppose your app expects this interface:

type PaymentProcessor interface {
    Pay(amount float64) error
}

But the third-party library you just bought, LegacyBankAPI, looks like this:

type LegacyBankAPI struct{}

func (l *LegacyBankAPI) MakeTransaction(cents int, currency string) bool {
    // Imagine complex legacy logic here
    fmt.Printf("Processing %d cents in %s\n", cents, currency)
    return true
}

You can't pass LegacyBankAPI into any function expecting a PaymentProcessor because MakeTransaction is not Pay. The signatures are completely different: one takes a float, the other takes an int and a string. One returns an error, the other returns a boolean.

Here is how I would handle this. I'd create an adapter that "translates" the request:

type BankAdapter struct {
    api *LegacyBankAPI
}

func NewBankAdapter(api *LegacyBankAPI) *BankAdapter {
    return &BankAdapter{api: api}
}

func (a *BankAdapter) Pay(amount float64) error {
    // Translation logic: convert dollars to cents
    cents := int(amount * 100)
    
    // Call the incompatible method
    success := a.api.MakeTransaction(cents, "USD")
    
    if !success {
        return fmt.Errorf("payment failed via legacy bank")
    }
    return nil
}

Now, the BankAdapter satisfies the PaymentProcessor interface. Your core business logic doesn't need to know that LegacyBankAPI even exists; it just calls Pay(), and the adapter handles the awkward translation. I've found that this is the only way to keep a codebase sane when you're forced to deal with rigid external dependencies.

  • The Client: Your app, which expects the PaymentProcessor interface.
  • The Adaptee: The LegacyBankAPI, which has the functionality but the wrong interface.
  • The Adapter: BankAdapter, which maps the Client's needs to the Adaptee's capabilities.



📋 Practical Task

Implementing a Cloud Storage Adapter

You are building a file upload system. Your application uses a FileUploader interface to ensure that you can switch cloud providers easily. However, you've just integrated a new SDK called FastStorageSDK that doesn't follow your interface.

Your Task: Create an adapter that allows FastStorageSDK to be used wherever a FileUploader is required.

Here is the existing code:

package main

import (
    "fmt"
)

// The interface your application expects
type FileUploader interface {
    UploadFile(filename string, data []byte) error
}

// The third-party SDK (The Adaptee)
// You cannot modify this struct or its methods.
type FastStorageSDK struct{}

func (f *FastStorageSDK) PutObject(key string, content []byte, size int) bool {
    fmt.Printf("FastStorage: Uploading %s (%d bytes)...\n", key, size)
    return true
}

func main() {
    sdk := &FastStorageSDK{}
    
    // TODO: Create your adapter here and pass it into the ProcessUpload function
    // var adapter FileUploader = ...
    
    err := ProcessUpload(nil, "profile.jpg", []byte("fake-image-data"))
    if err != nil {
        fmt.Println("Error:", err)
    }
}

func ProcessUpload(u FileUploader, name string, data []byte) error {
    if u == nil {
        return fmt.Errorf("uploader not provided")
    }
    return u.UploadFile(name, data)
}

Requirements:

  1. Define a StorageAdapter struct that wraps *FastStorageSDK.
  2. Implement the UploadFile method on StorageAdapter.
  3. Inside UploadFile, call the SDK's PutObject method, calculating the size of the data slice for the third argument.
  4. Handle the boolean return value of PutObject: if it returns false, return an error; otherwise, return nil.
  5. Update main() to instantiate your adapter and pass it to ProcessUpload.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.