Entwurfsmuster in JavaScript (Node.js): Eine ausführliche Anleitung
What is it and why? (a real-life analogy)
Imagine you are building a house. You could reinvent the wheel every time: decide for yourself where the windows will be, how to attach the roof, what shape to make the doors. But this is slow, expensive, and often leads to mistakes. It is much smarter to use proven architectural solutions: for example, make the foundation strip-type and the roof gabled. These solutions have been tested thousands of times, they are reliable and understandable to other builders.
Design Patterns are the same proven "architectural blueprints" for programmers. This is not ready-made code, but a solution template for a typical problem that arises again and again when creating programs.
A pattern is not a library you can download. It is an instruction: "If you have problem X, do Y, and here is why it will work."
Why are they needed? Imagine you are writing a game. You need there to be only one main character (you cannot create two "Players"). Or you need different parts of the program to learn about events (for example, that a monster died) without interfering with each other. Patterns provide ready-made, elegant, and understandable solutions for such situations.
In the world of JavaScript (especially Node.js), patterns help make code:
- Understandable — another developer will immediately grasp your intent.
- Flexible — easy to add new features without breaking old ones.
- Reliable — less chance of making a silly mistake.
Theory: how it works internally
In programming, patterns are divided into three large groups. Imagine these are three different sets of tools:
| Group | What does it do? | Real-life example |
|---|---|---|
| Creational | Responsible for creating objects. They help make this process flexible and controllable. | Like a car factory: you don't assemble the engine by hand, you just say "give me a sedan". |
| Structural | Define how classes and objects are assembled into larger structures. | Like a LEGO set: individual bricks (objects) connect to each other to form a castle. |
| Behavioral | Manage the interaction and communication between objects. | Like an air traffic controller: they prevent planes from colliding and coordinate their actions. |
How does it work "under the hood"? A pattern is always a set of rules. For example, the "Singleton" pattern says: "A class must have only one instance, and you must provide a global access point to it." In code, this is implemented through a check: if the object already exists — return it, if not — create a new one. Simple, but ingenious.
It is important to understand: patterns are not tied to a specific language. You can implement "Observer" in JavaScript, Python, and C++. But the syntax will be different. JavaScript (especially in Node.js) is very flexible: functions are objects, objects can be created on the fly. This makes implementing some patterns incredibly elegant.
Syntax in JavaScript (Node.js): all forms and variants
JavaScript does not have the implements or abstract keywords like Java. But that's not a problem. We use the capabilities of the language itself: objects, functions, classes (introduced in ES6), closures, and prototypes.
Let's look at three most popular patterns that you will encounter in every second Node.js project.
1. Singleton
Guarantees that a class has only one instance. In Node.js, this is often used for database connections (to avoid creating 100500 connections).
// Variant 1: Via class (classic)
class Database {
constructor() {
// If the instance already exists, return it
if (Database.instance) {
return Database.instance;
}
// Otherwise create a new one
this.connection = 'Database connection established';
Database.instance = this; // Save the reference
}
query(sql) {
console.log(`Executing query: ${sql}`);
}
}
// Verification
const db1 = new Database();
const db2 = new Database();
console.log(db1 === db2); // true! It is the same object
// Variant 2: Via module (most popular in Node.js)
// In Node.js, modules are cached. If you export an object,
// it will be created once on the first require.
class Logger {
log(message) {
console.log(`[Log]: ${message}`);
}
}
// Create an instance right here and export it
module.exports = new Logger();
// Now in any file where you write const logger = require('./logger');
// you will get the same object.
2. Factory
Allows creating objects without specifying a concrete class. Instead of new Car() you say factory.create('car').
// Factory for creating different types of notifications
class EmailNotification {
send(message) {
console.log(`Sending email: ${message}`);
}
}
class SMSNotification {
send(message) {
console.log(`Sending SMS: ${message}`);
}
}
class PushNotification {
send(message) {
console.log(`Sending Push: ${message}`);
}
}
// The factory itself
class NotificationFactory {
create(type) {
switch (type) {
case 'email':
return new EmailNotification();
case 'sms':
return new SMSNotification();