Fundamentals 28 min read

Master Design Patterns: Boost Code Reusability, Readability, and Maintainability

This comprehensive guide explains what design patterns are, why they matter, the fundamentals of object‑oriented programming, ten core SOLID principles, a mnemonic for 23 patterns, and detailed examples of creational, structural, and behavioral patterns with real‑world code snippets to help developers write cleaner, more maintainable software.

WeDoctor Frontend Technology
WeDoctor Frontend Technology
WeDoctor Frontend Technology
Master Design Patterns: Boost Code Reusability, Readability, and Maintainability

What problems do design patterns solve?

Design patterns are reusable solutions to specific problems, summarizing experienced developers' design knowledge; they aim to improve code reusability, readability, and maintainability, embodying object‑oriented design principles and preventing the need to reinvent the wheel.

What is object‑oriented programming?

Object‑Oriented Programming

is a programming paradigm that uses classes or objects as the basic unit of code organization, with encapsulation, abstraction, inheritance, and polymorphism as its four foundational traits. Object‑Oriented Programming Language provides syntax for classes/objects and built‑in mechanisms to implement the four traits. Object‑Oriented Development includes analysis (OOA), design (OOD), and programming (OOP).

10 Major Design Principles (SOLID + Others)

1. Single Responsibility Principle (SRP)

Implement classes with a single responsibility

: if a code block (function, class, module) handles multiple responsibilities, a change in one may break the other; therefore each block should have only one responsibility.

2. Open‑Closed Principle (OCP)

Open for extension, closed for modification

: modifying existing code to add new features can introduce bugs; instead, extend by adding new code.

3. Liskov Substitution Principle (LSP)

Do not break the inheritance hierarchy

: subclasses should be replaceable for their parent class without altering expected behavior.

4. Interface Segregation Principle (ISP)

Design interfaces to be simple and focused

: if a class needs only part of an interface, the unnecessary methods should be split into separate interfaces.

5. Dependency Inversion Principle (DIP)

Program to interfaces, not implementations

: high‑level modules should depend on abstractions, not concrete classes.

6. Least Knowledge Principle (Law of Demeter, LOD)

Reduce coupling

: a class should know as little as possible about other classes, communicating only with its direct friends.

7. Composition over Inheritance (CRP)

Prefer composition, use inheritance sparingly

: build new functionality by composing existing objects rather than inheriting.

8. Don't Repeat Yourself (DRY)

Merge duplicated semantics; remove duplicated execution. Duplicate logic with different semantics should be kept.

9. Keep It Simple, Stupid (KISS)

Prefer simple, highly readable code over complex, hard‑to‑understand implementations.

10. You Aren't Gonna Need It (YAGNI)

Avoid over‑optimizing, over‑designing, or adding features that are not currently needed.

23 Design Patterns Mnemonic

Creational: 5 patterns – Abstract Factory, Factory, Singleton, Builder, Prototype

Structural: 7 patterns – Bridge, Proxy, Adapter, Decorator, Composite, Facade, Flyweight

Behavioral: 11 patterns – Observer, Template, Strategy, Chain of Responsibility, State, Iterator, Visitor, Memento, Command, Interpreter, Mediator

Creational Patterns

Singleton

Application Scenarios

Handle resource access conflicts; create a globally unique class.

Solutions

Lazy (create when needed)

Eager (create at system startup)

Multiton (fixed number of instances)

Factory Method

Application Scenarios

Create objects that share a common parent class or interface based on a given type parameter.

Solution

enum HelloType { A, B }
interface Hello { sayHello() }
class A implements Hello { sayHello() { console.log('A'); } }
class B implements Hello { sayHello() { console.log('B'); } }
class HelloFactory {
  static list = new Map<HelloType, Hello>([
    [HelloType.A, new A()],
    [HelloType.B, new B()]
  ]);
  static getHello(type: HelloType) { return HelloFactory.list.get(type); }
}
// test
HelloFactory.getHello(HelloType.A).sayHello();
HelloFactory.getHello(HelloType.B).sayHello();

Abstract Factory

Application Scenarios

Create objects of multiple related types (different subclasses of a common parent) based on given parameters.

