Frontend Development 19 min read

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.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
Master Frontend Design Patterns: From Factory to Observer with Real 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:

Design pattern classification
Design pattern classification

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.

Closing illustration
Closing illustration
design patternsFrontendsoftware architectureJavaScriptbehavioralcreationalstructural
WeDoctor Frontend Technology
Written by

WeDoctor Frontend Technology

Official WeDoctor Group frontend public account, sharing original tech articles, events, job postings, and occasional daily updates from our tech team.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.