Master Frontend Design Patterns: From Factory to Observer with Real Code
This article introduces common frontend design patterns—creational, structural, and behavioral—explains the SOLID principles behind them, and provides clear JavaScript examples for each pattern, helping developers understand when and how to apply them for more maintainable code.
Wang Jun, a frontend engineer at WeDoctor cloud services, likens programming patterns to recipes. Design patterns encapsulate change and are guided by the SOLID principles, which can be summed up as “high cohesion, low coupling”.
The five SOLID principles are Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion, which the author simplifies into six Chinese characters representing high, internal, aggregation, low, coupling, and composition.
There are 23 design patterns, divided into Creational, Structural, and Behavioral categories, as illustrated below:
The article then walks through representative patterns under each category.
Creational
Factory Pattern
Factory patterns create objects through a unified interface. Simple Factory, Factory Method, and Abstract Factory are shown with JavaScript classes.
<code>class Factory {
constructor(username, pwd, role) {
this.username = username;
this.pwd = pwd;
this.role = role;
}
}
class CreateRoleFactory {
static create(username, pwd, role) {
return new Factory(username, pwd, role);
}
}
const admin = CreateRoleFactory.create('张三', '222', 'admin');</code>When simple factories cannot meet varied role requirements, Factory Method pushes object creation to subclasses.
<code>class User {
constructor(name, menuAuth) {
if (new.target === User) throw new Error('User cannot be instantiated');
this.name = name;
this.menuAuth = menuAuth;
}
}
class UserFactory extends User {
constructor(...props) { super(...props); }
static create(role) {
const roleCollection = new Map([
['admin', () => new UserFactory('管理员', ['首页', '个人中心'])],
['user', () => new UserFactory('普通用户', ['首页'])]
]);
return roleCollection.get(role)();
}
}
const admin = UserFactory.create('admin');
const user = UserFactory.create('user');</code>If business scenarios span multiple platforms, Abstract Factory creates families of related objects.
<code>class User {
constructor(hospital) {
if (new.target === User) throw new Error('Abstract class cannot be instantiated!');
this.hospital = hospital;
}
}
class ZheYiUser extends User {
constructor(name, departmentsAuth) {
super('zheyi_hospital');
this.name = name;
this.departmentsAuth = departmentsAuth;
}
}
class XiaoShanUser extends User {
constructor(name, departmentsAuth) {
super('xiaoshan_hospital');
this.name = name;
this.departmentsAuth = departmentsAuth;
}
}
const getAbstractUserFactory = (hospital) => {
switch (hospital) {
case 'zheyi_hospital': return ZheYiUser;
case 'xiaoshan_hospital': return XiaoShanUser;
}
};
const ZheYiUserClass = getAbstractUserFactory('zheyi_hospital');
const XiaoShanUserClass = getAbstractUserFactory('xiaoshan_hospital');
const user1 = new ZheYiUserClass('王医生', ['外科', '骨科', '神经外科']);
const user2 = new XiaoShanUserClass('王医生', ['外科', '骨科']);</code>Summary: Constructors are separated from object creation, satisfying the Open/Closed principle.
Use case: Generating different user roles based on permissions.
Singleton Pattern
The Singleton ensures a class has only one instance and provides a global access point. Two implementations are shown: lazy initialization and eager initialization.
<code>class Single {
static getInstance() {
if (!Single.instance) {
Single.instance = new Single();
}
return Single.instance;
}
}
const test1 = Single.getInstance();
const test2 = Single.getInstance();
console.log(test1 === test2); // true</code> <code>class Single {
static instance = new Single();
static getInstance() { return Single.instance; }
}
const test1 = Single.getInstance();
const test2 = Single.getInstance();
console.log(test1 === test2); // true</code>Summary: Returns the existing instance directly, adhering to the Open/Closed principle.
Use case: State management tools like Redux or Vuex, global objects, caching.
Prototype Pattern
Prototype creates new objects by cloning existing ones, useful when many objects share similar structure.
<code>const user = { name: 'zhangsan', age: 18 };
let userOne = Object.create(user);
console.log(userOne.__proto__); // {name: "zhangsan", age: 18}
class User {
constructor(name) { this.name = name; }
getName() { return this.name; }
}
class Admin extends User {
constructor(name) { super(name); }
setName(_name) { this.name = _name; }
}
const admin = new Admin('zhangsan');
console.log(admin.getName());
admin.setName('lisi');
console.log(admin.getName());</code>Summary: Simple cloning via Object.create() or class inheritance.
Use case: When a new object differs little from an existing one, reducing creation cost.
Structural
Decorator Pattern
Decorator separates core objects from additional responsibilities. In JavaScript, higher‑order functions illustrate this concept, and React higher‑order components (HOC) are a practical example.
<code>const add = (x, y, f) => f(x) + f(y);
const num = add(2, -2, Math.abs);
console.log(num); // 4
</code> <code>import React from 'react';
const BgHOC = WrappedComponent => class extends React.Component {
render() {
return (
<div style={{ background: 'blue' }}>
<WrappedComponent />
</div>
);
}
};
</code>Summary: Decorator separates object and wrapper, following Open/Closed and Single Responsibility.
Use case: ES7 decorators, Vue mixins, core‑decorators.
Adapter Pattern
Adapter resolves interface incompatibility. The example shows Axios choosing different adapters for Node and browser environments.
<code>function getDefaultAdapter() {
var adapter;
if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
// Node environment – use http adapter
adapter = require('./adapters/http');
} else if (typeof XMLHttpRequest !== 'undefined') {
// Browser – use xhr adapter
adapter = require('./adapters/xhr');
}
return adapter;
}
</code>Summary: Unifies interface, parameters, and return values without changing existing code.
Use case: Compatibility layers, cross‑environment libraries.
Proxy Pattern
Proxy controls access to an object. The article demonstrates a virtual image‑preload proxy and a caching proxy for expensive calculations.
<code>class ProxyImg {
constructor(imgEle) {
this.imgEle = imgEle;
this.DEFAULT_URL = 'xxx';
}
setUrl(targetUrl) {
this.imgEle.src = this.DEFAULT_URL;
const image = new Image();
image.onload = () => { this.imgEle.src = targetUrl; };
image.src = targetUrl;
}
}
</code> <code>const countSum = (...arg) => {
console.log('count...');
let result = 0;
arg.forEach(v => result += v);
return result;
};
const proxyCountSum = (() => {
const cache = {};
return (...arg) => {
const args = arg.join(',');
if (args in cache) return cache[args];
return cache[args] = countSum(...arg);
};
})();
proxyCountSum(1,2,3,4); // 10
proxyCountSum(1,2,3,4); // 10 (cached)
</code>Summary: Extends functionality via proxy while keeping original code closed.
Use case: Image preloading, caching, request interception.
Behavioral
Strategy Pattern
Strategy encapsulates algorithms and makes them interchangeable. The article replaces a series of if‑else discount calculations with a Map of functions.
<code>const activity = new Map([
['pre', price => price * 0.95],
['onSale', price => price * 0.9],
['back', price => price * 0.85],
['limit', price => price * 0.8]
]);
const getActivityPrice = (type, price) => activity.get(type)(price);
// Add a new rule
activity.set('newcomer', price => price * 0.7);
</code>Summary: Algorithms are encapsulated and can be swapped, satisfying Open/Closed.
Use case: Form validation, complex conditional logic, refactoring.
Observer Pattern
Observer (publish‑subscribe) defines a one‑to‑many dependency so that when the subject changes, all observers are notified. The example models a product manager notifying frontend and backend developers.
<code>class Publisher {
constructor() { this.observers = []; this.prdState = null; }
add(observer) { this.observers.push(observer); }
notify() { this.observers.forEach(observer => observer.update(this)); }
getState() { return this.prdState; }
setState(state) { this.prdState = state; this.notify(); }
}
class Observer {
constructor() { this.prdState = {}; }
update(publisher) { this.prdState = publisher.getState(); this.work(); }
work() { console.log(this.prdState); }
}
const wang = new Observer();
const zhang = new Observer();
const zeng = new Publisher();
const prd = { url: 'xxxxxxx' };
zeng.add(wang);
zeng.add(zhang);
zeng.setState(prd);
</code>In Vue, an EventBus acts as a central hub for publish‑subscribe communication.
<code>import Vue from 'vue';
const EventBus = new Vue();
Vue.prototype.$bus = EventBus;
// Subscribe
this.$bus.$on('testEvent', func);
// Emit
this.$bus.$emit('testEvent', params);
</code>Summary: Decouples components through a central event system, adhering to Open/Closed.
Use case: Cross‑component communication, event handling.
Iterator Pattern
Iterator provides a uniform way to traverse a collection without exposing its internal structure. Both ES6 built‑in iterators and a manual ES5 implementation are shown.
<code>(function(a, b, c) {
const arg = arguments;
const iterator = arg[Symbol.iterator]();
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
})(1, 2, 3);
</code> <code>function iteratorGenerator(list) {
var index = 0;
var len = list.length;
return {
next: function() {
var done = index >= len;
var value = !done ? list[index++] : undefined;
return { done: done, value: value };
}
};
}
var iterator = iteratorGenerator([1, 2, 3]);
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: undefined, done: true}
</code>Summary: Provides a unified traversal interface, complying with Single Responsibility and Open/Closed.
Use case: Any scenario requiring iteration over a collection.
Conclusion
Design patterns are abstract and scattered; understanding each example is easy, but applying them in real projects can be challenging. Practicing them in business development helps discover their usefulness. This article covered nine common frontend design patterns, all centered on “encapsulating change” to improve code readability, extensibility, and maintainability. Keeping this mindset in daily work lets developers experience the essence of design patterns.
WeDoctor Frontend Technology
Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.
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.