2022-08-27
Design patterns are common solutions to recurring problems in software development. They are not specific pieces of code that we copy and paste, but rather templates or guidelines that we can adapt to our own situation. Knowing a few patterns has helped me write better structured code.
The singleton pattern ensures that a class has only one instance throughout the application. This is useful for things like database connections or configuration managers, where we want to reuse the same instance everywhere instead of creating new ones.
In JavaScript, a simple way to achieve this is by using a module. When we export an object from a module, every file that imports it gets the same reference.
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
};
export default config;The observer pattern is about one object (the subject) maintaining a list of dependents (observers) and notifying them when its state changes. This pattern is actually everywhere in frontend development. Event listeners in the browser follow this pattern. React's state and re-rendering mechanism is also based on a similar idea. When state changes, all components that depend on that state get notified and re-render.
The factory pattern provides a way to create objects without specifying the exact class or constructor to use. Instead of calling new directly, we call a factory function that decides which object to create based on the input.
function createNotification(type, message) {
if (type === "email") {
return new EmailNotification(message);
}
if (type === "sms") {
return new SmsNotification(message);
}
return new PushNotification(message);
}This makes it easy to add new notification types later without changing the code that calls the factory.
The strategy pattern lets us define a family of algorithms, put each one in a separate function or class, and make them interchangeable. For example, if we have different ways to calculate shipping cost based on the shipping method:
const shippingStrategies = {
standard: (weight) => weight * 1.5,
express: (weight) => weight * 3.0,
overnight: (weight) => weight * 5.0,
};
function calculateShipping(method, weight) {
return shippingStrategies[method](weight);
}Instead of having a big if-else chain inside the function, we delegate the calculation to the appropriate strategy.
It is important not to force patterns into our code. If the code is simple and straightforward, adding a pattern on top of it can make it unnecessarily complex. Patterns are most useful when we notice a recurring problem or when the code starts to get difficult to maintain. Knowing the common patterns helps us recognise these situations and apply the right solution.