设计模式教程
设计模式(Design Patterns)是软件开发中常见问题的经典解决方案。它们不是代码,而是解决特定问题的思想和方法论。本教程将带你深入理解 23 种经典设计模式,掌握面向对象设计的核心思想。
什么是设计模式?
设计模式是一套被反复使用、多数人知晓的、经过分类编目的代码设计经验的总结。它描述了在软件设计过程中一些不断重复发生的问题,以及该问题的核心解决方案。
设计模式的起源
设计模式的概念最初来源于建筑学。建筑师 Christopher Alexander 在其著作《建筑模式语言》中提出了一种描述建筑设计中常见问题的模式语言。
1994 年,Erich Gamma、John Vlissides、Ralph Johnson 和 Richard Helm(被称为"四人帮"或 GoF)出版了《设计模式:可复用面向对象软件的基础》一书,将模式概念引入软件开发领域,总结了 23 种经典设计模式。
为什么学习设计模式?
- 提高代码质量:设计模式是经过验证的最佳实践,使用它们可以避免常见的设计错误
- 增强沟通效率:设计模式提供了一套通用的词汇,开发人员可以用"工厂模式"、"观察者模式"等术语快速交流设计思想
- 提升设计能力:学习设计模式能帮助你理解面向对象设计的核心原则
- 面试必备:设计模式是技术面试的高频考点
设计模式分类
设计模式根据其目的分为三大类:
创建型模式(Creational Patterns)
创建型模式关注对象的创建过程,将对象的创建和使用分离。
| 模式 | 简介 | 使用场景 |
|---|---|---|
| 单例模式 | 确保一个类只有一个实例 | 配置管理器、日志记录器 |
| 工厂方法模式 | 定义创建对象的接口,让子类决定实例化哪个类 | 日志记录器、数据库连接 |
| 抽象工厂模式 | 创建相关或依赖对象的家族 | 跨平台 UI 组件 |
| 建造者模式 | 将复杂对象的构建与表示分离 | 构建复杂配置对象 |
| 原型模式 | 通过复制现有对象来创建新对象 | 创建成本高的对象 |
结构型模式(Structural Patterns)
结构型模式关注类和对象的组合,通过继承或组合来构建更大的结构。
| 模式 | 简介 | 使用场景 |
|---|---|---|
| 适配器模式 | 将一个类的接口转换成客户期望的另一个接口 | 接口不兼容的系统集成 |
| 桥接模式 | 将抽象与实现分离,使它们可以独立变化 | JDBC 驱动 |
| 组合模式 | 将对象组合成树形结构以表示"部分-整体"层次 | 文件系统、组织架构 |
| 装饰器模式 | 动态地给对象添加额外职责 | Java I/O 流 |
| 外观模式 | 为子系统中的一组接口提供一个统一入口 | 简化复杂系统调用 |
| 享元模式 | 共享细粒度对象以减少内存占用 | 字符串常量池 |
| 代理模式 | 为其他对象提供代理以控制访问 | 远程代理、虚拟代理 |
行为型模式(Behavioral Patterns)
行为型模式关注对象之间的通信和职责分配。
| 模式 | 简介 | 使用场景 |
|---|---|---|
| 策略模式 | 定义一系列算法,使它们可以互相替换 | 支付方式选择 |
| 观察者模式 | 定义对象间一对多的依赖关系 | 事件监听、消息订阅 |
| 命令模式 | 将请求封装成对象 | 菜单操作、事务处理 |
| 迭代器模式 | 提供一种方法顺序访问聚合对象中的元素 | 集合遍历 |
| 模板方法模式 | 定义算法骨架,将某些步骤延迟到子类 | Servlet 生命周期 |
| 状态模式 | 允许对象在内部状态改变时改变行为 | 订单状态流转 |
| 责任链模式 | 将请求沿链传递,直到有对象处理 | 过滤器链 |
| 中介者模式 | 用中介对象封装对象间的交互 | MVC 框架 |
| 备忘录模式 | 在不破坏封装的前提下捕获对象内部状态 | 撤销操作 |
| 访问者模式 | 在不改变数据结构的前提下定义新操作 | 编译器语法树 |
设计原则
设计模式的核心是遵循以下设计原则(SOLID 原则):
1. 单一职责原则(Single Responsibility Principle, SRP)
一个类应该只有一个引起它变化的原因。即一个类只负责一项职责。
反例:
// 一个类负责太多事情
public class User {
public void login() { /* 登录逻辑 */ }
public void sendEmail() { /* 发送邮件 */ }
public void generateReport() { /* 生成报告 */ }
}
正例:
// 职责分离
public class User {
private String username;
// 用户相关属性和方法
}
public class AuthService {
public void login(User user) { /* 登录逻辑 */ }
}
public class EmailService {
public void sendEmail(User user) { /* 发送邮件 */ }
}
2. 开闭原则(Open-Closed Principle, OCP)
软件实体应该对扩展开放,对修改关闭。即在不修改现有代码的情况下扩展功能。
反例:
// 每次新增形状都需要修改这个类
public class AreaCalculator {
public double calculate(Object shape) {
if (shape instanceof Circle) {
return Math.PI * ((Circle) shape).getRadius() * ((Circle) shape).getRadius();
} else if (shape instanceof Rectangle) {
return ((Rectangle) shape).getWidth() * ((Rectangle) shape).getHeight();
}
return 0;
}
}
正例:
// 使用多态,新增形状只需实现接口
public interface Shape {
double calculateArea();
}
public class Circle implements Shape {
private double radius;
public double calculateArea() {
return Math.PI * radius * radius;
}
}
public class Rectangle implements Shape {
private double width, height;
public double calculateArea() {
return width * height;
}
}
3. 里氏替换原则(Liskov Substitution Principle, LSP)
子类对象必须能够替换其父类对象,且程序行为正确。
反例:
// 正方形继承矩形违反了 LSP
public class Rectangle {
protected int width, height;
public void setWidth(int w) { width = w; }
public void setHeight(int h) { height = h; }
public int getArea() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int w) {
width = w;
height = w; // 强制宽高相等
}
@Override
public void setHeight(int h) {
width = h;
height = h;
}
}
// 问题:Rectangle rect = new Square();
// rect.setWidth(5); rect.setHeight(4);
// 面积应该是 20,但实际是 16
4. 接口隔离原则(Interface Segregation Principle, ISP)
客户端不应该依赖它不需要的接口。应该将大接口拆分为多个小接口。
反例:
// 一个臃肿的接口
public interface Worker {
void work();
void eat();
void sleep();
}
// 机器人不需要吃饭睡觉
public class Robot implements Worker {
public void work() { /* 工作 */ }
public void eat() { /* 空实现,不需要 */ }
public void sleep() { /* 空实现,不需要 */ }
}
正例:
// 接口拆分
public interface Workable {
void work();
}
public interface Feedable {
void eat();
}
public interface Sleepable {
void sleep();
}
public class Human implements Workable, Feedable, Sleepable {
public void work() { /* 工作 */ }
public void eat() { /* 吃饭 */ }
public void sleep() { /* 睡觉 */ }
}
public class Robot implements Workable {
public void work() { /* 工作 */ }
}
5. 依赖倒置原则(Dependency Inversion Principle, DIP)
高层模块不应该依赖低层模块,两者都应该依赖其抽象。抽象不应该依赖细节,细节应该依赖抽象。
反例:
// 高层模块直接依赖低层模块
public class UserService {
private MySQLDatabase database = new MySQLDatabase();
public void saveUser(User user) {
database.save(user);
}
}
正例:
// 高层模块依赖抽象
public interface Database {
void save(User user);
}
public class MySQLDatabase implements Database {
public void save(User user) { /* MySQL 保存逻辑 */ }
}
public class PostgreSQLDatabase implements Database {
public void save(User user) { /* PostgreSQL 保存逻辑 */ }
}
public class UserService {
private Database database;
public UserService(Database database) {
this.database = database;
}
public void saveUser(User user) {
database.save(user);
}
}
其他重要原则
迪米特法则(Law of Demeter, LoD)
一个对象应该对其他对象有尽可能少的了解。又称"最少知识原则"。
核心思想:只与你的直接朋友通信,不跟"陌生人"说话。
// 反例:链式调用暴露了太多内部结构
user.getOrder().getItem().getProduct().getPrice();
// 正例:让对象自己处理内部逻辑
user.getOrderTotalPrice();
组合复用原则(Composite Reuse Principle, CRP)
尽量使用组合而非继承来达到复用目的。
// 继承方式(耦合度高)
public class Dog extends Animal {
public void bark() { /* 汪汪叫 */ }
}
// 组合方式(更灵活)
public class Dog {
private Animal animal;
private BarkBehavior barkBehavior;
public void bark() {
barkBehavior.bark();
}
}
本教程的约定
示例语言
本教程使用 Java 作为示例语言,因为:
- Java 是面向对象编程的经典语言
- 大多数设计模式书籍使用 Java 示例
- Java 语法清晰,易于理解
结构说明
每个设计模式的章节包含以下内容:
- 模式定义:模式的正式定义和核心思想
- 问题场景:说明什么情况下需要这个模式
- 解决方案:详细的实现步骤和代码示例
- 模式结构:UML 类图展示类之间的关系
- 代码实现:完整的 Java 代码示例
- 应用场景:实际开发中的应用案例
- 优缺点分析:客观分析模式的利弊
- 与其他模式的关系:模式的组合使用
学习路径建议
- 先理解原则:深入理解 SOLID 原则,这是设计模式的理论基础
- 从简单开始:先学习单例、工厂、策略等常用且易理解的模式
- 对比学习:比较相似模式的区别,如工厂方法 vs 抽象工厂
- 实践应用:在实际项目中识别和应用设计模式
- 避免过度设计:设计模式是工具,不是目的,不要为了用模式而用模式
开始学习
准备好开始学习了吗?让我们从最经典的 单例模式 开始!
参考资源
- 《设计模式:可复用面向对象软件的基础》- GoF
- 《Head First 设计模式》- Eric Freeman 等
- 《Effective Java》- Joshua Bloch
- 《重构:改善既有代码的设计》- Martin Fowler