Skip to main content

Real World Implementation Of Decorator Pattern

A common use-case for the decorator pattern is adding middleware layers to a web server request handling pipeline. Middleware components can handle tasks such as authentication, logging, data validation, error handling, etc.

In this diagram:

  • ServerRequest is an interface that BaseServer, ServerRequestDecorator, LoggingMiddleware, and AuthMiddleware implement.
  • BaseServer is a basic server that can handle requests.
  • ServerRequestDecorator is an abstract decorator class that contains a ServerRequest object and defines the handle() method.
  • LoggingMiddleware and AuthMiddleware are concrete decorator classes that add logging and authentication functionality, respectively. They each contain a ServerRequest (which could be another decorator or a BaseServer) and define their own handle() methods.

Real World Imeplementation

Here's a simple example using a server handling HTTP requests, represented as an object. We'll have middleware decorators for authentication and logging:

First, define an interface ServerRequest which represents a request handler.

interface ServerRequest {
handle(request: any): void;
}

Next, define a BaseServer class which will handle basic requests.

class BaseServer implements ServerRequest {
handle(request: any): void {
console.log("Handling request:", request);
}
}

Now, create a ServerRequestDecorator abstract class which will be the base for our middleware decorators. It implements ServerRequest and has a ServerRequest object as a property.

abstract class ServerRequestDecorator implements ServerRequest {
protected serverRequest: ServerRequest;

constructor(serverRequest: ServerRequest) {
this.serverRequest = serverRequest;
}

abstract handle(request: any): void;
}

Next, define a LoggingMiddleware class, a decorator that logs requests.

class LoggingMiddleware extends ServerRequestDecorator {
handle(request: any): void {
console.log("Logging request:", request);
this.serverRequest.handle(request);
}
}

And an AuthMiddleware class, a decorator that checks if requests are authenticated.

class AuthMiddleware extends ServerRequestDecorator {
handle(request: any): void {
if (request.isAuthenticated) {
console.log("Request is authenticated");
this.serverRequest.handle(request);
} else {
console.log("Unauthorized request");
}
}
}

Finally, you can create a BaseServer and wrap it in LoggingMiddleware and AuthMiddleware to add logging and authentication functionality to the request handling pipeline.

let server: ServerRequest = new BaseServer();
server = new LoggingMiddleware(server);
server = new AuthMiddleware(server);

const request = {
isAuthenticated: true,
body: "Hello, World!",
};

server.handle(request);

In this example, a request is first logged by LoggingMiddleware, then checked for authentication by AuthMiddleware, and finally handled by BaseServer if it's authenticated.

You could easily add new middleware decorators or remove existing ones without changing the BaseServer class or the decorators themselves. This is the advantage of the decorator pattern: it lets you add functionality to objects dynamically and transparently.

Advantages Of The Decorator Pattern

The Decorator pattern offers several key advantages:

Provides a flexible alternative to subclassing for extending functionality

With the Decorator Pattern, you can add new behaviors to objects without affecting the behaviors of other objects of the same class. This is more flexible than subclassing, which extends behavior at the class level, not the object level.

In our example, we can add LoggingMiddleware or AuthMiddleware functionality to the BaseServer on a case-by-case basis without affecting other instances of BaseServer.

let server1: ServerRequest = new BaseServer(); // No additional behaviors
let server2: ServerRequest = new LoggingMiddleware(new BaseServer()); // With logging
let server3: ServerRequest = new AuthMiddleware(new BaseServer()); // With authentication
let server4: ServerRequest = new AuthMiddleware(
new LoggingMiddleware(new BaseServer())
); // With logging and authentication

Allows functionality to be added and removed at runtime

Decorators can be added to and removed from an object dynamically at runtime.

In a more complex example, you could imagine turning the logging or authentication features on or off at runtime based on user preferences or system configurations.

Promotes code reuse and reduces code redundancy

Each decorator class encapsulates a specific feature, making it easy to add or remove these features without changing the underlying object or other decorators.

In our example, LoggingMiddleware and AuthMiddleware are separate classes. If you wanted to add a new feature like data validation, you could simply create a new ValidationMiddleware class without having to modify the existing middleware classes.

class ValidationMiddleware extends ServerRequestDecorator {
handle(request: any): void {
if (this.isValid(request)) {
this.serverRequest.handle(request);
} else {
console.log("Invalid request");
}
}

private isValid(request: any): boolean {
// Validate request...
return true;
}
}

let server: ServerRequest = new ValidationMiddleware(new BaseServer()); // Now with data validation

Keeps the code simple and clean by avoiding an overload of subclasses

Without decorators, each combination of features would require its own subclass. This would quickly become unmanageable as the number of possible combinations grows. Decorators allow each feature to be defined in its own class, which can be combined with other features as needed.

In our example, we can combine LoggingMiddleware, AuthMiddleware, and ValidationMiddleware in any way we like without needing to create separate subclasses for each possible combination.

Allows to follow the Single Responsibility Principle (SRP)

Each decorator is responsible for a specific feature, which is in line with the SRP. Changes to one decorator do not affect the others.

In our example, LoggingMiddleware is responsible for logging requests, AuthMiddleware for checking authentication, and ValidationMiddleware for validating request data. Each class can be modified independently without affecting the others.

TypeScript Course Instructor Image
TypeScript Course Instructor Image

Time To Transition From JavaScript To TypeScript

Level Up Your TypeScript And Object Oriented Programming Skills. The only complete TypeScript course on the marketplace you building TypeScript apps like a PRO.

SEE COURSE DETAILS

What Can You Do Next 🙏😊

If you liked the article, consider subscribing to Cloudaffle, my YouTube Channel, where I keep posting in-depth tutorials and all edutainment stuff for ssoftware developers.

YouTube @cloudaffle