Solution

enum Type { A, B }
enum Occupation { TEACHER, STUDENT }
interface Hello { sayHello() }
class TA implements Hello { sayHello() { console.log('Teacher A say hello'); } }
class TB implements Hello { sayHello() { console.log('Teacher B say hello'); } }
class SA implements Hello { sayHello() { console.log('Student A say hello'); } }
class SB implements Hello { sayHello() { console.log('Student B say hello'); } }
class AFactory { static list = new Map<Occupation, Hello>([[Occupation.TEACHER, new TA()], [Occupation.STUDENT, new SA()]]); static getHello(o: Occupation) { return AFactory.list.get(o); } }
class BFactory { static list = new Map<Occupation, Hello>([[Occupation.TEACHER, new TB()], [Occupation.STUDENT, new SB()]]); static getHello(o: Occupation) { return BFactory.list.get(o); } }
class HelloFactory { static list = new Map<Type, AFactory | BFactory>([[Type.A, AFactory], [Type.B, BFactory]]); static getType(t: Type) { return HelloFactory.list.get(t); } }
// test
HelloFactory.getType(Type.A).getHello(Occupation.TEACHER).sayHello();
HelloFactory.getType(Type.A).getHello(Occupation.STUDENT).sayHello();
HelloFactory.getType(Type.B).getHello(Occupation.TEACHER).sayHello();
HelloFactory.getType(Type.B).getHello(Occupation.STUDENT).sayHello();

Builder

Application Scenarios

Many required parameters need validation.

Parameters have ordering or inter‑dependency.

Object creation involves multiple steps that must all succeed.

Solution

class Programmer { constructor(p) { this.age = p.age; this.username = p.username; this.color = p.color; this.area = p.area; } toString() { console.log(this); } }
class Builder {
  constructor() { this.age = null; this.username = null; this.color = null; this.area = null; }
  setAge(age) { if (age > 18 && age < 36) { this.age = age; return this; } else { throw new Error('Invalid age'); } }
  setUsername(username) { if (username !== '小明') { this.username = username; return this; } else { throw new Error('Invalid username'); } }
  setColor(color) { if (color !== 'yellow') { this.color = color; return this; } else { throw new Error('Invalid color'); } }
  setArea(area) { this.area = area; return this; }
  build() { if (this.age && this.username && this.color && this.area) { return new Programmer(this); } else { throw new Error('Missing information'); } }
}
// test
const p = new Builder().setAge(20).setUsername('小红').setColor('red').setArea('hz').build();
 p.toString();

Prototype

Application Scenarios

Clone existing objects instead of modifying the prototype chain.

Object creation is costly but instances share most data.

Immutable objects use shallow cloning; mutable objects may need deep cloning.

Versioned objects can be compared via shallow vs. deep clones.

Structural Patterns

These patterns describe classic class/object compositions that decouple structure from usage.

Bridge

Application Scenarios

Decouple abstraction from implementation so they can vary independently.

Multiple independent dimensions of variation can be combined via composition.

Follows the principle "favor composition over inheritance".

Solution

enum MsgLevel { ERROR, WARN }
enum MsgType { EMAIL, PHONE }
interface MsgContent { content() }
class ErrorMsg implements MsgContent { content() { return 'ERROR'; } }
class WarnMsg implements MsgContent { content() { return 'WARN'; } }
interface MsgSender { send() }
class PhoneSend implements MsgSender { constructor(msgContent) { this.msgContent = msgContent; } send() { console.log(`phone send ${this.msgContent.content()}`); } }
class EmailSend implements MsgSender { constructor(msgContent) { this.msgContent = msgContent; } send() { console.log(`email send ${this.msgContent.content()}`); } }
// test
new PhoneSend(new WarnMsg()).send();
new PhoneSend(new ErrorMsg()).send();
new EmailSend(new WarnMsg()).send();
new EmailSend(new ErrorMsg()).send();

Proxy

Application Scenarios

Add non‑functional requirements (monitoring, logging, auth, etc.) without changing the original class.

Solution

