news 2026/7/29 1:23:33

设计模式中的原则

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
设计模式中的原则

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

前言

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

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

// 一个臃肿的 UserService:校验、持久化、通知、日志全堆在一起publicclassUserService{publicvoidregister(Useruser){if(user.getName()==null||user.getName().isEmpty()){thrownewIllegalArgumentException("用户名不能为空");}Connectionconn=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 的设计

// 一个类承担了数据管理、权限验证、消息通知三个职责publicclassUserService{publicvoidsaveUser(Useruser){/* 保存用户 */}publicbooleancheckPermission(Useruser,Stringresource){/* 检查权限 */}publicvoidsendEmail(Useruser,Stringmessage){/* 发送邮件 */}}

遵循 SRP 的设计

// 按职责拆分,每个类只负责一件事publicclassUserService{publicvoidsaveUser(Useruser){/* 保存用户 */}}publicclassPermissionService{publicbooleancheckPermission(Useruser,Stringresource){/* 检查权限 */}}publicclassNotificationService{publicvoidsendEmail(Useruser,Stringmessage){/* 发送邮件 */}}
优点缺点
降低类的复杂度,每个类只负责一项职责增加类的数量,系统复杂度上升
提高代码可读性与可维护性过度拆分会增加类间依赖
修改一个职责不影响其他职责需要合理判断职责边界

开闭原则(OCP)

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

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

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

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

违反 OCP 的设计

// 每次新增支付方式都要修改这里的 if/elsepublicclassPaymentProcessor{publicvoidprocessPayment(Stringtype,BigDecimalamount){if("alipay".equals(type)){/* 支付宝 */}elseif("wechat".equals(type)){/* 微信 */}}}

遵循 OCP 的设计

// 通过抽象扩展,新增支付方式只需新增类,不改老代码publicinterfacePaymentStrategy{voidpay(BigDecimalamount);}publicclassAlipayStrategyimplementsPaymentStrategy{publicvoidpay(BigDecimalamount){/* 支付宝 */}}publicclassPaymentProcessor{privatePaymentStrategystrategy;publicPaymentProcessor(PaymentStrategystrategy){this.strategy=strategy;}publicvoidprocessPayment(BigDecimalamount){strategy.pay(amount);}}
优点缺点
提高系统稳定性,减少回归测试需要预判变化,增加设计难度
提高代码复用性与可维护性增加抽象层,提高复杂度
新功能通过扩展实现,降低修改风险过度抽象会让代码难懂

依赖倒转原则(DIP)

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

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

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

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

违反 DIP 的设计

// 高层直接 new 低层具体实现,换数据库要改源码publicclassUserService{privateMySQLUserDaouserDao=newMySQLUserDao();publicUsergetUser(intid){returnuserDao.findById(id);}}

遵循 DIP 的设计

// 高层依赖抽象,低层实现抽象,通过构造器注入publicinterfaceUserDao{UserfindById(intid);}publicclassMySQLUserDaoimplementsUserDao{publicUserfindById(intid){/* MySQL 查询 */}}publicclassUserService{privateUserDaouserDao;publicUserService(UserDaouserDao){this.userDao=userDao;}publicUsergetUser(intid){returnuserDao.findById(id);}}
优点缺点
降低类间耦合,提高系统稳定性增加抽象层,提高复杂度
提高可扩展性,便于替换具体实现需要较好的抽象能力
便于并行开发与单元测试(轻松 Mock 依赖)依赖注入框架增加学习成本

里氏代换原则(LSP)

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

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

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

违反 LSP 的设计

// 正方形继承矩形,重写方法强制宽高相等,替换后面积计算出错publicclassRectangle{protectedintwidth,height;publicvoidsetWidth(intw){width=w;}publicvoidsetHeight(inth){height=h;}publicintgetArea(){returnwidth*height;}}publicclassSquareextendsRectangle{publicvoidsetWidth(intw){width=w;height=w;}publicvoidsetHeight(inth){width=h;height=h;}}

遵循 LSP 的设计

// 矩形和正方形共同实现 Shape 接口,不再用继承强行关联publicinterfaceShape{intgetArea();}publicclassRectangleimplementsShape{privateintwidth,height;publicRectangle(intw,inth){width=w;height=h;}publicintgetArea(){returnwidth*height;}}publicclassSquareimplementsShape{privateintside;publicSquare(intside){this.side=side;}publicintgetArea(){returnside*side;}}
优点缺点
保证继承复用的正确性限制继承的灵活性
提高可维护性,子类不破坏父类行为需仔细设计继承关系
便于统一测试父类行为可能导致类层次变深

