设计模式中的原则

本文是【GoF设计模式】系列的前置篇

前言

本篇可以当做设计模式学习前的入门,也可以当成设计模式学习后的复习。

先看一段典型的"面条代码":

java 复制代码
// 一个臃肿的 UserService:校验、持久化、通知、日志全堆在一起
public class UserService {
    public void register(User user) {
        if (user.getName() == null || user.getName().isEmpty()) {
            throw new IllegalArgumentException("用户名不能为空");
        }
        Connection conn = DriverManager.getConnection("jdbc:mysql://localhost/db", "root", "root");
        // 保存、发邮件、记日志全在这里:换数据库要改源码,也无法单独测试
    }
}

这段代码职责过多、强依赖具体实现、难以替换和测试。随着代码增长,这类结构会越来越难维护。设计原则正是从这些痛点中提炼出的经验总结,能让代码组织得更灵活、可维护、可测试。

面向对象设计中有几个经典原则,前五个常被合称为 SOLID

缩写 原则 核心要义
S 单一职责原则(SRP) 一个类只承担一个职责
O 开闭原则(OCP) 对扩展开放,对修改关闭
L 里氏代换原则(LSP) 子类必须能替换父类
I 接口隔离原则(ISP) 不依赖不需要的接口
D 依赖倒转原则(DIP) 依赖抽象,不依赖细节

此外还有迪米特法则(LoD)和合成/聚合复用原则(CARP)。下面逐一介绍。

单一职责原则(SRP)

单一职责原则(Single Responsibility Principle):就一个类而言,应该仅有一个引起它变化的原因

如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能削弱或抑制它完成其他职责的能力,导致脆弱的设计。软件设计的重要内容之一,就是发现职责并把它们相互分离。

判断一个类是否职责过多,可以看是否能想到多个动机去改变它;或用一句话描述它的职责,若出现"和""或"等连接词,往往说明违反了该原则。

违反 SRP 的设计

java 复制代码
// 一个类承担了数据管理、权限验证、消息通知三个职责
public class UserService {
    public void saveUser(User user) { /* 保存用户 */ }
    public boolean checkPermission(User user, String resource) { /* 检查权限 */ }
    public void sendEmail(User user, String message) { /* 发送邮件 */ }
}

遵循 SRP 的设计

java 复制代码
// 按职责拆分,每个类只负责一件事
public class UserService {
    public void saveUser(User user) { /* 保存用户 */ }
}

public class PermissionService {
    public boolean checkPermission(User user, String resource) { /* 检查权限 */ }
}

public class NotificationService {
    public void sendEmail(User user, String message) { /* 发送邮件 */ }
}
优点 缺点
降低类的复杂度,每个类只负责一项职责 增加类的数量,系统复杂度上升
提高代码可读性与可维护性 过度拆分会增加类间依赖
修改一个职责不影响其他职责 需要合理判断职责边界

开闭原则(OCP)

开闭原则(Open-Closed Principle):软件实体(类、模块、函数等)应该可以扩展,但是不可修改

即对扩展开放、对修改封闭。无论模块多么封闭,都会存在无法封闭的变化,设计人员需要猜测最可能发生的变化,再构造抽象来隔离它们。开闭原则是面向对象设计的核心,遵循它能带来可维护、可扩展、可复用、灵活性好等好处。但拒绝不成熟的抽象和抽象本身一样重要,不要对每个部分都刻意抽象。

实现开闭原则的常见方式:

  1. 抽象与多态:用接口或抽象类定义抽象层,客户端依赖抽象而非具体实现
  2. 参数化:把变化的部分抽象为参数(策略对象、回调)
  3. 配置化:可变部分提取到配置文件,运行时读取配置决定实现
  4. 元数据驱动:用注解驱动框架行为,新增逻辑靠加注解而非改框架

违反 OCP 的设计

java 复制代码
// 每次新增支付方式都要修改这里的 if/else
public class PaymentProcessor {
    public void processPayment(String type, BigDecimal amount) {
        if ("alipay".equals(type)) { /* 支付宝 */ }
        else if ("wechat".equals(type)) { /* 微信 */ }
    }
}

遵循 OCP 的设计

java 复制代码
// 通过抽象扩展,新增支付方式只需新增类,不改老代码
public interface PaymentStrategy {
    void pay(BigDecimal amount);
}

