DEV Community

Cover image for Design Patterns: Adapter Pattern
Brent Dalling
Brent Dalling

Posted on

Design Patterns: Adapter Pattern

Okay. Let's explore something together. I've recently come across a few open source projects that can really use this pattern / strategy.

So hear me out. Code design patterns exist and are thoroughly documented and taught for a reason. They improve out efficiencies and code reliability. So, why don't more developers use them? I think the answer is because they're normally grouped with Data Structures and Algorithms in schools. Therefore they're thought of as scary. Or, self taught developers (like me) just don't know they exist until someone mentions them.

I took about 3 years of being a developer to really start learning these concepts. Before then I was building custom software for an ISP. My code was clean. But it was not efficient. So, let's dive into one of my favorite patterns. The Adapter Pattern. duh. dong. daaaaaaa.

Adapter is a structural design pattern that allows objects with incompatible interfaces to collaborate.
https://refactoring.guru/design-patterns/adapter

Okay. So, it some how allows for two objects with completely different methods and attributes to collaborate? That's wild! It's like combining a pizza and a slice of bread into a glorious cake!

It's easy to understand at a conceptual level. However, when we look at the actual implementation it can get a little tricky. In fact, I think most developers instinctively understand the adapter pattern. They usually just lose their way during implementation.

Deep Dive Begins

Let's take a deep dive into how this works with a working code example that we will build together. I will use Typescript for strict typing. It is probably most familiar to most people reading this article.

Defining the problem

We want to implement payment code for our fancy new SAAS app. However, our target demographic is other companies. Those companies may want to use any number of preferred payment providers.

Determining potential solutions

We have a few options. We can build out custom payment endpoints on an API for each payment method. The drawback here is having to maintain multiple sections of code that does the exact same thing.

We can build out an adapter pattern that contains the same payment code / interface across all payment types. We write the shared interface code once and handle our payment provider specific code in each payment providers code. This abstracts it so that we can ignore complexities.

We can also just tell people to use one payment provider. However, they might not like that. So let's avoid it.

Writing a sotution

The code below lays the groundwork for our fancy new adapter pattern. The code creates a new interface (a set of methods and attributes that code implementing it must have to be valid) that defines methods for our adapters. These methods will be the same no matter what payment provider we are using.

interface PaymentsProvider {
  name: string;
  provider: 'stripe' | 'paypal' | 'braintree' | 'adyen' | 'payone';

  getPayments(): Promise<Payment[]>
  getPayment(id: string): Promise<Payment>
  createPayment(payment: Payment): Promise<Payment>
  updatePayment(payment: Payment): Promise<Payment>
  deletePayment(id: string): Promise<void>
}

// Define our payment structure
type Payment = {
  id: string;
  amount: number;
  currency: string;
  status: 'pending' | 'completed';
};

// Defines the provider type as a union of defined providers
type Provider = StripeProvider | PayPalProvider;

// Provides and example payment object
let paymentResponse: Payment = {
  id: '123',
  amount: 100,
  currency: 'USD',
  status: 'pending'
}
Enter fullscreen mode Exit fullscreen mode

Okay. So we now have a way to define how we should interaction with payment providers. We also built out the payment structure. In addition to the payment structure we built out a sample payment object. We will use all of this later. For now, let's build an example adapter.

class StripeProvider implements PaymentsProvider {
  name = 'Stripe';
  provider = 'stripe';

  async getPayments(): Promise<Payment[]> {
    return [paymentResponse];
  }

  async getPayment(id: string): Promise<Payment> {
    return paymentResponse;
  }

  async createPayment(payment: Payment): Promise<Payment> {
    return paymentResponse;
  }

  async updatePayment(payment: Payment): Promise<Payment> {
    return paymentResponse;
  }

  async deletePayment(id: string): Promise<{ success: boolean }> {
    return { success: true };
  }
}
Enter fullscreen mode Exit fullscreen mode

This code is all boilerplate. However, in a real world situation you would write all of your payment provider specific code in these payment provider classes.

function Payments () {
  let providers: { [key: string]: Provider };

  return {
    addProvider(provider: Provider) {
      providers[provider.provider] = provider;
    },
    getProvider(provider: string): Provider {
      return providers[provider];
    },
    getProviders(): Provider[] {
      return Object.values(providers);
    },
    removeProvider(provider: string) {
      delete providers[provider];
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This is the provider manager. It'svery similar to a builder method. It's how I prefer to handle these. It's simple and easy to understand while keeping it easy to maintain. Let's move onto seeing it put together!

const payments = Payments();

payments.addProvider(new StripeProvider());
payments.addProvider(new PayPalProvider());

console.log(payments.getProviders());

payments.getProvider('stripe').getPayments().then(console.log);
payments.getProvider('paypal').getPayments().then(console.log);
Enter fullscreen mode Exit fullscreen mode

You can view the full example source code below.

If you found this helpful please leave a like and bookmark. Please leave a comment! Let's discuss where we can use this! If you spotted a problem with the information provided please leave a comment and let me know. Let's have a healthy discussion! Thanks for reading!

Top comments (0)