软件设计原则详解:开闭原则、里氏替换原则、迪米特法则
软件设计三大核心原则(开闭+里氏替换+依赖倒置)全网最细讲解,附Java正反例|面试必背
在日常开发中,你一定遇到过这些痛点:
- 加个小功能,改出一堆Bug
- 继承乱用,逻辑越跑越偏
- 换个数据库/组件,要改几十处代码
这些问题,本质都是没遵守软件设计原则。今天就把面试+工作最常用的三大核心设计原则讲透:开闭原则、里氏替换原则、依赖倒置原则。全文配Java代码正反例,看完就能用在项目里。
前言
设计原则不是玄学,是前人总结的代码健壮性、可扩展、可维护的底层规律。
今天讲的三大原则地位:
- 开闭原则(OCP):设计原则的核心
- 里氏替换原则(LSP):继承的黄金标准
- 依赖倒置原则(DIP):解耦的终极手段
一、开闭原则 OCP
1. 核心思想
对扩展开放,对修改关闭。
- 扩展:新增功能、新增类、新增实现
- 关闭:不修改已测试、已上线的稳定代码
一句话:能加就不加改,能扩就不改旧。
2. 为什么要遵守?
- 不改老代码 = 不引入新Bug
- 系统更稳定、可维护性更强
- 符合“高内聚低耦合”
生活例子:电脑USB接口。插U盘、鼠标、键盘不用拆电脑,这就是开闭原则。
3. 反例(违反OCP)
// 绘制工具类:用if-else判断类型,违反开闭publicclassShapeDrawer{publicvoiddrawShape(StringshapeType){if(shapeType.equals("Circle")){System.out.println("绘制圆形");}elseif(shapeType.equals("Square")){System.out.println("绘制正方形");}}}问题:
- 加三角形必须加
else if - 改老代码,风险极高
- 完全违反开闭原则
4. 正例(遵守OCP)
Step1:定义抽象接口
publicinterfaceShape{voiddraw();}Step2:具体图形实现
publicclassCircleimplementsShape{@Overridepublicvoiddraw(){System.out.println("绘制圆形");}}publicclassSquareimplementsShape{@Overridepublicvoiddraw(){System.out.println("绘制正方形");}}Step3:稳定的绘制工具
publicclassShapeDrawer{// 依赖抽象,不依赖具体publicvoiddrawShape(Shapes){s.draw();}}扩展三角形(完全不用改旧代码)
publicclassTriangleimplementsShape{@Overridepublicvoiddraw(){System.out.println("绘制三角形");}}5. 开闭原则总结
- 核心:扩展不改旧
- 关键:面向抽象/接口编程
- 目的:系统稳定、易扩展、低风险
二、里氏替换原则 LSP
1. 核心思想
子类可以完全替换父类,程序行为不变。
父类能用的地方,子类换上去照样跑,逻辑不崩、结果不错。
这是继承是否合理的唯一标准。
2. 核心理解
- 继承不是为了复用代码
- 继承是为了行为统一
- 子类必须是父类的真正子类型
3. 经典反例(正方形≠长方形)
// 父类:长方形classRectangle{intwidth;intheight;voidsetWidth(intw){width=w;}voidsetHeight(inth){height=h;}}// 子类:正方形(错误继承)classSquareextendsRectangle{voidsetWidth(intw){width=height=w;}voidsetHeight(inth){width=height=h;}}测试逻辑会崩:
voidresize(Rectangler){r.setWidth(20);// 预期高度不变}问题:
- 子类破坏父类行为约定
- 无法替换父类
- 违反里氏替换
4. 正例(遵守LSP)
Step1:抽象父类
publicabstractclassShape{publicabstractintgetArea();}Step2:各自实现
publicclassRectangleextendsShape{privateintw,h;publicRectangle(intw,inth){this.w=w;this.h=h;}@OverridepublicintgetArea(){returnw*h;}}publicclassSquareextendsShape{privateintside;publicSquare(intside){this.side=side;}@OverridepublicintgetArea(){returnside*side;}}Step3:任意替换
Shapes1=newRectangle(10,20);Shapes2=newSquare(10);行为完全一致,安全替换。
5. 里氏替换总结
- 核心:子类能替父类,行为不跑偏
- 关键:继承看行为,不看代码复用
- 目的:保证多态安全、系统稳定
三、依赖倒置原则 DIP
1. 核心思想
高层不依赖低层,二者都依赖抽象。
抽象不依赖细节,细节依赖抽象。
大白话:
- 别直接new具体类
- 依赖接口/抽象类
- 实现可以随便换,高层不动
2. 为什么重要?
- 解耦!解耦!解耦!
- 换组件不用改高层
- 方便测试、方便扩展
- 是Spring/IOC的核心思想
3. 反例(高层依赖具体实现)
// 低层:MySQL实现publicclassMySQLDao{publicvoidqueryUser(){System.out.println("MySQL查询用户");}}// 高层:直接new死,强耦合publicclassUserService{privateMySQLDaomySQLDao=newMySQLDao();publicvoidgetUserInfo(){mySQLDao.queryUser();}}问题:
- 换Oracle必须改UserService
- 紧耦合、难扩展、难测试
4. 正例(依赖抽象)
Step1:抽象接口
publicinterfaceUserDao{voidqueryUser();}Step2:具体实现
publicclassMySQLDaoimplementsUserDao{@OverridepublicvoidqueryUser(){System.out.println("MySQL查询");}}publicclassOracleDaoimplementsUserDao{@OverridepublicvoidqueryUser(){System.out.println("Oracle查询");}}Step3:高层依赖抽象
publicclassUserService{privateUserDaouserDao;// 注入:构造/Setter都行publicUserService(UserDaodao){this.userDao=dao;}publicvoidgetUserInfo(){userDao.queryUser();}}使用:
publicclassTest{publicstaticvoidmain(String[]args){// 随意切换,高层代码完全不动UserServiceservice=newUserService(newMySQLDao());service.getUserInfo();service=newUserService(newOracleDao());service.getUserInfo();}}5. 依赖倒置总结
- 核心:依赖抽象,不依赖具体
- 关键:面向接口编程、依赖注入
- 目的:极致解耦、高灵活、易扩展
四、三大原则终极总结
| 原则 | 英文 | 一句话口诀 | 核心解决 |
|---|---|---|---|
| 开闭原则 | OCP | 能扩不改旧 | 扩展风险、稳定性 |
| 里氏替换 | LSP | 子类能替父类 | 继承安全、多态正确 |
| 依赖倒置 | DIP | 依赖抽象不绑死 | 解耦、扩展、替换 |
一句话串起来:
用里氏替换保证继承正确,用依赖倒置实现解耦,最终达到开闭原则——扩展不改、系统稳定。
五、课堂小测(面试常考)
- 新增支付方式,直接修改支付工具类 → 违反开闭原则
- 子类替换父类后逻辑异常 → 违反里氏替换原则
- Service直接new Dao,换库要改代码 → 违反依赖倒置原则
六、总结
这三大原则是设计模式的基础,也是高级工程师必备素养。
- 简单工厂 → 工厂方法,就是为了遵守开闭
- Spring IOC/AOP,核心就是依赖倒置
- 合理继承,必须遵守里氏替换
建议收藏,每次写代码前对照一遍,代码质量直接上一个档次。