class User { login() { console.log('user login...'); } }
class UserProxy extends User { login() { console.log('login before'); super.login(); console.log('login after'); } }
// Interface‑based proxy (preferred)
class User implements Login { login() { console.log('user login...'); } }
class UserProxy implements Login { constructor() { this.user = new User(); } login() { console.log('login before'); this.user.login(); console.log('login after'); } }

Decorator

Application Scenarios

Enhance original functionality without altering the original class.

Multiple decorators can be stacked.

Replace complex inheritance hierarchies with composition.

Solution (AOP style)

Function.prototype.before = function (beforeFn) { return (...arg) => { beforeFn(...arg); return this(...arg); }; };
Function.prototype.after = function (afterFn) { return (...arg) => { const result = this(...arg); afterFn(...arg); return result; }; };
function ImportEvent1() { console.log('重要的事情说三遍 1'); }
function ImportEvent2() { console.log('重要的事情说三遍 2'); }
function ImportEvent3() { console.log('重要的事情说三遍 3'); }
// test
ImportEvent2.before(ImportEvent1).after(ImportEvent3)();

Adapter

Application Scenarios

Make incompatible interfaces compatible.

Wrap flawed interfaces.

Unify multiple class interfaces.

Replace external systems.

Support legacy interfaces.

Adapt different data formats.

Solution

// Target interface
interface ITarget { f1(); f2(); f3(); }
// Incompatible existing class
class Origin { fa() {} fb() {} f3() {} }
// Adapter using class inheritance
class Adaptor implements ITarget { constructor() { this.origin = new Origin(); } f1() { this.origin.fa(); } f2() { this.origin.fb(); } f3() { this.origin.f3(); } }

Flyweight

Application Scenarios

Share immutable objects to save memory (e.g., chess pieces in many rooms).

Composite

Application Scenarios

Organize objects into a tree structure to represent "part‑whole" hierarchies, allowing clients to treat individual and composite objects uniformly.

Solution

abstract class FileSystemNode { path; abstract getFilesCount(); abstract getFilesSize(); }
class FileNode extends FileSystemNode { constructor(path) { super(); this.path = path; } getFilesCount() { return 1; } getFilesSize() { return require(this.path).length; } }
class Directory extends FileSystemNode { constructor(path) { super(); this.path = path; this.subNodes = []; } getFilesCount() { return this.subNodes.reduce((c, item) => c + item.getFilesCount(), 0); } getFilesSize() { return this.subNodes.reduce((s, item) => s + item.getFilesSize(), 0); } }

Facade

Application Scenarios

Combine multiple backend calls into a single interface to improve performance.

Provide a high‑level API that composes fine‑grained interfaces for easier use.

Behavioral Patterns

These patterns define classic ways for objects to interact, decoupling behavior from the objects themselves.

Observer

Application Scenarios

Decouple observers from the subject.

Unlike publish‑subscribe, there is no central broker.

Solution

class Subject { constructor() { this.observerList = []; } addObserver(o) { this.observerList.push(o); } notify() { this.observerList.forEach(observer => observer.update()); } }
class Observer { constructor(cb) { if (typeof cb !== 'function') throw new Error('Observer constructor requires a function'); this.cb = cb; } update() { this.cb(); } }
// test
const observerCallback = function () { console.log('I was notified'); };
const observer = new Observer(observerCallback);
const subject = new Subject();
subject.addObserver(observer);
subject.notify();

Template Method

Application Scenarios

Define the skeleton of an algorithm in a method, deferring some steps to subclasses.

Enable reuse and extension without altering the overall structure.

Solution

abstract class Drinks { firstStep() { console.log('烧开水'); } abstract secondStep(); thirdStep() { console.log('倒入杯子'); } abstract fourthStep(); drink() { this.firstStep(); this.secondStep(); this.thirdStep(); this.fourthStep(); } }
class Tea extends Drinks { secondStep() { console.log('浸泡茶叶'); } fourthStep() { console.log('加柠檬'); } }
class Coffee extends Drinks { secondStep() { console.log('冲泡咖啡'); } fourthStep() { console.log('加糖'); } }
// test
new Tea().drink();
new Coffee().drink();

Strategy

Application Scenarios

Define a family of algorithms, encapsulate each one, and make them interchangeable.