public class AlipayStrategy implements PaymentStrategy {
    public void pay(BigDecimal amount) { /* 支付宝 */ }
}

public class PaymentProcessor {
    private PaymentStrategy strategy;
    public PaymentProcessor(PaymentStrategy strategy) { this.strategy = strategy; }
    public void processPayment(BigDecimal amount) { strategy.pay(amount); }
}
优点 缺点
提高系统稳定性,减少回归测试 需要预判变化,增加设计难度
提高代码复用性与可维护性 增加抽象层,提高复杂度
新功能通过扩展实现,降低修改风险 过度抽象会让代码难懂

依赖倒转原则(DIP)

依赖倒转原则(Dependency Inversion Principle):

  1. 高层不应依赖低层,两者都依赖抽象
  2. 抽象不应依赖细节,细节应依赖抽象

依赖倒转可以说是面向对象设计的标志:如果编写时考虑的都是针对抽象编程而非细节编程,即所有依赖关系都终止于抽象类或接口,就是面向对象的设计,反之就是过程化设计。传统过程化设计中高层依赖低层,依赖倒转把这个关系"倒转"过来,让两者都依赖抽象,从而解耦。

实现方式是高层模块定义接口、低层模块实现接口,再用依赖注入把低层模块注入高层模块,做到面向接口编程而非面向实现编程。

违反 DIP 的设计

java 复制代码
// 高层直接 new 低层具体实现,换数据库要改源码
public class UserService {
    private MySQLUserDao userDao = new MySQLUserDao();
    public User getUser(int id) { return userDao.findById(id); }
}

遵循 DIP 的设计

java 复制代码
// 高层依赖抽象,低层实现抽象,通过构造器注入
public interface UserDao {
    User findById(int id);
}

public class MySQLUserDao implements UserDao {
    public User findById(int id) { /* MySQL 查询 */ }
}

public class UserService {
    private UserDao userDao;
    public UserService(UserDao userDao) { this.userDao = userDao; }
    public User getUser(int id) { return userDao.findById(id); }
}
优点 缺点
降低类间耦合,提高系统稳定性 增加抽象层,提高复杂度
提高可扩展性,便于替换具体实现 需要较好的抽象能力
便于并行开发与单元测试(轻松 Mock 依赖) 依赖注入框架增加学习成本

里氏代换原则(LSP)

里氏代换原则(Liskov Substitution Principle):子类型必须能替换父类型

只有当子类可以替换父类、软件单位的功能不受影响时,父类才能真正被复用,子类也能在父类基础上增加新行为。里氏代换是继承复用的基石,只有子类能完全替换父类时,继承才是合理的设计。

经典反例是"正方形继承矩形":正方形重写 setWidth/setHeight 强制宽高相等,导致在期望矩形的地方替换为正方形时面积计算出错。正确做法是让矩形和正方形共同实现一个 Shape 接口,而非用继承强行关联。Java 集合框架中任何使用 List 接口的地方都能无缝替换为 ArrayListLinkedList,就是该原则的体现。

违反 LSP 的设计

java 复制代码
// 正方形继承矩形,重写方法强制宽高相等,替换后面积计算出错
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 {
    public void setWidth(int w) { width = w; height = w; }
    public void setHeight(int h) { width = h; height = h; }
}

遵循 LSP 的设计

java 复制代码
// 矩形和正方形共同实现 Shape 接口,不再用继承强行关联
public interface Shape {
    int getArea();
}

public class Rectangle implements Shape {
    private int width, height;
    public Rectangle(int w, int h) { width = w; height = h; }
    public int getArea() { return width * height; }
}

public class Square implements Shape {
    private int side;
    public Square(int side) { this.side = side; }
    public int getArea() { return side * side; }
}
优点 缺点
保证继承复用的正确性 限制继承的灵活性
提高可维护性,子类不破坏父类行为 需仔细设计继承关系
便于统一测试父类行为 可能导致类层次变深

迪米特法则(LoD)

迪米特法则(Law of Demeter),又叫最少知识原则:如果两个类不必直接通信,就不应直接相互作用,需要调用时通过第三者转发

它强调每个类都应尽量降低成员的访问权限,一个对象应该对其他对象有尽可能少的了解,只与"直接朋友"通信,不跟"陌生人"说话。

  • 直接朋友:对象自身、成员对象、方法参数、方法内创建的对象
  • 陌生人:通过方法返回值间接获得的对象等

