Use the decorator pattern to wrap a request class without editing it
You are given a class GetRequest with async do(url, options) that performs a GET via fetch. Without editing GetRequest, add two behaviours using the GoF structural pattern decorator: a LoggerDecorator that logs each URL before the request, and an AuthDecorator that injects an Authorization header. Decorators must be composable (you can wrap one in another) and expose the same do(url, options) interface as the wrapped object.
class GetRequest {
async do(url, options) {
return fetch(url, { ...options, method: 'GET' });
}
}
Write the implementation.
Define a base Decorator that stores the wrapped object and forwards do to it. Each concrete decorator extends it, adds its behaviour, then calls super.do(...): LoggerDecorator logs the URL, AuthDecorator merges an Authorization header. Because every decorator keeps the same do signature, you compose them — new LoggerDecorator(new AuthDecorator(new GetRequest())) — and the wrapped class is never modified.
- ✗Editing the wrapped class instead of wrapping it, which defeats the open/closed purpose of the pattern
- ✗Breaking the shared
do(url, options)interface, so decorators can no longer wrap each other - ✗Forgetting to call
super.do(...), so the inner request never actually runs
- →How does keeping the same
dosignature on every decorator let you stack them in any order? - →How does the decorator pattern differ from just subclassing
GetRequest?
Solution
The base Decorator stores the wrapped object and delegates do to it by default; concrete decorators add behaviour and call super.do.
class Decorator {
constructor(request) {
this.request = request;
}
async do(url, options) {
return this.request.do(url, options);
}
}
class LoggerDecorator extends Decorator {
async do(url, options) {
console.info(`[Logger] GET ${url}`);
return super.do(url, options);
}
}
class AuthDecorator extends Decorator {
async do(url, options) {
const headers = { ...options?.headers, Authorization: 'Bearer token' };
return super.do(url, { ...options, headers });
}
}
const request = new LoggerDecorator(new AuthDecorator(new GetRequest()));
request.do('/api/items');
How it works
Decorator holds a reference to the wrapped object and by default simply forwards the do call. Each concrete decorator extends it, does its job — logging or adding a header — and then calls super.do(...), passing control further down the chain.
The key to composition is the single do(url, options) signature shared by everyone: GetRequest and every decorator alike. So a decorator can wrap either the request itself or another decorator, and the wrapping order sets the execution order. The original GetRequest is never modified — the pattern extends behaviour without touching the class code. </content>