Avoid long if‑else or switch statements.

Solution

enum StrategyType { S, A, B }
const strategyFn = {
  'S': salary => salary * 4,
  'A': salary => salary * 3,
  'B': salary => salary * 2,
};
function calculateBonus(level, salary) { return strategyFn[level](salary); }
// example
calculateBonus(StrategyType.A, 10000); // 30000

Chain of Responsibility

Application Scenarios

Multiple handlers process the same request sequentially; the chain stops when a handler succeeds.

Used for filters, interceptors, processors.

Solution

const order500 = (orderType, pay, stock) => { if (orderType === 1 && pay) { console.log('500 元定金预购, 得 100 元优惠券'); return true; } return false; };
const order200 = (orderType, pay, stock) => { if (orderType === 2 && pay) { console.log('200 元定金预购, 得 50 元优惠券'); return true; } return false; };
const orderCommon = (orderType, pay, stock) => { if ((orderType === 3 || !pay) && stock > 0) { console.log('普通购买, 无优惠券'); return true; } console.log('库存不够, 无法购买'); return false; };
class Chain { constructor(fn) { this.fn = fn; this.nextFn = null; } setNext(nextFn) { this.nextFn = nextFn; } init(...args) { const result = this.fn(...args); if (!result && this.nextFn) { this.nextFn.init(...args); } } }
const order500New = new Chain(order500);
const order200New = new Chain(order200);
const orderCommonNew = new Chain(orderCommon);
order500New.setNext(order200New);
order200New.setNext(orderCommonNew);
order500New.init(3, true, 500); // 普通购买, 无优惠券

State

Application Scenarios

Encapsulate each possible state of an object into a class; state changes alter behavior.

Solution

class WeakLight { constructor(light) { this.light = light; } press() { console.log('打开弱光'); this.light.setState(this.light.weakLight); } }
class StrongLight { constructor(light) { this.light = light; } press() { console.log('打开强光'); this.light.setState(this.light.strongLight); } }
class OffLight { constructor(light) { this.light = light; } press() { console.log('关灯'); this.light.setState(this.light.offLight); } }
class Light { constructor() { this.weakLight = new WeakLight(this); this.strongLight = new StrongLight(this); this.offLight = new OffLight(this); this.currentState = this.offLight; } press() { this.currentState.press(); } setState(state) { this.currentState = state; } }
// test
const light = new Light();
light.press(); // 打开弱光
light.press(); // 打开强光
light.press(); // 关灯

Command

Application Scenarios

Encapsulate requests as objects to support undo/redo, queuing, logging, and decoupling invoker from executor.

Solution

interface Command { execute(); }
class CloseDoorCommand implements Command { execute() { console.log('close door'); } }
class OpenPcCommand implements Command { execute() { console.log('open pc'); } }
class OpenQQCommand implements Command { execute() { console.log('login qq'); } }
class CommandManager { constructor() { this.commandList = []; } addCommand(command) { this.commandList.push(command); } execute() { this.commandList.forEach(command => command.execute()); } }
// test
const manager = new CommandManager();
manager.addCommand(new CloseDoorCommand());
manager.addCommand(new OpenPcCommand());
manager.addCommand(new OpenQQCommand());
manager.execute();

Interpreter

Application Scenarios

Define a grammar for a language and implement an interpreter that evaluates sentences in that language.

Mediator

Application Scenarios

Introduce a mediator to convert many‑to‑many dependencies into one‑to‑many, reducing coupling and improving readability and maintainability.

How to Evaluate Code Quality?

Readability, extensibility, maintainability, reusability, testability.

High cohesion, low coupling.

Good code is simple, understandable by beginners, and avoids unnecessary clever tricks.

How to Form Long‑Term Memory?

Connect scattered knowledge points into a pyramid structure or mnemonic.

Deep thinking transforms others' knowledge into your own.

Repeated review turns short‑term memory into long‑term memory.

Knowledge is static, code is dynamic. Use design patterns wisely—master them for interviews and sharing, but apply them judiciously to keep code maintainable.
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Design Patternssoftware architecturecode qualityObject-Oriented ProgrammingSOLID principles
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

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.