Design Pattern thumbrules or different concepts using which you can solve the problem of modelling real world examples into object oriented design.
Design Patterns as Thumb Rules:
Factory Pattern
Abstract Factory Pattern
Singleton Pattern
Strategy Pattern
Observer Pattern
Decorator Pattern
It is creational pattern that provides an interface for creating instances of a class, with its subclasses deciding which class to instantiate.
It is Factory of Factory. when we have different object but we can group them in any way.
It is Class Creational design pattern. Singleton means we will create single instance of a class. Make sure that there is no other instance created for this class.
The Observer pattern is a behavioral design pattern where an object, known as the subject, maintains a list of its dependents, known as observers, that are notified of any changes in the subject's state. This pattern is commonly used to implement distributed event handling systems.
The strategy pattern is a behavioral design pattern that enables selecting an algorithm at runtime. Instead of implementing a single algorithm directly, code receives run-time instructions as to which in a family of algorithms to use.
Strategy pattern, we are trying to separate out dependency of drive from the main class as much as possible by making various strategies to inject it dynamically based on client requirement so that in future if new requirement for drive comes, there will be minimal/no changes in existing code(Open closed Principle).
// With Strategy Pattern
class VehicleWithStrategy {
constructor(make, model, driveStrategy) {
this.make = make;
this.model = model;
this.driveStrategy = driveStrategy;
}
setDriveStrategy(driveStrategy) {
this.driveStrategy = driveStrategy;
}
drive(speed) {
this.driveStrategy.drive(speed, this.make);
}
}
// Strategies
class CarDriveStrategy {
drive(speed, make) {
console.log(`Driving the ${make} at ${speed} km/h.`);
}
}
class MotorcycleDriveStrategy {
drive(speed, make) {
console.log(`Riding the ${make} at ${speed} km/h.`);
}
}
// Usage with Strategy Pattern
const carWithStrategy = new VehicleWithStrategy('Car', 'Sedan', new CarDriveStrategy());
carWithStrategy.drive(120);
const motorcycleWithStrategy = new VehicleWithStrategy('Motorcycle', 'Sport', new MotorcycleDriveStrategy());
motorcycleWithStrategy.drive(80);
// Changing strategy dynamically
carWithStrategy.setDriveStrategy(new MotorcycleDriveStrategy());
carWithStrategy.drive(90);
The Builder Pattern is a creational design pattern that is used to construct a complex object step by step. This pattern is particularly useful when an object needs to be created with many optional components or configurations.
Example
Let's consider a scenario where we need to create a complex object, such as a Car, with many optional components and configurations. Without the Builder Pattern, you might end up with a constructor that has numerous parameters, making it hard to read, understand, and use correctly. Additionally, not all components are required, and providing default values for each optional component in the constructor could lead to a bloated and inflexible design.
In this example, creating a Car object requires specifying many parameters, including optional ones. This can lead to confusion about the order of parameters and may result in errors if the wrong values are provided. Additionally, it doesn't provide a clear way to create a Car without specifying all optional components.
With the Builder Pattern, you can create a Car instance step by step. You only set the components you care about, and the builder provides a clean and readable API for constructing the object. This makes the code more maintainable, and it's easier to understand which components are being configured without the need for a long parameter list. Additionally, it allows for a more flexible and extensible design, as new components or configurations can be added to the builder without modifying the product's class.
// without builder pattern
class Car {
constructor(name, color, year, bluetooth, sunroof, rare_camera, front_camera) {
this.name = name;
this.color = color;
this.year = year;
this.bluetooth = bluetooth;
this.sunroof = sunroof;
this.rare_camera = rare_camera;
this.front_camera = front_camera;
}
describe() {
console.log(`${this.name} ${this.color} ${this.year}`)
}
}
let thar1 = new Car('thar', 'black', 2024);
let thar2 = new Car('thar', 'black', 2024, false, true, true, false);
console.log(thar1)
console.log(thar2)
// with builder pattern
class CarBuilder {
constructor(name, color, year) {
this.car = new Car(name, color, year)
}
addBluetooth() {
this.car.bluetooth = true
return this;
}
addSunroof() {
this.car.sunroof = true
return this;
}
addRareCamera() {
this.car.rare_camera = true;
return this;
}
addFrontCamera() {
this.car.front_camera = true;
return this;
}
build() {
return this.car;
}
}
let ertiga = new CarBuilder('ertiga', 'black', 2024).addRareCamera().addSunroof().build();
console.log(ertiga)
The Decorator Pattern is a structural design pattern that enables the dynamic augmentation of an object's behavior, either at compile-time or runtime, without altering the behavior of other instances of the same class. This pattern is particularly useful when there is a need to extend the functionality of objects in a flexible and reusable manner.
The structure of the Decorator Pattern involves a component interface defining the base functionality, concrete components implementing this interface, decorators that conform to the same interface and enhance the behavior of the components, and a client that interacts with the decorated objects. Decorators can be stacked or combined to add multiple functionalities to an object, allowing for a more modular and extensible design without resorting to a multitude of subclassing.
This pattern promotes code flexibility and maintainability by providing a means to dynamically tailor the behavior of objects without the need for complex class hierarchies.
// Problematic implementation without Decorator Pattern
class Coffee {
constructor() {
this.description = 'simple coffee',
this.cost = 5;
}
getCost() { return this.cost; }
getDescription() { return this.description; }
}
class CoffeeWithMilk extends Coffee {
constructor() {
super();
this.description = 'coffee with milk';
this.cost = 7;
}
getCost() { return this.cost; }
getDescription() { return this.description; }
}
class CoffeeWithSugar extends Coffee {
constructor() {
super();
this.description = 'coffee with sugar';
this.cost = 10;
}
getCost() { return this.cost; }
getDescription() { return this.description; }
}
class MilkDecorator {
constructor(coffee) {
this.coffee = coffee;
}
getCost() {
return this.coffee.getCost() + 2;
}
getDescription() {
return `${this.coffee.getDescription()} with milk`;
}
}
const simpleCoffee = new Coffee;
const milkCoffee = new MilkDecorator(new Coffee());
console.log(milkCoffee.getCost())
The Adapter Pattern is a structural design pattern that allows incompatible interfaces to work together. It acts as a bridge between two incompatible interfaces, making them compatible without altering their source code.
It is used when you have existing code or components with interfaces that don't match the requirements, and you want them to work together seamlessly.
// Existing XML to JSON converter with an incompatible interface
class XMLtoJSONConverter {
convert(xmlData) {
// Simulating the existing XML to JSON conversion logic
console.log('Converting XML to JSON:', xmlData);
const jsonData = { result: 'Converted JSON Data' };
return jsonData;
}
}
// New interface expected by the application
class JSONConverter {
convertJSON(jsonData) {
console.log('Displaying JSON Data:', jsonData);
}
}
// Adapter to make XMLtoJSONConverter compatible with JSONConverter
class XMLtoJSONConverterAdapter extends JSONConverter {
constructor(xmlToJSONConverter) {
super();
this.xmlToJSONConverter = xmlToJSONConverter;
}
convertJSON() {
const xmlData = 'XML Data '; // Simulated XML data
const jsonData = this.xmlToJSONConverter.convert(xmlData);
super.convertJSON(jsonData);
}
}
// Usage with Adapter
const xmlToJSONConverter = new XMLtoJSONConverter();
const xmlToJSONConverterAdapter = new XMLtoJSONConverterAdapter(xmlToJSONConverter);
// Now, the XMLtoJSONConverter can be used seamlessly with the JSONConverter interface
xmlToJSONConverterAdapter.convertJSON();
Facade pattern hides the complexities of the system and provides an interface to the client using which the client can access the system. This type of design pattern comes under structural pattern as this pattern adds an interface to existing system to hide its complexities.
This pattern involves a single class which provides simplified methods required by client and delegates calls to methods of existing system classes.
// Interface: Shape
class Shape {
draw() {
console.log("Drawing a shape");
}
}
// Concrete Class: Rectangle
class Rectangle extends Shape {
draw() {
console.log("Drawing a Rectangle");
}
}
// Concrete Class: Square
class Square extends Shape {
draw() {
console.log("Drawing a Square");
}
}
// Concrete Class: Circle
class Circle extends Shape {
draw() {
console.log("Drawing a Circle");
}
}
// Facade: ShapeMaker
class ShapeMaker {
constructor() {
this.circle = new Circle();
this.rectangle = new Rectangle();
this.square = new Square();
}
drawCircle() {
this.circle.draw();
}
drawRectangle() {
this.rectangle.draw();
}
drawSquare() {
this.square.draw();
}
}
// FacadePatternDemo
const shapeMaker = new ShapeMaker();
shapeMaker.drawCircle();
shapeMaker.drawRectangle();
shapeMaker.drawSquare();