Skip to main content

Adapter

The Adapter Pattern is a structural design pattern that allows objects with incompatible interfaces to work together. It acts as a bridge between two otherwise incompatible interfaces. This pattern involves a single class, the adapter, which joins functionalities of independent or incompatible interfaces.

Here's a basic overview of how the Adapter Pattern works:

adapter

Adapter in the Wikipedia

  1. Target Interface: This is the interface that the client expects to work with. It is the interface that your application's existing classes already follow.
  2. Adaptee: This is the class that needs adapting. It has a different interface, and you want to use it in your application, but it’s not compatible with your existing code.
  3. Adapter: This is the class that implements the Target interface and holds an instance of the Adaptee. The adapter translates calls from the Target interface into a form that the Adaptee understands.

Use Cases​

The Adapter Pattern is useful when:

  • You want to use an existing class that doesn't quite fit your interface requirements.
  • You want to create a reusable class that cooperates with classes which don't have compatible interfaces.
  • You need to create a class that can interact with classes that don't yet exist, by simulating their interfaces.

Here's a simple example of the Adapter Pattern in TypeScript:

// Target Interface
interface ITarget {
request(): string;
}

// Adaptee
class Adaptee {
public specificRequest(): string {
return '.eetpadA eht fo roivaheb laicepS';
}
}

// Adapter
class Adapter implements ITarget {
private adaptee: Adaptee;

constructor(adaptee: Adaptee) {
this.adaptee = adaptee;
}

public request(): string {
const result = this.adaptee.specificRequest().split('').reverse().join('');
return `Adapter: (TRANSLATED) ${result}`;
}
}

// Client Code
function clientCode(target: ITarget) {
console.log(target.request());
}

const adaptee = new Adaptee();
console.log(`Adaptee: ${adaptee.specificRequest()}`);

const adapter = new Adapter(adaptee);
clientCode(adapter);

In this example, the Adaptee class has a specificRequest method that returns a string. The Adapter class implements the ITarget interface and translates the specificRequest method into a form that the request method of the ITarget interface expects.

Tips for Fullstack Developers​

  • When applying design patterns like the Adapter, consider how they can help in creating a more maintainable and flexible codebase.
  • In a fullstack context, adapters can be especially useful for dealing with various APIs or data sources that might not have compatible formats or interfaces.
  • Design patterns can sometimes add complexity. Use them judiciously and only when they clearly solve a specific problem in your application architecture.

FP version​

In a functional programming (FP) context, the Adapter Pattern can be implemented using functions instead of classes. The key idea remains the same: we want to convert the interface of one function to match that of another. In TypeScript, this can be done by defining function signatures for the target and adaptee, and then creating an adapter function that maps between them.

// Adaptee Function
function adapteeFunction(): string {
return '.eetpadA eht fo roivaheb laicepS';
}

// Target Function Signature
type TargetFunction = () => string;

// Adapter Function
const adapterFunction: TargetFunction = () => {
const result = adapteeFunction().split('').reverse().join('');
return `Adapter: (TRANSLATED) ${result}`;
};

// Client Code
function clientCode(targetFn: TargetFunction) {
console.log(targetFn());
}

// Using the Adapter Function
clientCode(adapterFunction);

In this example, adapteeFunction is a function that we want to adapt. TargetFunction is a type representing the signature of the function expected by our client code. adapterFunction is the adapter that conforms to the TargetFunction type and internally uses adapteeFunction, adapting its behavior.

Tips for Functional Programming in Fullstack Development​

  • Embrace immutability and pure functions for predictability and easier debugging.
  • Utilize higher-order functions to create adapters, as they can manipulate other functions and create new functions dynamically.
  • Leverage TypeScript's type system to enforce function signatures and interfaces, ensuring that adapters correctly match the expected patterns.
  • Consider the composition of functions, which is a core concept in FP, to build complex operations from simpler ones. This can often replace the need for more traditional OOP patterns.