迪米特法则(LoD)

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

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

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

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

违反 LoD 的设计

// 房客直接与房东、银行等多个“陌生人”交互publicclassTenant{publicvoidrentRoom(){Landlordlandlord=newLandlord();Bankbank=newBank();landlord.negotiate();// 直接调用房东bank.transfer();// 直接调用银行}}

遵循 LoD 的设计

// 房客只与中介(直接朋友)交互,由中介转发调用publicclassTenant{privateAgentagent;publicvoidrentRoom(){agent.rentRoom();}}publicclassAgent{privateLandlordlandlord;privateBankbank;publicvoidrentRoom(){landlord.negotiate();bank.transfer();}}
优点缺点
降低类间耦合,提高模块独立性可能产生大量"中介"类
提高可读性与可维护性过度使用会使结构复杂
修改一个类影响范围更小可能增加方法调用层数

接口隔离原则(ISP)

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

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

与单一职责原则的区别:

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

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

违反 ISP 的设计

// 臃肿接口:机器人被迫实现用不到的 eat、sleeppublicinterfaceWorker{voidwork();voideat();voidsleep();}publicclassRobotWorkerimplementsWorker{publicvoidwork(){/* 工作 */}publicvoideat(){}// 空实现,机器人不吃饭publicvoidsleep(){}// 空实现,机器人不睡觉}

遵循 ISP 的设计

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

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

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

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

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

违反 CARP 的设计

// 用继承复用引擎:汽车 IS-A 引擎?逻辑不对,且引擎变化会波及汽车publicclassCarextendsEngine{// 引擎的任何改动都会影响汽车}

遵循 CARP 的设计

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

原则与设计模式的对应

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

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

原则之间的关系

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

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

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

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

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

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

记忆口诀

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

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/29 1:22:26

如何通过智能直链解析工具提升网盘文件下载效率

如何通过智能直链解析工具提升网盘文件下载效率 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 ,支持 百度网盘 / 阿里云盘 / 中国移动云盘 / 天翼云盘 / 迅雷云盘 /…

作者头像 李华
网站建设 2026/7/29 1:21:10

基于YOLOv8行人车辆检测系统

本项目面向道路交通场景中的行人与车辆目标检测任务,完成 YOLOv8 与 Faster R-CNN 两类检测模型的训练、评估、可视化对比,并集成 PyQt5 桌面端检测系统。系统支持图片检测、视频检测、摄像头实时检测、检测数量统计、历史记录保存以及 CSV / JSON 数据导…

作者头像 李华
网站建设 2026/7/29 1:19:05

接 AI API 别只问支不支持新模型,也要看倍率和日志

最近接 AI API 网关时,很多人只关心一个问题:有没有最新模型入口。 比如 GPT、Claude、Gemini、DeepSeek、GLM、Kimi、Grok 这些模型能不能用,当然重要。但长期项目里,我更建议把第一次测试拆成一个可复盘流程: 1. 先查…

作者头像 李华
网站建设 2026/7/29 1:18:59

数字人短视频矩阵方案落地:4个工程级踩坑分析与解决思路

数字人短视频矩阵是2026年内容自动化领域的热门方向。方案设计阶段看起来很清晰:形象克隆 → 批量口播生成 → 多账号分发 → 流量回收。但实际部署后,大量项目在3个月内停摆。不是技术选型的问题,是工程化落地时的认知偏差。以下是对200矩阵…

作者头像 李华
网站建设 2026/7/29 1:17:29

2026一站式AI电商创作平台推荐 全链路能力测评指南

2026AI电商创作平台测评速览本次测评覆盖3款主流一站式AI电商创作平台,从功能覆盖、流程连贯、数据打通、易用性四个维度展开,纳米P视频在电商专属场景适配度上表现突出一站式平台可解决多工具切换带来的成本高、数据不通、学习门槛高三大痛点&#xff0…

作者头像 李华
网站建设 2026/7/29 1:11:13

Unity UI进阶:UI布局组件(Horizontal/Vertical Layout Group)使用

Unity UI进阶:UI布局组件(Horizontal/Vertical Layout Group)使用📚 本章学习目标:深入理解UI布局组件(Horizontal/Vertical Layout Group)使用的核心概念与实践方法,掌握关键技术要…

作者头像 李华