写这篇博客的起因很简单:我背得出"单一职责""开闭原则""依赖倒转"这些词,但很长一段时间里,我看别人写的策略模式、装饰模式、代理模式代码,总觉得这些原则像是挂在墙上的标语——知道它存在,但看不出它具体落在哪一行代码上。
直到我把每个模式拆开,逐行去对,才发现:原则不是注释里写的标语,而是代码结构本身。
这篇博客把最常用的七个设计模式(策略、装饰、代理、工厂方法、观察者、适配器、模板方法)拆开,告诉你每一行代码在守护哪条原则。代码注释里不会只扔一个"OCP"缩写,而是写成"(开闭原则) OCP"这种形式,降低阅读门槛。
注:本篇代码全部使用 Java 语言,与软考官方教材保持一致。
一、先统一"暗号"
在看代码之前,先把六大原则的中文名和英文缩写对号入座,后面代码注释里都会写成"中文 (英文缩写)"的形式:
表格
| 中文名 | 英文缩写 | 一句话记住它 |
|---|---|---|
| 单一职责原则 | SRP (Single Responsibility Principle) | 一个类只负责一件事,修改原因只有一个 |
| 开闭原则 | OCP (Open/Closed Principle) | 新增功能时,只新增文件,不修改旧文件 |
| 依赖倒转原则 | DIP (Dependency Inversion Principle) | 变量声明用接口/抽象类,不用具体类 |
| 里氏替换原则 | LSP (Liskov Substitution Principle) | 子类能完全替代父类,不改语义 |
| 接口隔离原则 | ISP (Interface Segregation Principle) | 接口要小而专,别搞"万能接口" |
| 迪米特法则 | LoD (Law of Demeter) | 只和直接朋友说话,别搞a.b.c.doSomething() |
| 合成复用原则 | CRP (Composite Reuse Principle) | 优先用"组合/has a",别一上来就"继承/is a" |
二、策略模式(Strategy)—— 支付方式的"插拔"
场景
订单结算,支持支付宝、微信、银行卡三种支付。未来可能还要加"云闪付""数字人民币"。
不用策略模式的写法(反面教材)
// 这个类什么都管,违反 (单一职责) SRP
// 每次加新支付方式都要改这个文件,违反 (开闭原则) OCP
// 高层直接依赖具体实现,违反 (依赖倒转) DIP
public class OrderService {
public void pay(String orderId, String payType) {
if (payType.equals("Alipay")) {
// 调用支付宝 SDK...
} else if (payType.equals("Wechat")) {
// 调用微信 SDK...
} else if (payType.equals("Bank")) {
// 调用银联 SDK...
}
}
}
策略模式的写法(原则落地)
// 1. 抽象策略 —— (依赖倒转) DIP 的体现:大家都依赖这个接口,不依赖具体类
public interface PaymentStrategy {
void pay(String orderId, double amount);
}
// 2. 具体策略 —— 每个类只负责一种支付,体现 (单一职责) SRP
public class AlipayStrategy implements PaymentStrategy {
@Override
public void pay(String orderId, double amount) {
// 只干支付宝的事,别的不管
}
}
public class WechatStrategy implements PaymentStrategy {
@Override
public void pay(String orderId, double amount) {
// 只干微信的事,别的不管
}
}
// 3. 上下文 —— 持有的是接口,不是具体类,体现 (依赖倒转) DIP
public class OrderContext {
private PaymentStrategy strategy; // ← (依赖倒转) DIP:字段类型是接口
public OrderContext(PaymentStrategy strategy) {
this.strategy = strategy; // ← 通过构造注入,不是 new 具体类
}
public void checkout(String orderId, double amount) {
strategy.pay(orderId, amount); // ← 运行时多态,解耦
}
}
// 4. 使用 —— 新增"云闪付"时,只需要新增一个类文件,不改任何旧代码
// 这就是 (开闭原则) OCP:"对扩展开放,对修改关闭"
public class Client {
public static void main(String[] args) {
OrderContext order = new OrderContext(new AlipayStrategy());
order.checkout("O2026001", 199.99);
}
}
原则落点总结
-
(单一职责) SRP:
AlipayStrategy只改支付宝逻辑,OrderContext只负责调度流程,各管一摊。 -
(开闭原则) OCP:新增支付方式 = 新增一个
.java文件,OrderContext一行不动。 -
(依赖倒转) DIP:
OrderContext的字段类型是PaymentStrategy,不是AlipayStrategy。
三、装饰模式(Decorator)—— 给流"套娃"
场景
文件流需要叠加功能:基础文件流 → 加缓冲 → 加加密 → 加压缩。每一层都是可选的,顺序可以任意组合。
代码实现
// 1. 抽象组件
public abstract class Stream {
public abstract void write(byte[] data);
}
// 2. 具体组件 —— 只负责真正写入磁盘,体现 (单一职责) SRP
public class FileStream extends Stream {
@Override
public void write(byte[] data) {
// 真正写入文件,别的不管
}
}
// 3. 装饰基类 —— 关键:持有组件接口,而不是继承具体组件
// 这是 (合成复用) CRP 的体现:用组合替代继承
public abstract class StreamDecorator extends Stream {
protected Stream component; // ← (合成复用) CRP:has a Stream,不是 is a Stream
protected StreamDecorator(Stream component) {
this.component = component;
}
@Override
public void write(byte[] data) {
component.write(data); // 委托给被装饰者
}
}
// 4. 具体装饰 —— 每个装饰只负责一个增强功能,体现 (单一职责) SRP
public class BufferedStream extends StreamDecorator {
public BufferedStream(Stream stream) {
super(stream);
}
@Override
public void write(byte[] data) {
// 先缓冲(自己的职责)
super.write(data); // 再交给下层
}
}
public class EncryptedStream extends StreamDecorator {
public EncryptedStream(Stream stream) {
super(stream);
}
@Override
public void write(byte[] data) {
// 先加密(自己的职责)
super.write(data);
}
}
// 5. 使用 —— 运行时动态组合,像套娃一样
// 体现 (开闭原则) OCP:功能可以无限叠加,不用改 FileStream 源码
public class Client {
public static void main(String[] args) {
Stream stream = new FileStream();
stream = new BufferedStream(stream); // 加缓冲
stream = new EncryptedStream(stream); // 加加密
// 想加压缩?再套一层:stream = new CompressedStream(stream);
stream.write(new byte[]{});
}
}
原则落点总结
-
(开闭原则) OCP:要给流加"压缩"功能?新建
CompressedStream类即可,FileStream、BufferedStream完全不用改。 -
(合成复用) CRP:
BufferedStream不是extends FileStream,而是内部has a Stream。如果继承,缓冲流就只能装饰文件流;组合则可以装饰网络流、内存流等任何Stream。 -
(单一职责) SRP:
BufferedStream只关心缓冲,EncryptedStream只关心加密,绝不越界。
四、代理模式(Proxy)—— 图片的"门卫"
场景
图片加载,代理负责"懒加载 + 权限检查 + 计数统计",真实对象只管加载图片本身。
代码实现
// 1. 抽象主题 —— (依赖倒转) DIP 的抽象层
public interface Image {
void display();
}
// 2. 真实主题 —— (单一职责) SRP:只负责从磁盘加载并显示,不掺和权限、统计
public class RealImage implements Image {
private String filename;
public RealImage(String filename) {
this.filename = filename;
loadFromDisk(); // 构造时就很重,直接读磁盘
}
private void loadFromDisk() {
System.out.println("Loading " + filename);
}
@Override
public void display() {
System.out.println("Displaying " + filename);
}
}
// 3. 代理 —— (单一职责) SRP:只负责控制访问,不处理图片解码
public class ProxyImage implements Image {
private RealImage realImage; // 组合关系
private String filename;
public ProxyImage(String filename) {
this.filename = filename; // 构造时很轻,不加载图片
}
@Override
public void display() {
// 权限检查(代理自己的职责)
if (!checkAccess()) return;
// 懒加载(代理自己的职责)
if (realImage == null) {
realImage = new RealImage(filename);
}
// 记录访问次数(代理自己的职责)
logAccess();
realImage.display(); // 最终委托给真实对象
}
private boolean checkAccess() { /* ... */ return true; }
private void logAccess() { /* ... */ }
}
// 4. 客户端 —— (迪米特法则) LoD:客户端只认识代理,不认识真实对象
// 客户端不知道 RealImage 的存在,也不需要知道,代理把真实对象"藏"起来了
public class Client {
public static void main(String[] args) {
Image image = new ProxyImage("photo.jpg");
image.display(); // 第一次会加载
image.display(); // 第二次直接显示,不再加载
}
}
原则落点总结
-
(单一职责) SRP:
RealImage只管图片解码;ProxyImage只管访问控制、懒加载、日志。如果写在一个类里,这个类就有两个修改原因(图片格式变了 / 权限策略变了)。 -
(依赖倒转) DIP:客户端声明
Image image = new ProxyImage(...),依赖的是接口,不是具体类。 -
(迪米特法则) LoD:客户端只和
ProxyImage打交道,不和RealImage打交道,更不用调用RealImage的内部方法。
五、工厂方法模式(Factory Method)—— 把 new 关进笼子
场景
日志记录,可能记录到文件、数据库、控制台。未来可能要加"Redis日志"。
代码实现
// 1. 产品接口 —— (依赖倒转) DIP 的抽象
public interface Logger {
void log(String message);
}
// 2. 具体产品 —— (单一职责) SRP:每个类只负责一种写入方式
public class FileLogger implements Logger {
@Override
public void log(String message) {
// 写入文件,别的不管
// Java 中可用 FileWriter 等实现
}
}
public class DatabaseLogger implements Logger {
@Override
public void log(String message) {
// 写入数据库,别的不管
}
}
// 3. 创建者抽象 —— 核心:把"创建对象"的责任独立出来
public abstract class LoggerCreator {
// 工厂方法:由子类决定实例化哪个具体类
// 父类只依赖接口,体现 (依赖倒转) DIP
public abstract Logger createLogger(); // ← 延迟到子类实现
public void writeLog(String message) {
Logger logger = createLogger(); // ← 依赖抽象,不是 new FileLogger()
logger.log(message);
}
}
// 4. 具体创建者 —— 每个创建者只负责创建一种日志器,体现 (单一职责) SRP
public class FileLoggerCreator extends LoggerCreator {
@Override
public Logger createLogger() {
return new FileLogger();
}
}
public class DatabaseLoggerCreator extends LoggerCreator {
@Override
public Logger createLogger() {
return new DatabaseLogger();
}
}
// 5. 使用 —— 要切换日志方式,只需换创建者,writeLog 方法完全不用改
// 新增"Redis日志"时:新增 RedisLogger + RedisLoggerCreator
// 旧代码完全不动 —— 体现 (开闭原则) OCP
public class Client {
public static void main(String[] args) {
LoggerCreator creator = new FileLoggerCreator();
creator.writeLog("系统启动"); // 实际写到文件
}
}
原则落点总结
-
(开闭原则) OCP:新增日志方式 = 新增两个类(
RedisLogger+RedisLoggerCreator),LoggerCreator的writeLog方法一行不改。 -
(依赖倒转) DIP:
writeLog里用的是Logger,不是FileLogger;createLogger返回的也是Logger。 -
(单一职责) SRP:
FileLogger只管文件写入;FileLoggerCreator只管创建;LoggerCreator只管使用流程。如果不用工厂方法,创建逻辑和使用逻辑会耦合在一个类里。
六、观察者模式(Observer)—— 订阅与广播
场景
股票价格变动,需要同时通知"邮件告警系统""短信通知系统""APP推送系统"。未来可能还要加"微信机器人推送"。
代码实现
import java.util.ArrayList;
import java.util.List;
// 1. 观察者接口 —— (依赖倒转) DIP:主题只依赖这个接口,不依赖具体观察者
public interface Observer {
void update(String stockCode, double price);
}
// 2. 主题接口 —— 维护观察者列表,提供订阅/取消订阅/通知
public interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers();
}
// 3. 具体主题 —— 只负责维护股票数据和观察者列表,体现 (单一职责) SRP
public class Stock implements Subject {
private List<Observer> observers = new ArrayList<>(); // ← 依赖接口
private String stockCode;
private double price;
public Stock(String stockCode, double price) {
this.stockCode = stockCode;
this.price = price;
}
public double getPrice() {
return price;
}
public void setPrice(double price) {
this.price = price;
notifyObservers(); // 价格一变就广播
}
@Override
public void attach(Observer observer) {
observers.add(observer);
}
@Override
public void detach(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(stockCode, price); // 统一调用接口方法
}
}
}
// 4. 具体观察者 —— 每个观察者只负责一种通知方式,体现 (单一职责) SRP
public class EmailAlert implements Observer {
@Override
public void update(String stockCode, double price) {
System.out.println("邮件告警:" + stockCode + " 价格变为 " + price);
}
}
public class SmsAlert implements Observer {
@Override
public void update(String stockCode, double price) {
System.out.println("短信通知:" + stockCode + " 价格变为 " + price);
}
}
public class AppPush implements Observer {
@Override
public void update(String stockCode, double price) {
System.out.println("APP推送:" + stockCode + " 价格变为 " + price);
}
}
// 5. 使用 —— 新增"微信机器人推送"?新增 WeChatBot 类实现 Observer 即可
// Stock 类一行不改,体现 (开闭原则) OCP
public class Client {
public static void main(String[] args) {
Stock stock = new Stock("000001", 10.5);
stock.attach(new EmailAlert());
stock.attach(new SmsAlert());
stock.attach(new AppPush());
stock.setPrice(11.2); // 价格一变,三个观察者同时收到通知
}
}
原则落点总结
-
(开闭原则) OCP:新增通知方式 = 新增一个实现
Observer的类,Stock类完全不用改。 -
(依赖倒转) DIP:
Stock内部的观察者列表是List<Observer>,不是List<EmailAlert>。它只认识接口,不认识具体观察者。 -
(单一职责) SRP:
Stock只管维护数据和通知流程;EmailAlert只管发邮件;SmsAlert只管发短信。各扫门前雪。
七、适配器模式(Adapter)—— 旧接口的"转接头"
场景
老系统里有一个 OldPrinter 类,接口是 printOld()。新系统统一用 Printer 接口,方法是 print()。不能改老系统的源码,怎么让老打印机接入新系统?
代码实现
// 1. 新系统期望的接口 —— (依赖倒转) DIP 的目标接口
public interface Printer {
void print(String content);
}
// 2. 老系统的类 —— 源码不能改,可能是第三方库或遗留系统
public class OldPrinter {
public void printOld(String content) {
System.out.println("老打印机输出:" + content);
}
}
// 3. 适配器 —— 把老接口包装成新接口,体现 (开闭原则) OCP:不改旧代码,只新增适配器
public class OldPrinterAdapter implements Printer {
private OldPrinter oldPrinter; // ← (合成复用) CRP:组合旧对象,不继承旧类
public OldPrinterAdapter(OldPrinter oldPrinter) {
this.oldPrinter = oldPrinter;
}
@Override
public void print(String content) {
// 内部做接口转换:新接口的 print 调用旧接口的 printOld
oldPrinter.printOld(content);
}
}
// 4. 新系统使用 —— 只认 Printer,不认识 OldPrinter,体现 (依赖倒转) DIP
public class DocumentService {
private Printer printer; // ← 依赖抽象接口
public DocumentService(Printer printer) {
this.printer = printer;
}
public void printDocument(String content) {
printer.print(content); // 统一调用新接口
}
}
// 5. 使用 —— 老打印机通过适配器接入新系统,OldPrinter 源码一行没动
public class Client {
public static void main(String[] args) {
OldPrinter old = new OldPrinter();
Printer adapter = new OldPrinterAdapter(old);
DocumentService service = new DocumentService(adapter);
service.printDocument("年度报告");
}
}
原则落点总结
-
(开闭原则) OCP:
OldPrinter的源码不能改、也没改。新增OldPrinterAdapter就解决了兼容问题,旧代码完全封闭。 -
(合成复用) CRP:
OldPrinterAdapter不是extends OldPrinter(可能也继承不了,因为是第三方 final 类),而是内部has a OldPrinter。 -
(依赖倒转) DIP:
DocumentService只认Printer,它不知道也不关心底层到底是新打印机还是老打印机加适配器。
八、模板方法模式(Template Method)—— 骨架不变,步骤可换
场景
数据导入流程:读取文件 → 解析数据 → 校验数据 → 写入数据库。这个流程骨架是固定的,但"解析"这一步,CSV 和 Excel 的解析方式完全不同。
代码实现
import java.util.List;
// 1. 抽象类 —— 定义算法骨架,体现 (开闭原则) OCP:骨架固定,可变部分留给子类
public abstract class DataImporter {
// 模板方法:骨架,final 不让子类乱改
public final void importData(String filePath) {
String rawData = readFile(filePath); // 通用步骤
List<DataRecord> parsedData = parse(rawData); // ← 可变步骤:抽象方法,子类实现
validate(parsedData); // 通用步骤
saveToDatabase(parsedData); // 通用步骤
}
private String readFile(String filePath) {
// Java 中可用 Files.readString() 等实现
return "file content"; // 具体实现,子类不用管
}
// 抽象方法 —— 子类必须实现,体现 (依赖倒转) DIP:父类依赖抽象方法,不依赖具体解析逻辑
protected abstract List<DataRecord> parse(String rawData);
private void validate(List<DataRecord> data) {
// 通用校验逻辑
}
private void saveToDatabase(List<DataRecord> data) {
// 通用写入逻辑
}
}
// 数据记录类(辅助)
class DataRecord {
// 数据字段...
}
// 2. 具体子类 —— 只负责解析 CSV,体现 (单一职责) SRP
public class CsvImporter extends DataImporter {
@Override
protected List<DataRecord> parse(String rawData) {
// 按逗号分割解析 CSV
return parseCsv(rawData);
}
private List<DataRecord> parseCsv(String rawData) { /* ... */ return null; }
}
// 3. 具体子类 —— 只负责解析 Excel,体现 (单一职责) SRP
public class ExcelImporter extends DataImporter {
@Override
protected List<DataRecord> parse(String rawData) {
// 按单元格解析 Excel
return parseExcel(rawData);
}
private List<DataRecord> parseExcel(String rawData) { /* ... */ return null; }
}
// 4. 使用 —— 新增"JSON 导入"?新建 JsonImporter 继承 DataImporter,重写 parse 即可
// importData 方法的骨架一行不改,体现 (开闭原则) OCP
public class Client {
public static void main(String[] args) {
DataImporter importer = new CsvImporter();
importer.importData("data.csv");
}
}
原则落点总结
-
(开闭原则) OCP:新增一种导入格式(比如 JSON),只需要新增
JsonImporter类并重写parse方法。DataImporter的importData骨架和readFile、validate、saveToDatabase完全不用改。 -
(依赖倒转) DIP:
DataImporter作为父类,它的importData方法调用的是抽象的parse方法,而不是某个具体的解析逻辑。它依赖的是"抽象步骤",不是"具体实现"。 -
(单一职责) SRP:
CsvImporter只懂 CSV;ExcelImporter只懂 Excel。解析逻辑和通用流程彻底分离。
九、速查表:模式 ↔ 原则(带代码气味)
| 设计模式 | 主要体现的原则 | 代码里的"识别气味" |
|---|---|---|
| 策略模式 | (开闭原则) OCP、(依赖倒转) DIP、(单一职责) SRP | 接口作为参数传入构造器,运行时切换算法 |
| 装饰模式 | (开闭原则) OCP、(合成复用) CRP、(单一职责) SRP | 构造器传入同类接口,方法里先干自己的事再 super.write() 委托 |
| 代理模式 | (单一职责) SRP、(依赖倒转) DIP、(迪米特法则) LoD | 代理类内部 new 真实对象,客户端只接触代理接口 |
| 工厂方法 | (开闭原则) OCP、(依赖倒转) DIP、(单一职责) SRP | 把 new 语句隔离到子类,工厂方法返回抽象类型 |
| 观察者模式 | (开闭原则) OCP、(依赖倒转) DIP、(单一职责) SRP | 主题维护 List<接口>,遍历调用接口方法广播通知 |
| 适配器模式 | (开闭原则) OCP、(合成复用) CRP、(依赖倒转) DIP | 包装旧接口,内部做方法名/参数转换,不改旧代码 |
| 模板方法 | (开闭原则) OCP、(依赖倒转) DIP、(单一职责) SRP | 父类定义 final 骨架,抽象方法留给子类填"钩子" |
十、一句话心法
判断一段代码是否体现了某条原则,不要看注释里写了什么,只看三个地方:
-
成员变量声明的类型 是接口还是具体类?—— 这是 (依赖倒转) DIP
-
新增功能时 是新增文件还是修改旧文件?—— 这是 (开闭原则) OCP
-
类名和类里的方法 是不是都在说同一件事?—— 这是 (单一职责) SRP
把这三个问题套到任何设计模式的代码上,那些原本像标语一样的原则,就会一行一行地钉在代码里,看得见、摸得着。

1101

被折叠的 条评论
为什么被折叠?



