Design patterns are general reusable solutions to common problems encountered in software design. Below are some common design patterns in JavaScript along with examples:
1. Singleton Pattern
Ensures that a class has only one instance and provides a global point of access to that instance.
const Singleton = (function () {
let instance;
function createInstance() {
return { key: 'value' };
}
return {
getInstance: function () {
if (!instance) {
instance = createInstance();
}
return instance;
}
};
})();
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();
console.log(instance1 === instance2); // true
2. Factory Pattern
Defines an interface for creating an object but lets subclasses alter the type of objects that will be created.
function CarFactory() {}
CarFactory.prototype.createCar = function (type) {
let car;
if (type === 'SUV') {
car = new SUV();
} else if (type === 'Sedan') {
car = new Sedan();
}
return car;
};
function SUV() { /* ... */ }
function Sedan() { /* ... */ }
const factory = new CarFactory();
const suv = factory.createCar('SUV');
3. Module Pattern
Encapsulates private data by exposing only certain properties and methods.
const MyModule = (function () {
let privateData = 42;
function privateFunction() {
return 'This is private!';
}
return {
publicMethod: function () {
return privateFunction() + ' But this is public!';
}
};
})();
console.log(MyModule.publicMethod()); // This is private! But this is public!
4. Observer Pattern
Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
class Subject {
constructor() {
this.observers = [];
}
addObserver(observer) {
this.observers.push(observer);
}
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class Observer {
update(data) {
console.log('Observer updated with data:', data);
}
}
const subject = new Subject();
const observer = new Observer();
subject.addObserver(observer);
subject.notify('Some data'); // Observer updated with data: Some data
5. Strategy Pattern
Defines a family of algorithms, encapsulates each one, and makes them interchangeable.
class StrategyA {
execute() {
return 'Strategy A';
}
}
class StrategyB {
execute() {
return 'Strategy B';
}
}
class Context {
constructor(strategy) {
this.strategy = strategy;
}
executeStrategy() {
return this.strategy.execute();
}
}
const strategyA = new StrategyA();
const contextA = new Context(strategyA);
console.log(contextA.executeStrategy()); // Strategy A
const strategyB = new StrategyB();
const contextB = new Context(strategyB);
console.log(contextB.executeStrategy()); // Strategy B
6. MVC Pattern
A pattern that separates an application into three interconnected components: Model, View, and Controller.
- Model: Represents the data and business logic.
- View: Represents the UI.
- Controller: Acts as an interface between Model and View.
Example implementations of MVC in JavaScript can be quite extensive as they typically involve DOM manipulation and potentially backend communication. Frameworks like Angular, React with Redux, or Backbone.js follow this pattern.
These patterns serve as a solid base for clean and maintainable code and are used in many modern JavaScript libraries and frameworks. Understanding them can improve the overall design and structure of your applications.
This example demonstrates a basic counter application, which allows you to increase or decrease the count.
Model
The Model represents the data and the business logic.
class Model {
constructor() {
this.count = 0;
this.observers = [];
}
increment() {
this.count++;
this.notifyObservers();
}
decrement() {
this.count--;
this.notifyObservers();
}
addObserver(observer) {
this.observers.push(observer);
}
notifyObservers() {
this.observers.forEach(observer => observer.update(this.count));
}
}
View
The View represents the UI and listens for changes to the Model.
class View {
constructor(model) {
this.model = model;
this.model.addObserver(this);
this.display();
}
update(count) {
this.display(count);
}
display(count = 0) {
const counterElement = document.getElementById('counter');
counterElement.textContent = `Counter: ${count}`;
}
}
Controller
The Controller acts as an interface between the Model and the View.
class Controller {
constructor(model, view) {
this.model = model;
this.view = view;
this.initialize();
}
initialize() {
document.getElementById('increment').addEventListener('click', () => {
this.model.increment();
});
document.getElementById('decrement').addEventListener('click', () => {
this.model.decrement();
});
}
}
HTML
This is the HTML code for the buttons and the display element.
<button id="increment">Increment</button>
<button id="decrement">Decrement</button>
<div id="counter">Counter: 0</div>
Initialization
You can connect everything together with the following code:
const model = new Model();
const view = new View(model);
const controller = new Controller(model, view);
Here, the Model is responsible for handling the data and the business logic, the View takes care of displaying the data, and the Controller handles user input and updates both the Model and the View.