Mastering Design Patterns: Essential Concepts and Real-World Frontend Examples
This article introduces design patterns, explains their origins and core principles, and demonstrates how common patterns such as Publish‑Subscribe, Strategy, Adapter, Proxy, Chain of Responsibility, and Singleton can be applied in frontend development with practical code examples and usage guidelines.
1. What Is a Design Pattern?
Before discussing design patterns, we need a basic understanding of the concept.
1.1 Definition
Design patterns are a set of reusable, general‑purpose solutions to common problems that arise during software design. They are not directly translatable into source code but represent best‑practice guidelines that developers should follow when architecting a system.
1.2 Origin of Design Patterns
In 1994, four authors (often called the "Gang of Four") published *Design Patterns – Elements of Reusable Object‑Oriented Software*, which introduced the concept of design patterns in software development. Their patterns are based on object‑oriented design principles such as programming to an interface rather than an implementation and preferring object composition over inheritance.
1.3 Design Patterns and Frontend Development
Some may wonder whether design patterns, being rooted in object‑oriented programming, are irrelevant to JavaScript, which is not strictly class‑based. The answer is no—design patterns are fundamentally a programming mindset and can be applied regardless of whether the language is object‑oriented or procedural.
1.4 Design Pattern Principles
There are many ways to describe pattern principles; here we briefly reference the SOLID principles.
2. Common Frontend Design Patterns
2.1 Publish‑Subscribe Pattern
Definition: Publish/Subscribe is a messaging paradigm where the sender (publisher) does not send messages directly to specific receivers (subscribers). Instead, messages are categorized and delivered to all subscribers of the relevant category.
The publish‑subscribe pattern is one of the most frequently mentioned patterns in frontend development; many state‑management tools and component‑communication mechanisms rely on it.
It can be seen as an extension of the Observer pattern, adding an intermediary to decouple the subject from its observers.
Example:
Little Red and Little Ming both want milk.
In the Observer pattern, both call the milk station directly.
In the Publish‑Subscribe pattern, they call a broker; the broker contacts the milk station, allowing other drinks (e.g., juice) to be requested through the same broker without contacting the station again.
Publish‑Subscribe ↑
Observer ↑
Typical Use Cases
Event listening, event bus
When to Use Publish‑Subscribe
Use it when modules are independent, have one‑to‑many dependencies, unstable dependencies, or are developed by different teams.
Things to Watch
Increasing numbers of subscribed events can raise maintenance costs.
2.2 Strategy Pattern
Definition: The strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable.
It can be thought of as an upgraded if‑else structure where the algorithm is selected at runtime.
Example: Rendering operation buttons on a list page based on backend data.
// Assume operators returned from backend: ['edit','add','delete']
function renderOperators(operators) {
return operators.map(item => {
if (item === 'edit') {
// return ...
} else if (item === 'add') {
// return ...
} else if (item === 'delete') {
// return ...
} else {
return null;
}
})
}When requirements change, adding new branches makes the function bulky. Using the strategy pattern:
const strategies = {
edit: function(key) { /* do something */ },
add: function(key) { /* do something */ },
delete: function(key) { /* do something */ }
};
function renderOperators(operators) {
return operators.map(item => strategies[item]());
}This adheres to the Open/Closed principle.
Typical Use Cases
Multiple login methods, form validation, dynamic UI rendering
When to Use Strategy
When each conditional branch is independent, complex, and may need flexible composition.
Things to Watch
Callers must understand the differences between strategies to choose the appropriate one.
2.3 Adapter Pattern
Definition: The adapter pattern converts one interface into another that a client expects, allowing otherwise incompatible classes to work together.
Example: Reusing a coupon component from a CRM system in a storefront by adapting the backend data format to match the component’s expected input.
Typical Use Cases
Interface data conversion
2.4 Proxy Pattern
Definition: The proxy pattern provides a surrogate object that controls access to another object, while the actual work is performed by the original object.
Example: Email sending with a proxy that filters certain domains before delegating to the real send function.
const emailList = ['qq.com', '163.com', 'weimob.com'];
const ProxyEmail = function(email) {
if (emailList.includes(email)) {
// block
} else {
SendEmail.call(this, email);
}
};
const SendEmail = function(email) {
// send email
};
ProxyEmail('cctv.com');
ProxyEmail('ojbk.com');Another example: wrapping console.log so that logs only appear when a specific query parameter is present.
function myLog(...args) {
if (!location.search.includes('xxx')) {
return;
}
console.log(...args);
}Typical Use Cases
Image preloading, data caching, request interceptors
When to Use Proxy
Single‑purpose modules that need controlled access
Extending behavior of a method without modifying the original
Enforcing interaction constraints between modules
Things to Watch
Additional indirection can affect performance
Proxy changes the interface indirectly; it does not alter the original API
Unlike decorators, proxies aim to control access rather than add functionality
2.5 Chain of Responsibility Pattern
Definition: The chain of responsibility pattern gives multiple objects a chance to handle a request, decoupling the sender from the receiver.
Example: Device application flow where address selection, verifier selection, and inventory check must occur sequentially, each step passing data to the next.
const Chain = function(fn) {
this.fn = fn;
this.next = null;
this.setNext = function(chain) {
this.next = chain;
return this.next;
};
this.run = function(...args) {
const nextData = this.fn(...args);
this.next?.run?.(nextData);
};
};
const applyDevice = function(data) {};
const chainApplyDevice = new Chain(applyDevice);
const selectAddress = function(data) {};
const chainSelectAddress = new Chain(selectAddress);
const selectChecker = function(data) {};
const chainSelectChecker = new Chain(selectChecker);
chainApplyDevice.setNext(chainSelectAddress).setNext(chainSelectChecker);
chainApplyDevice.run();Typical Use Cases
JavaScript event bubbling
Koa’s onion middleware model
Webpack plugins
Things to Watch
Debugging can be harder due to indirect calls
Improper configuration may leave requests unhandled
2.6 Singleton Pattern
Definition: The singleton pattern ensures a class has only one instance and provides a global access point to it.
Two JavaScript implementations are shown: a class‑based static method and a closure‑based function.
// Class version
class Singleton {
constructor(name) { this.name = name; }
static getInstance(name) {
if (!this.instance) { this.instance = new Singleton(name); }
return this.instance;
}
}
const a = Singleton.getInstance('a1');
const b = Singleton.getInstance('b2');
console.log(a == b); // true // Closure version
const Singleton = function(name) { this.name = name; };
Singleton.getInstance = (function() {
let instance;
return function(name) {
if (!instance) { instance = new Singleton(name); }
return instance;
};
})();
const a = Singleton.getInstance('a1');
const b = Singleton.getInstance('b2');
console.log(a === b); // trueIn JavaScript, a plain object can serve as a singleton:
const single = {};Typical Use Cases
Shared utilities
Global modal dialogs
State‑management stores such as Vuex or Redux
Things to Watch
Avoid polluting the global namespace
Developers unfamiliar with the singleton may find debugging difficult
3. Summary
After learning design patterns, we realize they are ubiquitous. Previously, we relied on experience; many of those heuristics align with established patterns.
Design patterns are language‑agnostic tools that improve code robustness and maintainability, but they should be applied judiciously. Blindly forcing patterns can lead to over‑engineered, hard‑to‑maintain code.
Understanding the context and limitations of each pattern enables better architectural decisions.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
