设计模式到底在“守“什么原则?用代码把六大原则“钉“在每一行上

📅 2026/7/14 10:44:09 👁️ 阅读次数 📝 编程学习
设计模式到底在“守“什么原则?用代码把六大原则“钉“在每一行上

写这篇博客的起因很简单:我背得出"单一职责""开闭原则""依赖倒转"这些词,但很长一段时间里,我看别人写的策略模式、装饰模式、代理模式代码,总觉得这些原则像是挂在墙上的标语——知道它存在,但看不出它具体落在哪一行代码上。

直到我把每个模式拆开,逐行去对,才发现:原则不是注释里写的标语,而是代码结构本身

这篇博客把最常用的七个设计模式(策略、装饰、代理、工厂方法、观察者、适配器、模板方法)拆开,告诉你每一行代码在守护哪条原则。代码注释里不会只扔一个"OCP"缩写,而是写成"(开闭原则) OCP"这种形式,降低阅读门槛。

注:本篇代码全部使用 Java 语言,与软考官方教材保持一致。


一、先统一"暗号"

在看代码之前,先把六大原则的中文名和英文缩写对号入座,后面代码注释里都会写成"中文 (英文缩写)"的形式:

表格

中文名英文缩写一句话记住它
单一职责原则(单一职责) SRP一个类只负责一件事,修改原因只有一个
开闭原则(开闭原则) OCP新增功能时,只新增文件,不修改旧文件
依赖倒转原则(依赖倒转) DIP变量声明用接口/抽象类,不用具体类
里氏替换原则(里氏替换) LSP子类能完全替代父类,不改语义
接口隔离原则(接口隔离) ISP接口要小而专,别搞"万能接口"
迪米特法则(迪米特法则) LoD只和直接朋友说话,别搞a.b.c.doSomething()
合成复用原则(合成复用) CRP优先用"组合/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); } }

原则落点总结

  • (单一职责) SRPAlipayStrategy只改支付宝逻辑,OrderContext只负责调度流程,各管一摊。

  • (开闭原则) OCP:新增支付方式 = 新增一个.java文件,OrderContext一行不动。

  • (依赖倒转) DIPOrderContext的字段类型是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类即可,FileStreamBufferedStream完全不用改。

  • (合成复用) CRPBufferedStream不是extends FileStream,而是内部has a Stream。如果继承,缓冲流就只能装饰文件流;组合则可以装饰网络流、内存流等任何Stream

  • (单一职责) SRPBufferedStream只关心缓冲,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(); // 第二次直接显示,不再加载 } }

原则落点总结

  • (单一职责) SRPRealImage只管图片解码;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),LoggerCreatorwriteLog方法一行不改。

  • (依赖倒转) DIPwriteLog里用的是Logger,不是FileLoggercreateLogger返回的也是Logger

  • (单一职责) SRPFileLogger只管文件写入;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类完全不用改。

  • (依赖倒转) DIPStock内部的观察者列表是List<Observer>,不是List<EmailAlert>。它只认识接口,不认识具体观察者。

  • (单一职责) SRPStock只管维护数据和通知流程;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("年度报告"); } }

原则落点总结

  • (开闭原则) OCPOldPrinter的源码不能改、也没改。新增OldPrinterAdapter就解决了兼容问题,旧代码完全封闭。

  • (合成复用) CRPOldPrinterAdapter不是extends OldPrinter(可能也继承不了,因为是第三方 final 类),而是内部has a OldPrinter

  • (依赖倒转) DIPDocumentService只认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方法。DataImporterimportData骨架和readFilevalidatesaveToDatabase完全不用改。

  • (依赖倒转) DIPDataImporter作为父类,它的importData方法调用的是抽象的parse方法,而不是某个具体的解析逻辑。它依赖的是"抽象步骤",不是"具体实现"。

  • (单一职责) SRPCsvImporter只懂 CSV;ExcelImporter只懂 Excel。解析逻辑和通用流程彻底分离。


九、速查表:模式 ↔ 原则(带代码气味)

设计模式主要体现的原则代码里的"识别气味"
策略模式(开闭原则) OCP、(依赖倒转) DIP、(单一职责) SRP接口作为参数传入构造器,运行时切换算法
装饰模式(开闭原则) OCP、(合成复用) CRP、(单一职责) SRP构造器传入同类接口,方法里先干自己的事再super.write()委托
代理模式(单一职责) SRP、(依赖倒转) DIP、(迪米特法则) LoD代理类内部new真实对象,客户端只接触代理接口
工厂方法(开闭原则) OCP、(依赖倒转) DIP、(单一职责) SRPnew语句隔离到子类,工厂方法返回抽象类型
观察者模式(开闭原则) OCP、(依赖倒转) DIP、(单一职责) SRP主题维护List<接口>,遍历调用接口方法广播通知
适配器模式(开闭原则) OCP、(合成复用) CRP、(依赖倒转) DIP包装旧接口,内部做方法名/参数转换,不改旧代码
模板方法(开闭原则) OCP、(依赖倒转) DIP、(单一职责) SRP父类定义final骨架,抽象方法留给子类填"钩子"

十、一句话心法

判断一段代码是否体现了某条原则,不要看注释里写了什么,只看三个地方:

  1. 成员变量声明的类型是接口还是具体类?—— 这是 (依赖倒转) DIP

  2. 新增功能时是新增文件还是修改旧文件?—— 这是 (开闭原则) OCP

  3. 类名和类里的方法是不是都在说同一件事?—— 这是 (单一职责) SRP

把这三个问题套到任何设计模式的代码上,那些原本像标语一样的原则,就会一行一行地钉在代码里,看得见、摸得着。