a.getB().doSomething() 这种链式调用属于"和陌生人说话",违反该法则。Java 三层架构中 Controller 只调用 Service、Service 只调用 Repository,就是迪米特法则的典型应用。

违反 LoD 的设计

java 复制代码
// 房客直接与房东、银行等多个"陌生人"交互
public class Tenant {
    public void rentRoom() {
        Landlord landlord = new Landlord();
        Bank bank = new Bank();
        landlord.negotiate();  // 直接调用房东
        bank.transfer();       // 直接调用银行
    }
}

遵循 LoD 的设计

java 复制代码
// 房客只与中介(直接朋友)交互,由中介转发调用
public class Tenant {
    private Agent agent;
    public void rentRoom() { agent.rentRoom(); }
}

public class Agent {
    private Landlord landlord;
    private Bank bank;
    public void rentRoom() {
        landlord.negotiate();
        bank.transfer();
    }
}
优点 缺点
降低类间耦合,提高模块独立性 可能产生大量"中介"类
提高可读性与可维护性 过度使用会使结构复杂
修改一个类影响范围更小 可能增加方法调用层数

接口隔离原则(ISP)

接口隔离原则(Interface Segregation Principle):客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应建立在最小接口上

要建立单一的接口,不要建立庞大臃肿的接口,接口中的方法应尽量少,只包含客户端需要的方法。接口过大时,实现类被迫实现用不到的方法,产生冗余代码。这里的"隔离"不是物理隔离,而是通过拆分接口让客户端只依赖需要的方法,与不需要的方法隔离。

与单一职责原则的区别:

对比维度 单一职责原则(SRP) 接口隔离原则(ISP)
关注点 类的职责(业务功能) 接口的依赖范围
判断依据 引起变化的原因 客户端需要的方法
拆分对象 接口

两者经常配合:SRP 保证类职责单一,ISP 保证接口粒度合适。典型例子是 Java 集合的 Iterable/Collection/List/RandomAccess 接口层次,客户端可按需依赖最小接口。

违反 ISP 的设计

java 复制代码
// 臃肿接口:机器人被迫实现用不到的 eat、sleep
public interface Worker {
    void work();
    void eat();
    void sleep();
}

public class RobotWorker implements Worker {
    public void work() { /* 工作 */ }
    public void eat() { }    // 空实现,机器人不吃饭
    public void sleep() { }  // 空实现,机器人不睡觉
}

遵循 ISP 的设计

java 复制代码
// 拆分接口,机器人只实现需要的接口
public interface Workable { void work(); }

public class RobotWorker implements Workable {
    public void work() { /* 工作 */ }
}
优点 缺点
降低接口耦合,客户端只依赖需要的方法 接口数量增多,复杂度上升
提高内聚性,接口职责单一 拆分过细可能导致类爆炸
减少冗余实现,扩展不影响已有接口 需合理判断接口粒度

合成/聚合复用原则(CARP)

合成/聚合复用原则(Composite/Aggregate Reuse Principle):优先用合成/聚合,少用类继承

继承关系在编译时就固定下来,无法在运行时改变;子类与父类紧密耦合,父类的任何变化都会波及子类,限制了灵活性和复用性。合成/聚合则保持每个类的封装性,让类继承层次保持较小规模。

合成与聚合都是关联的特殊种类:聚合是弱的"拥有"关系(部分可独立存在,如学生与班级),合成是强的"拥有"关系(部分与整体同生命周期,如人与心脏)。

违反 CARP 的设计

java 复制代码
// 用继承复用引擎:汽车 IS-A 引擎?逻辑不对,且引擎变化会波及汽车
public class Car extends Engine {
    // 引擎的任何改动都会影响汽车
}

遵循 CARP 的设计

java 复制代码
// 用合成复用引擎:汽车 HAS-A 引擎,运行时可切换
public interface Engine {
    void start();
}

public class Car {
    private Engine engine;
    public Car(Engine engine) { this.engine = engine; }
    public void start() { engine.start(); }
}
对比维度 继承 合成/聚合
耦合度 高(编译时绑定) 低(运行时可替换)
灵活性
复用性 受限于父类实现 可组合多个对象
封装性 破坏封装 保持封装
适用场景 IS-A 关系(狗是动物) HAS-A 关系(汽车有引擎)
优点 缺点
降低类间耦合度 可能产生大量小类
提高灵活性与可扩展性 对象组合可能让结构复杂
支持运行时动态组合、保持封装 初期设计成本较高

