Adapter Design Pattern in TypeScript (Part 1): Intent & Motivation

Hooman Momtaheni
5 min readMay 25, 2024

--

In this series of articles, I plan to cover the adapter design pattern as described in the book “Design Patterns: Elements of Reusable Object-Oriented Software” using the same content structure. But I attempted to illustrate the tricky sections by more explanations and using TypeScript examples.

Intent

Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces.

Also Known As

Wrapper

Motivation

Often, a toolkit class that’s designed for reuse isn’t actually reusable because its interface doesn’t align with the domain-specific interface required by an application. Take, for instance, a drawing editor that allows users to create and organize graphical elements such as lines, polygons, and text into images and diagrams. The central abstraction in this drawing editor is the graphical object, which has an editable shape and can render itself. This interface is specified by an abstract class called Shape. For each type of graphical object, the editor defines a subclass of Shape: LineShape for lines, PolygonShape for polygons, and so on.

Implementing classes for basic geometric shapes like LineShape and PolygonShape is straightforward because their drawing and editing capabilities are limited by nature. On the other hand, creating a TextShape subclass that can both display and edit text is much more complex, due to the intricate screen updates and buffer management required for text editing. Meanwhile, a sophisticated class such as TextView from an existing UI toolkit might already handle text display and editing effectively. Ideally, we want to reuse TextView to implement TextShape, but since TextView was not designed with Shape classes in mind, TextView and Shape objects cannot be used interchangeably.

abstract class Shape {
abstract BoundingBox(): { x: number, y: number, width: number, height: number };
abstract createManipulator(): void;
}

class TextView {
constructor(public text: string) {}

GetExtent(): { x: number, y: number, width: number, height: number } {
// Mock implementation of text extent calculation.
return { x: 0, y: 0, width: this.text.length * 7, height: 20 };
}

displayText(): void {
console.log("Displaying text: " + this.text);
}

// Other text-view related methods can be defined here.
}

To enable existing, unrelated classes like TextView to function within an application that expects a different and incompatible interface, we face a dilemma. Modifying the TextView class to conform to the Shape interface isn’t feasible without access to the toolkit’s source code. Moreover, even with the source code, it wouldn’t be appropriate to alter TextView to fit one application’s specific needs.

Instead, we can create TextShape to adapt the TextView interface to match that of Shape. This can be achieved in two ways:

(1) by inheriting from Shape and implementing TextView’s functionality or

(2) by incorporating a TextView instance within TextShape and implementing TextShape to work with TextView’s interface.

These two methods correspond to the class and object forms of the Adapter pattern. Thus, TextShape serves as an adapter.

Example of (1) class form of Adapter pattern:

For implementing Adapter in this way in TypeScript that has not ability of multi-inheritance, we should use a mixin function (A mixin function is a design pattern used to create reusable chunks of behavior or functionality that can be added to multiple classes without using inheritance. Mixins allow for the combination of methods and properties from different sources, enabling developers to compose classes with shared functionality in a flexible and modular manner.):

function TextViewMixin<TBase extends new (...args: any[]) => {}>(Base: TBase) {
return class extends Base {
textView: TextView;

constructor(...args: any[]) {
super(...args);
this.textView = new TextView(args[0]);
}

displayText(): void {
this.textView.displayText();
}

GetExtent(): { x: number, y: number, width: number, height: number } {
return this.textView.GetExtent();
}
};
}

Since we can’t use Shape directly in the mixin, we create ConcreteShape as an intermediary:

class ConcreteShape extends Shape {
private text: string;

constructor(text: string) {
super();
this.text = text;
}

BoundingBox(): { x: number, y: number, width: number, height: number } {
return { x: 0, y: 0, width: 100, height: 100 };
}

createManipulator(): void {
console.log("Creating manipulator for ConcreteShape");
}
}

Ultimately, we create our TextShape class with mixin:

class TextShape extends TextViewMixin(ConcreteShape) {
constructor(text: string) {
super(text);
}

BoundingBox(): { x: number, y: number, width: number, height: number } {
return this.GetExtent();
}

createManipulator(): void {
console.log("Creating manipulator for TextShape");
}

draw(): void {
this.displayText();
}
}

Usage:

const shapes: Shape[] = [
new TextShape("Hello, world!"),
new TextShape("Adapter pattern example")
];

shapes.forEach(shape => {
shape.createManipulator();
const bounds = shape.BoundingBox();
console.log(`Bounds: x=${bounds.x}, y=${bounds.y}, width=${bounds.width}, height=${bounds.height}`);
});

Note that if instead of Shape abstract class, we defined Shape as an interface, we didn’t need mixin and we could create adapter like this:

class TextShape extends TextView implements Shape{
//adapter operations
}

Example of (2) class form of Adapter pattern:

The object form of the Adapter pattern is easier to implement than the class form. Instead of using mixins, the adapter class directly holds an instance of the class it adapts (TextView). The adapter class (TextShape) implements the target interface (Shape) and delegates the necessary method operations to the instance of the adaptee class (TextView).

class TextShape extends Shape {
private textView: TextView;

constructor(text: string) {
super();
this.textView = new TextView(text);
}

BoundingBox(): { x: number, y: number, width: number, height: number } {
return this.textView.GetExtent();
}

createManipulator(): void {
console.log("Creating manipulator for TextShapeAdapter");
}

draw(): void {
this.textView.displayText();
}
}

And usage:

const shapes: Shape[] = [
new TextShape ("Hello, world!"),
new TextShape ("Adapter pattern example")
];

shapes.forEach(shape => {
shape.createManipulator();
const bounds = shape.BoundingBox();
console.log(`Bounds: x=${bounds.x}, y=${bounds.y}, width=${bounds.width}, height=${bounds.height}`);
});
Credit: Design Patterns: Elements of Reusable Object-Oriented Software

This diagram illustrates the object adapter case (2nd form). It shows how BoundingBox requests, declared in class Shape, are converted to GetExtent requests defined in TextView. Since TextShape adapts TextView to the Shape interface, the drawing editor can reuse the otherwise incompatible TextView class.

Often the adapter is responsible for functionality the adapted class doesn’t provide. The diagram shows how an adapter can fulfill such responsibilities. The user should be able to “drag” every Shape object to a new location interactively, but TextView isn’t designed to do that. TextShape can add this missing functionality by implementing Shape’s CreateManipulator operation, which returns an instance of the appropriate Manipulator subclass.

abstract class Manipulator {
abstract drag(x: number, y: number): void;
}
class TextManipulator extends Manipulator {
constructor(private shape: TextShapeAdapter) {
super();
}

drag(x: number, y: number): void {
console.log(`Dragging text shape to (${x}, ${y})`);
// Here you can add logic to update the shape's position.
}
}
class TextShapeAdapter extends Shape {
private textView: TextView;

constructor(text: string) {
super();
this.textView = new TextView(text);
}

BoundingBox(): { x: number, y: number, width: number, height: number } {
return this.textView.GetExtent();
}

createManipulator(): Manipulator {
return new TextManipulator(this);
}

draw(): void {
this.textView.displayText();
}
}

Manipulator is an abstract class for objects that know how to animate a Shape in response to user input, like dragging the shape to a new location. There are subclasses of Manipulator for different shapes; TextManipulator, for example, is the corresponding subclass for TextShape. By returning a TextManipulator instance, TextShape adds the functionality that TextView lacks but Shape requires.

In the next part (Part 2) we will talk about applicability and structure of Adapter design pattern.

--

--

Hooman Momtaheni

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