Design patterns in Go: Adapter

01 Jan 2021

There might be cases when two different types of objects should be used for a similar processing. It’s easy when those types share similar interface, but how about the cases when interfaces are different. Obviously, there should be a mechanism for translating the difference to the consumer object. Here is where Adapter design patern comes to help.

Problem

Let’s imagine a case, that you have integrated a payment service, and you have only single payment functionality in place. But, here comes a request form bussiness, in which a subscription should be added, eventually a subscription payment should be added as well. Let’s consider the third party payment service pretty rudimentary, and it does not have it’s own subscription payments functionality, only transactions API.

This way you need to adapt the incoming payment request, in order to perform a transaction. For that purpose an adapter structure will be created in order to translate the subscription payment request.

Solution

Let’s take a look on the code:

type StripeService struct{}

func NewStripeService() StripeService {
	return StripeService{}
}

func (s StripeService) Execute(amount int, sender, receiver string) error {
	var err error

	// ... handling all Stripe API transaction logic, and return err if it occurs
	fmt.Printf("Paying $%d from %s to %s\n", amount, sender, receiver)

	return err
}

type Service interface {
	Execute(amount int, sender, receiver string) error
}

type PaymentsService struct {
	service Service
}

func NewPaymentsService(service Service) PaymentsService {
	return PaymentsService{service}
}

func (p PaymentsService) MakeTransaction(payment Payment) error {
	var err error

	err = p.service.Execute(
		payment.GetAmount(),
		payment.GetSenderID(),
		payment.GetRecipientID(),
	)

	return err
}

Here are defined the Stripe library with specific interface, and payments service, which actually handles the payment logic.

Next, we will take a look on SinglePayment structure:

type Payment interface {
	GetAmount() int
	GetRecipientID() string
	GetSenderID() string
}

type SinglePayment struct {
	senderID    string
	recipientID string
	amount      int
}

func NewSinglePayment(amount int, senderID, recipientID string) *SinglePayment {
	return &SinglePayment{senderID, recipientID, amount}
}

func (sp SinglePayment) GetAmount() int {
	return sp.amount
}

func (sp SinglePayment) GetSenderID() string {
	return sp.senderID
}

func (sp SinglePayment) GetRecipientID() string {
	return sp.recipientID
}

Now, let’s assume that subscription structure will have a different interface, like following:

type SubscriptionPayment struct {
	customerID       string
	subscriptionPlan string
	units            int
}

func NewSubscriptionPayment(customerId, subscriptionPlan string, units int) *SubscriptionPayment {
	return &SubscriptionPayment{customerId, subscriptionPlan, units}
}

func (sb SubscriptionPayment) GetCustomerPaymentID() (string, error) {
	customer, err := customers.Find(sb.customerID)
	if err != nil {
		return "", err
	}

	return customer.paymentId, nil
}

func (sb SubscriptionPayment) GetSubscriptionPlan() string {
	return sb.subscriptionPlan
}

func (sb SubscriptionPayment) GetTimeUnits() int {
	return sb.units
}

In order to make it suitable for PaymentsService structure, we need to implement Payment interface. However that would break Single Responsibility Principle of this struct. This means we will need an adapter structure defined:

type SubscriptionPaymentsAdapter struct {
	payment           SubscriptionPayment
	subscriptionPlans map[string]int
}

func NewSubscriptionPaymentsAdapter(payment SubscriptionPayment) *SubscriptionPaymentsAdapter {
	plans := map[string]int{
		"MONTHLY": 9990,
		"YEARLY":  24990,
	}
	return &SubscriptionPaymentsAdapter{payment, plans}
}

func (sp SubscriptionPaymentsAdapter) GetAmount() int {
	planFee, ok := sp.subscriptionPlans[sp.payment.GetSubscriptionPlan()]
	if !ok {
		return 0
	}
	return planFee * sp.payment.GetTimeUnits()
}

func (sp SubscriptionPaymentsAdapter) GetSenderID() string {
	id, _ := sp.payment.GetCustomerPaymentID()
	return id
}

func (sp SubscriptionPaymentsAdapter) GetRecipientID() string {
	return os.Getenv("COMPANY_PAYMENT_ID")
}

The code for handling this kind of architecture will be as follows:

	var payment Payment
	service := NewPaymentsService(NewStripeService())
	payment = NewSinglePayment(200, "A", "B")

	service.MakeTransaction(payment)

	subscriptionPayment := NewSubscriptionPayment("2", "MONTHLY", 2)
	payment = NewSubscriptionPaymentsAdapter(*subscriptionPayment)
	service.MakeTransaction(payment)

This way, the subscription payment, that at first sight is not compatible with payment interface, can be used as a payment. Of course this example illustrates only the payment logic part, but this can be valable for different functionalities as well.

Conclusion

This pattern is also useful for wrapping up some third party library code so that can not be changed, and it will provide application specific logic, based on other kind of interfaces