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.
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 Programmingis 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); // 30000Chain 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.
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.
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.
