Adapter Design Pattern in TypeScript (Part 2): Applicability & Structure

Hooman Momtaheni
4 min readMay 25, 2024

--

In part 1, we explained the intent of adopting the Adapter design pattern; in this section, we will explore its applicability and structure.

Applicability

Use the Adapter pattern when:

· you want to use an existing class, and its interface does not match the one you need. (Same as above part 1 scenario.)

· you want to create a reusable class that cooperates with unrelated or unforeseen classes, that is, classes that don’t necessarily have compatible interfaces. In this case adapter allows the reusable class to work with different interfaces.
Suppose you are designing a drawing application that needs to work with different graphical elements, including rectangles, circles, and text. The interfaces of these elements are not known in advance.
You can define a DrawingEditor class that uses the Adapter pattern to work with these different elements:

interface Shape {
draw(): void;
}

class DrawingEditor {
private shapes: Shape[] = [];

addShape(shape: Shape): void {
this.shapes.push(shape);
}

draw(): void {
this.shapes.forEach(shape => shape.draw());
}
}

// Adapting different classes to the Shape interface
class RectangleAdapter implements Shape {
private rectangle: LegacyRectangle;

constructor(rectangle: LegacyRectangle) {
this.rectangle = rectangle;
}

draw(): void {
this.rectangle.draw(0, 0);
}
}

class CircleAdapter implements Shape {
private circle: LegacyCircle;

constructor(circle: LegacyCircle) {
this.circle = circle;
}

draw(): void {
this.circle.draw(0, 0, 10);
}
}

// Assuming LegacyCircle has its own interface
class LegacyCircle {
draw(x: number, y: number, radius: number): void {
console.log(`Drawing circle at (${x}, ${y}) with radius ${radius}`);
}
}

class LegacyRectangle {
draw(x: number, y: number): void {
console.log(`Drawing rectangle at (${x}, ${y})`);
}
}

// Usage
const drawingEditor = new DrawingEditor();

const legacyRectangle = new LegacyRectangle();
const rectangleAdapter = new RectangleAdapter(legacyRectangle);
drawingEditor.addShape(rectangleAdapter);

const legacyCircle = new LegacyCircle();
const circleAdapter = new CircleAdapter(legacyCircle);
drawingEditor.addShape(circleAdapter);

drawingEditor.draw();

In this case, the DrawingEditor class is designed to work with any Shape interface, making it reusable with different, unrelated classes. The adapters (RectangleAdapter and CircleAdapter) allow these classes to be used with DrawingEditor.

· (Object adapter only) you need to use several existing subclasses, but it’s unpractical to adapt their interface by subclassing everyone. An object adapter can adapt the interface of its parent class.
Suppose you have a hierarchy of different shapes in a drawing library, and each shape subclass has its own method for drawing, but with slightly different interfaces. Instead of creating a new subclass for each shape, you can use a single object adapter to adapt the common parent class interface.

// Common interface for all shapes in the drawing library
class Shape {
draw(): void {}
}

class Rectangle extends Shape {
drawRectangle(x: number, y: number, width: number, height: number): void {
console.log(`Drawing rectangle at (${x}, ${y}) with width ${width} and height ${height}`);
}
}

class Circle extends Shape {
drawCircle(x: number, y: number, radius: number): void {
console.log(`Drawing circle at (${x}, ${y}) with radius ${radius}`);
}
}

class Triangle extends Shape {
drawTriangle(x: number, y: number, base: number, height: number): void {
console.log(`Drawing triangle at (${x}, ${y}) with base ${base} and height ${height}`);
}
}

You want to use these shapes in a new system that expects the Shape interface with a standard draw() method.

// The adapter adapts the interface of the parent class Shape.
class ShapeAdapter {
private shape: Shape;

constructor(shape: Shape) {
this.shape = shape;
}

draw(): void {
if (this.shape instanceof Rectangle) {
(this.shape as Rectangle).drawRectangle(0, 0, 10, 20);
} else if (this.shape instanceof Circle) {
(this.shape as Circle).drawCircle(0, 0, 10);
} else if (this.shape instanceof Triangle) {
(this.shape as Triangle).drawTriangle(0, 0, 10, 20);
} else {
this.shape.draw();
}
}
}

// Usage
const shapes: Shape[] = [
new Rectangle(),
new Circle(),
new Triangle(),
];

const adapters: ShapeAdapter[] = shapes.map(shape => new ShapeAdapter(shape));

adapters.forEach(adapter => adapter.draw());

In above example Rectangle, Circle, and Triangle are subclasses of Shape, each with their own drawing methods and the new system expects a unified draw() method. So, ShapeAdapter takes an instance of Shape and adapts its specific drawing method to the draw() method. With this technique we only need one adapter class instead of creating a separate adapter subclass for each shape and also this single adapter can handle any future shapes that inherit from Shape, as long as the appropriate casting and method calls are added. As you can see This approach simplifies adapting multiple subclasses and provides a unified interface for the client code.

Structure

A class adapter uses multiple inheritance to adapt one interface to another (As we said earlier, in TypeScript we use mixin for multiple inheritance):

Credit: Design Patterns: Elements of Reusable Object-Oriented Software

An object adapter relies on object composition:

Credit: Design Patterns: Elements of Reusable Object-Oriented Software

Participants

· Target (Shape)

- defines the domain-specific interface that Client uses.

· Client (DrawingEditor)

- collaborates with objects conforming to the Target interface.

· Adaptee (TextView)

- defines an existing interface that needs adapting.

· Adapter (TextShape)

- adapts the interface of Adaptee to the Target interface.

Collaborations

Clients call operations on an Adapter instance. In turn, the adapter calls Adaptee operations that carry out the request.

In the next part we will explore consequences of this pattern.

--

--

Hooman Momtaheni

Full Stack Web Application Developer | Admire foundations and structures | Helping companies have reliable web apps and motivated software development team