原则与设计模式的对应

设计原则是设计模式的"灵魂",每个 GoF 模式背后都对应着一条或多条原则。理解这层映射,能在遇到问题时快速定位该用哪个模式:

原则 典型对应的设计模式 体现方式
单一职责(SRP) 门面模式、桥接模式 按职责拆分类与接口
开闭(OCP) 策略、装饰、观察者、模板方法 通过新增类扩展功能,不改已有代码
里氏代换(LSP) 策略、模板方法、状态 子类安全替换父类,多态有意义
接口隔离(ISP) 门面模式、适配器模式 为客户端提供最小接口
依赖倒转(DIP) 工厂方法、抽象工厂、依赖注入 高层依赖抽象,低层实现抽象
迪米特法则 中介者、外观模式 通过中介/门面减少直接交互
合成/聚合复用(CARP) 桥接、装饰、策略、代理、组合 用组合代替继承

原则之间的关系

这些原则并非孤立存在,而是相互支撑、形成完整的面向对象设计思想体系:

复制代码
                    开闭原则(核心目标)
                          ↑
           ┌──────────────┼──────────────┐
           │              │              │
    单一职责原则    依赖倒转原则    里氏代换原则
    (类的拆分)    (抽象层解耦)   (继承的正确使用)
           │              │              │
           └──────────────┼──────────────┘
                          ↓
               接口隔离原则(接口拆分)
                          ↓
               迪米特法则(降低耦合)
                          ↓
               合成/聚合复用原则(复用方式)

开闭原则是核心目标,让系统易于扩展、难以修改;单一职责通过职责拆分为开闭原则创造条件;依赖倒转通过抽象层解耦实现开闭原则;里氏代换保证继承的正确性,使多态有意义;接口隔离通过拆分臃肿接口让依赖更精确;迪米特法则通过减少通信降低耦合;合成/聚合复用通过组合提高灵活性和复用性。

这些原则有时也会相互掣肘,需要权衡:

  • 接口隔离拆接口 vs 合成复用导致类爆炸:只有当不同客户端确实只用到接口的一部分时才拆分
  • 依赖倒转抽象层 vs 简单性:抽象的前提是变化真实存在或可预见,变体只有一种时直接依赖具体实现反而更清晰
  • 迪米特减少通信 vs 中介类泛滥:中介只在需要降低"跨层耦合"时引入,不为内部协作加壳

原则不是强制规定,不要为了遵循原则而过度设计。先让代码简单正确地跑起来,等到变化真正出现时再重构引入抽象------重复三次的代码才考虑抽象

记忆口诀

  • 拆分靠 SRP:一个类只做一件事
  • 扩展靠 OCP:加功能不改老代码
  • 换实现靠 DIP:都依赖抽象,运行时替换
  • 继承看 LSP:子类能替父类,多态才安全
  • 接口靠 ISP:用不到的方法别塞过来
  • 通信靠 LoD:只跟直接朋友说话
  • 复用靠 CARP:能用组合就别用继承

一句话总纲:先简单正确,再按需抽象;拆得清职责,换得动实现,扩得起新功能。

相关推荐
安冬的码畜日常3 小时前
【工欲善其事】深入理解 Node.js 带并发上限的异步任务批量执行逻辑
javascript·设计模式·node.js·ai编程·异步编程·并发执行
腾科IT教育12 小时前
Oracle认证怎么选?2026年OCP/OCM报考指南
linux·运维·华为认证·hcie·开闭原则·datacom
AI大法师15 小时前
一个机场如何被做成 IP 场景:宝可梦机场的设计方法总结
大数据·人工智能·设计模式·新媒体运营
晚安code18 小时前
干掉成山的 if-else:工厂造、策略选,一文讲透两个模式的配合
后端·设计模式
workflower1 天前
从幻觉,到现实
人工智能·深度学习·机器学习·设计模式·机器人
电子科技圈1 天前
第二代无线平台历久弥新,赋能物联网创新迭代
人工智能·嵌入式硬件·mcu·物联网·设计模式·硬件架构·iot
choumin2 天前
创建型模式——工厂方法模式
c++·设计模式·工厂方法模式·创建型模式
choumin2 天前
创建型模式——原型模式
c++·设计模式·原型模式·创建型模式
37.2℃9952 天前
Claude Design哪个公司技术好
python·设计模式