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

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

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

这篇博客把最常用的七个设计模式(策略、装饰、代理、工厂方法、观察者、适配器、模板方法)拆开,告诉你每一行代码在守护哪条原则。代码注释里不会只扔一个"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);
    }
}

原则落点总结

  • (单一职责) 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

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值