初识设计模式 —— 为什么需要设计模式、SOLID 原则、UML 基础
代码能跑不叫本事,代码能长期演进才算功夫。设计模式不是银弹,但它是前人用无数踩坑经验凝练出的”通用解题套路”。学好这套套路,你在面对复杂系统时就不再手足无措。
📋 开篇自测:你已经知道多少?
- 你能说出”设计模式”这个概念最早来源于哪个领域吗?
- SOLID 五大原则中,“L”代表什么?它要解决的核心问题是什么?
- UML 类图中实线箭头和虚线箭头分别表示什么关系?
一、设计模式的前世今生
1.1 从建筑学到软件工程
“设计模式”这四个字最早并不属于计算机。1977 年,建筑师 Christopher Alexander 与 Sara Ishikawa、Murray Silverstein 等人在合著的《A Pattern Language》一书中提出了”模式语言”的概念——他发现,那些让人住得舒服的建筑,背后总有一些反复出现的空间组织规律。把这些规律提炼出来,就形成了”模式”。
十几年后,四位软件工程师——Erich Gamma、Richard Helm、Ralph Johnson、John Vlissides——把这套思想移植到了面向对象编程领域。1994 年,他们合著的《Design Patterns: Elements of Reusable Object-Oriented Software》横空出世。因为作者正好四位,业界戏称他们为 Gang of Four(GoF)。这本书收录了 23 种设计模式,至今仍是设计模式领域的”圣经”。
1.2 设计模式到底解决什么问题
先来看一段日常开发中极其常见的代码:
// 一个处理订单的函数,随着需求增长越来越臃肿
function processOrder(order: Order) {
// 1. 验证订单
if (!order.items || order.items.length === 0) {
throw new Error('订单不能为空');
}
if (order.totalAmount <= 0) {
throw new Error('金额异常');
}
// 2. 计算折扣——三个月后加了满减、优惠券、会员折扣...
let discount = 0;
if (order.couponCode === 'SUMMER20') {
discount = order.totalAmount * 0.2;
} else if (order.userLevel === 'vip') {
discount = order.totalAmount * 0.1;
} else if (order.totalAmount > 500) {
discount = 50;
}
// 半年后这里已经有 200 行 if-else...
// 3. 扣库存
for (const item of order.items) {
updateInventory(item.skuId, -item.quantity);
}
// 4. 发通知——先是邮件,后来又加了短信、站内信、企业微信...
sendEmail(order.userId, '订单已提交');
sendSMS(order.userId, '订单已提交');
sendWechatWork(order.userId, '订单已提交');
}
这段代码有什么问题?功能上它完全正确。但是:
- 修改折扣规则时,你必须深入这个几百行的函数内部,小心翼翼地改 if-else,改错一个条件全盘皆输。
- 增加通知渠道时,你又要往函数尾部追加代码,processOrder 变成了一个什么都干的”上帝函数”。
- 测试困难,因为所有逻辑搅在一起,你没法单独测折扣计算逻辑。
设计模式解决的正是这类问题——它不是让代码能跑,而是让代码能优雅地应对变化。上面这段代码,用策略模式可以解决折扣的 if-else 膨胀,用观察者模式可以解耦通知逻辑,用建造者模式可以规范订单的组装流程。
1.3 设计模式的三大家族
GoF 的 23 种模式按照用途分成三大类:
| 分类 | 关注点 | 包含模式 |
|---|---|---|
| 创建型(Creational) | 怎样创建对象 | 单例、工厂方法、抽象工厂、建造者、原型 |
| 结构型(Structural) | 怎样组合类和对象 | 适配器、装饰器、代理、外观、桥接、组合、享元 |
| 行为型(Behavioral) | 对象之间怎样通信协作 | 观察者、策略、模板方法、迭代器、责任链、命令、状态、中介者、备忘录、访问者、解释器 |
你不需要死记这张表。随着后续章节逐一拆解,这些名字会变成你信手拈来的工具。
🤔 想一想 回忆一下你最近写过的代码,有没有遇到过”改一个需求要动十几个文件”的情况?如果有,你觉得问题出在哪里?
二、SOLID:面向对象设计的五根支柱
设计模式是”招式”,SOLID 原则是”内功”。你可以不用任何设计模式,但只要遵循 SOLID 原则,代码质量就不会太差。反过来,不理解 SOLID 而机械套用模式,往往适得其反。
2.1 S — 单一职责原则(Single Responsibility Principle)
一个类只应该有一个引起它变化的原因。
这是最容易理解、也最容易被违反的原则。来看一个反面教材:
// 反例:一个类同时负责用户数据管理和日志记录
class UserService {
private users: Map<string, User> = new Map();
addUser(user: User): void {
this.users.set(user.id, user);
// 日志逻辑不应该出现在这里
const logEntry = `[${new Date().toISOString()}] 新增用户: ${user.name}`;
fs.appendFileSync('/var/log/app.log', logEntry + '\n');
}
getUser(id: string): User | undefined {
const user = this.users.get(id);
const logEntry = `[${new Date().toISOString()}] 查询用户: ${id}`;
fs.appendFileSync('/var/log/app.log', logEntry + '\n');
return user;
}
}
问题在哪里?如果某天日志的存储方式从文件改为数据库,你需要修改 UserService。但 UserService 的本职工作是管理用户数据,日志存到哪里关它什么事?
// 正例:职责分离
class Logger {
log(message: string): void {
const entry = `[${new Date().toISOString()}] ${message}`;
// 日志存储方式变化只影响这一个类
fs.appendFileSync('/var/log/app.log', entry + '\n');
}
}
class UserService {
constructor(
private users: Map<string, User> = new Map(),
private logger: Logger = new Logger()
) {}
addUser(user: User): void {
this.users.set(user.id, user);
this.logger.log(`新增用户: ${user.name}`);
}
getUser(id: string): User | undefined {
const user = this.users.get(id);
this.logger.log(`查询用户: ${id}`);
return user;
}
}
2.2 O — 开闭原则(Open-Closed Principle)
对扩展开放,对修改关闭。
这是 SOLID 中最重要也最抽象的一条。它说的是:当需求变化时,你应该通过新增代码来应对,而不是修改已有代码。
回到开头那个折扣计算的例子,每加一种折扣规则就要改原函数,这就违反了开闭原则。正确的做法是定义一个折扣策略接口,每种折扣规则都是一个独立的实现:
interface DiscountStrategy {
calculate(amount: number, context: OrderContext): number;
}
class CouponDiscount implements DiscountStrategy {
calculate(amount: number, context: OrderContext): number {
if (context.couponCode === 'SUMMER20') return amount * 0.2;
return 0;
}
}
class VipDiscount implements DiscountStrategy {
calculate(amount: number, context: OrderContext): number {
if (context.userLevel === 'vip') return amount * 0.1;
return 0;
}
}
// 新增折扣规则?加一个新类就行,不碰已有代码
class FullReductionDiscount implements DiscountStrategy {
calculate(amount: number, context: OrderContext): number {
if (amount > 500) return 50;
return 0;
}
}
2.3 L — 里氏替换原则(Liskov Substitution Principle)
子类对象必须能够替换其父类对象被使用,而程序行为不发生改变。
这条原则由 Barbara Liskov 在 1987 年提出。通俗地说:如果你的代码期望一个”鸟”类型的参数,那传入”麻雀”或”老鹰”都应该正常工作。如果传入”企鹅”导致 fly() 方法抛出异常,那就违反了里氏替换。
// 反例:违反里氏替换
class Bird {
fly(): string {
return '飞行中...';
}
}
class Penguin extends Bird {
fly(): string {
// 企鹅不会飞,但被迫实现了 fly 方法
throw new Error('企鹅不会飞!');
}
}
function makeBirdFly(bird: Bird): void {
console.log(bird.fly()); // 传入 Penguin 会爆炸
}
正确的做法是重新审视继承体系。不是所有鸟都能飞,所以”会飞”不应该是 Bird 的固有能力:
// 正例:合理的抽象层次
interface Flyable {
fly(): string;
}
class Bird {
eat(): string {
return '进食中...';
}
}
class Sparrow extends Bird implements Flyable {
fly(): string {
return '麻雀飞行中...';
}
}
class Penguin extends Bird {
swim(): string {
return '企鹅游泳中...';
}
}
2.4 I — 接口隔离原则(Interface Segregation Principle)
客户端不应该被迫依赖它不使用的接口。
宁可有多个小而精的接口,也不要一个大而全的”胖接口”。
// 反例:胖接口
interface SmartDevice {
call(number: string): void;
takePicture(): void;
playMusic(track: string): void;
browseWeb(url: string): void;
measureHeartRate(): void;
}
// 一个简单的音乐播放器被迫实现一堆不相关的方法
class MusicPlayer implements SmartDevice {
call() { throw new Error('不支持'); }
takePicture() { throw new Error('不支持'); }
playMusic(track: string) { /* 真正的逻辑 */ }
browseWeb() { throw new Error('不支持'); }
measureHeartRate() { throw new Error('不支持'); }
}
// 正例:拆成细粒度接口
interface Callable { call(number: string): void; }
interface Camera { takePicture(): void; }
interface MusicCapable { playMusic(track: string): void; }
interface WebBrowsable { browseWeb(url: string): void; }
interface HealthMonitor { measureHeartRate(): void; }
// 各取所需
class MusicPlayer implements MusicCapable {
playMusic(track: string) { /* 只实现自己需要的 */ }
}
class SmartPhone implements Callable, Camera, MusicCapable, WebBrowsable {
call(number: string) { /* ... */ }
takePicture() { /* ... */ }
playMusic(track: string) { /* ... */ }
browseWeb(url: string) { /* ... */ }
}
2.5 D — 依赖倒置原则(Dependency Inversion Principle)
高层模块不应该依赖低层模块,两者都应该依赖抽象。
这条原则是实现”可替换性”的关键。想象一下你家的电器——台灯、风扇、冰箱,它们都依赖”插座”这个抽象接口,而不是直接焊接在电线上。正因为依赖插座这个”抽象”,你才能随时换一台不同品牌的电器。
// 反例:高层直接依赖低层具体实现
class MySQLDatabase {
query(sql: string): any[] { /* MySQL 专属逻辑 */ return []; }
}
class OrderRepository {
private db = new MySQLDatabase(); // 直接绑死了 MySQL
findById(id: string): Order | null {
const rows = this.db.query(`SELECT * FROM orders WHERE id = '${id}'`);
// ⚠️ 注意:这里用字符串拼接只是为了简化示例,
// 实际项目中必须使用参数化查询防止 SQL 注入
return rows[0] ?? null;
}
}
// 正例:依赖抽象接口
interface Database {
query(statement: string, params?: any[]): any[];
}
class MySQLDatabase implements Database {
query(statement: string, params?: any[]): any[] { /* MySQL 实现 */ return []; }
}
class PostgresDatabase implements Database {
query(statement: string, params?: any[]): any[] { /* Postgres 实现 */ return []; }
}
class OrderRepository {
constructor(private db: Database) {} // 依赖抽象,不关心具体实现
findById(id: string): Order | null {
const rows = this.db.query('SELECT * FROM orders WHERE id = $1', [id]);
return rows[0] ?? null;
}
}
// 使用时注入具体实现
const repo = new OrderRepository(new PostgresDatabase());
🤔 想一想 SOLID 五个原则之间有没有内在联系?比如,违反单一职责原则的类,是否也更容易违反开闭原则?试着用自己的话梳理一下它们之间的关系。
三、UML 类图:设计模式的”建筑蓝图”
学设计模式绕不开 UML 类图。但你不需要学全套 UML——对于设计模式来说,掌握类图中最核心的六种关系就够了。
3.1 类的基本表示
在 UML 类图中,一个类用一个矩形表示,分成三格:
┌─────────────────────┐
│ ClassName │ ← 类名
├─────────────────────┤
│ - privateField: T │ ← 属性(- 私有, + 公有, # 保护)
│ + publicField: T │
├─────────────────────┤
│ + publicMethod(): R │ ← 方法
│ - privateMethod(): R │
└─────────────────────┘
TypeScript 中的接口和抽象类也用类似的方式表示,但类名上方会加上 <<interface>> 或 <<abstract>> 标记。
3.2 六种核心关系
继承(Generalization):实线 + 空心三角箭头,指向父类。表示”is-a”关系。
┌──────────┐ ┌──────────┐
│ Animal │◁──────│ Dog │
└──────────┘ └──────────┘
对应 TypeScript:class Dog extends Animal {}
实现(Realization):虚线 + 空心三角箭头,指向接口。
┌───────────────┐ ┌──────────┐
│ <<interface>> │◁------│ MyClass │
│ Printable │ └──────────┘
└───────────────┘
对应 TypeScript:class MyClass implements Printable {}
关联(Association):实线箭头,表示一个类”知道”另一个类。
┌──────────┐ ┌──────────┐
│ Teacher │──────>│ Course │
└──────────┘ └──────────┘
对应代码:Teacher 类中有一个 Course 类型的属性。
依赖(Dependency):虚线箭头,表示一个类”临时用到”另一个类(比如方法参数)。
┌──────────┐ ┌──────────┐
│ Driver │------>│ Car │
└──────────┘ └──────────┘
对应代码:Driver 的某个方法参数是 Car 类型,但 Driver 本身不持有 Car。
聚合(Aggregation):实线 + 空心菱形,表示”has-a”且整体与部分可以独立存在。
┌──────────┐ ┌──────────┐
│ School │◇──────│ Teacher │
└──────────┘ └──────────┘
学校有老师,但老师离开学校仍然是老师。
组合(Composition):实线 + 实心菱形,表示”has-a”且部分的生命周期依赖整体。
┌──────────┐ ┌──────────┐
│ House │◆──────│ Room │
└──────────┘ └──────────┘
房子没了,房间也不存在了。
3.3 阅读类图的实战技巧
初学者看类图经常犯一个错误:试图一次看懂整张图。正确的读法是:
- 先找核心类——通常是被最多箭头指向的那个。
- 看继承和实现关系——了解类的层次结构。
- 看关联和依赖——了解类之间如何协作。
- 最后看聚合和组合——了解对象的生命周期管理。
在后续章节中,每个设计模式都会附上精简的类图。养成看图的习惯,你理解模式的速度会快很多。
🤔 想一想 TypeScript 中没有”抽象类”的运行时概念(编译后就消失了),这会影响我们在 TypeScript 项目中使用 UML 建模吗?你觉得 TypeScript 的 interface 和 abstract class 分别对应 UML 中的什么?
四、设计模式的正确打开方式
4.1 不要背,要理解场景
很多人学设计模式的姿势是错误的:背定义、背 UML 图、背代码模板。结果面试时能答上来”观察者模式是一对多的依赖关系”,但回到实际开发中完全不知道什么时候该用。
正确的打开方式是:从问题出发。每个设计模式的诞生都是为了解决某一类特定的设计难题。你要理解的是”什么情况下我会遇到这个问题”,而不是”这个模式的 UML 图长什么样”。
4.2 不要滥用,保持克制
学会了锤子,看什么都像钉子——这是设计模式学习中最大的陷阱。一个只需要简单 if-else 就能搞定的逻辑,没必要强行套一个策略模式。一个只有两三个监听者的场景,没必要搞一套完整的事件系统。
判断该不该用模式,有一个简单的标准:如果引入模式之后,代码的复杂度降低了、可读性提高了、未来扩展更容易了,那就用;如果引入之后代码变得更绕、同事看不懂了,那就别用。
4.3 TypeScript 视角下的设计模式
本课程的所有代码示例都使用 TypeScript。相比传统的 Java/C++,TypeScript 有几个特点会影响设计模式的实现方式:
- 接口是零成本抽象:TypeScript 的 interface 在编译后完全消失,不会增加运行时开销,所以可以放心大胆地使用接口来定义契约。
- 函数是一等公民:很多在 Java 中需要用类来包装的模式(比如策略、命令),在 TypeScript 中可以直接用函数或箭头函数实现,代码更简洁。
- 泛型很强大:TypeScript 的泛型系统让很多模式的实现更加类型安全。
- 装饰器语法原生支持:TypeScript 有 decorator 语法,与装饰器模式天然契合。
我们会在后续章节中充分利用这些语言特性,写出既符合模式精神、又贴合 TypeScript 生态的代码。
五、本章小结与知识地图
这一章我们建立了三个关键认知:
- 设计模式是经验的结晶——它不是学术发明,而是从无数真实项目中提炼出的解题套路。23 种经典模式分为创建型、结构型、行为型三大家族。
- SOLID 是模式背后的原则——单一职责、开闭、里氏替换、接口隔离、依赖倒置,五大原则构成了面向对象设计的基石。模式是原则的具体化身。
- UML 类图是交流的语言——掌握继承、实现、关联、依赖、聚合、组合六种关系,你就能读懂任何设计模式的结构图。
从下一章开始,我们正式进入具体的设计模式学习。第一站:创建型模式——从对象的诞生讲起。
📝 结尾自测
- GoF 的 23 种设计模式分为哪三大类?每类各关注什么问题?
- 用自己的话解释”开闭原则”,并举一个违反开闭原则的代码场景。
- UML 类图中,聚合和组合的区别是什么?分别用什么符号表示?
- 为什么说”依赖倒置原则”是实现可替换性的关键?
- 在 TypeScript 中,哪些语言特性会让设计模式的实现方式与 Java 有所不同?
购买课程解锁全部内容
写出优雅代码:10 章掌握 23 种设计模式
¥29.90