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 thatBaseServer
,ServerRequestDecorator
,LoggingMiddleware
, andAuthMiddleware
implement.BaseServer
is a basic server that can handle requests.ServerRequestDecorator
is an abstract decorator class that contains aServerRequest
object and defines thehandle()
method.LoggingMiddleware
andAuthMiddleware
are concrete decorator classes that add logging and authentication functionality, respectively. They each contain aServerRequest
(which could be another decorator or aBaseServer
) and define their ownhandle()
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.
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 DETAILSWhat 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.