03.开闭原则详细介绍
目录介绍
- 01.问题思考的分析
- 02.如何理解开闭原则
- 03.开闭原则的背景
- 04.开闭原则比较难学
- 05.实现开闭原则方式
- 06.画图形案例分析
- 07.银行业务案例分析
- 08.开闭原则优缺点
- 09.开闭原则的总结
推荐一个好玩网站
一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网
https://yccoding.com/
设计模式Git项目地址:https://github.com/yangchong211/YCDesignBlog
本文摘要介绍
本文详细介绍了面向对象设计中的开闭原则(OCP),即软件实体应对扩展开放、对修改关闭。文章通过问题思考、背景分析、实现方式及案例教学,深入探讨了如何在实际开发中应用这一原则,以提高代码的可维护性和扩展性。
文中还结合图形绘制和银行业务两个具体案例,展示了如何通过抽象类、接口、策略模式等技术手段实现开闭原则。最后总结了开闭原则的优缺点,并强调了其在设计模式中的重要地位。
01.问题思考的分析
- 什么叫作开闭原则,他的主要用途是什么?
- 如何做到拓展开放,修改封闭这一准则,结合案例说一下如何实现?
- 你平常是如何理解开闭原则的,判断的标准是什么?
02.如何理解开闭原则
开闭原则的英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。
我们把它翻译成中文就是:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。
这个描述比较简略,如果我们详细表述一下,那就是,添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
03.开闭原则的背景
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本已经经过测试的旧代码中,破坏原有系统。
因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。
当然,在现实开发中,只通过继承的方式来升级、维护原有系统只是一个理想化的愿景,因此,在实际的开发过程中,修改原有代码、扩展代码往往是同时存在的。
软件开发过程中,最不会变化的就是变化本身。产品需要不断地升级、维护,没有一个产品从第一版本开发完就再没有变化了,除非在下个版本诞生之前它已经被终止。
产品需要升级,修改原来的代码就可能会引发其他的问题。那么如何确保原有软件模块的正确性,以及尽量少地影响原有模块,答案就是尽量遵守本章要讲述的开闭原则。
04.开闭原则比较难学
个人觉得,开闭原则是 SOLID 中最难理解、最难掌握,同时也是最有用的一条原则。
之所以说这条原则难理解,那是因为,“怎样的代码改动才被定义为‘扩展’?怎样的代码改动才被定义为‘修改’?怎么才算满足或违反‘开闭原则’?修改代码就一定意味着违反‘开闭原则’吗?”等等这些问题,都比较难理解。
之所以说这条原则难掌握,那是因为,“如何做到‘对扩展开放、修改关闭’?如何在项目中灵活地应用‘开闭原则’,以避免在追求扩展性的同时影响到代码的可读性?”等等这些问题,都比较难掌握。
之所以说这条原则最有用,那是因为,扩展性是代码质量最重要的衡量标准之一。在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则。
05.实现开闭原则方式
为了实现开闭原则,常用的设计技术有以下几种:
- 抽象类和接口:通过定义抽象类和接口来约定行为,然后通过继承和实现这些抽象类和接口来扩展功能。
- 策略模式:将算法的实现分离到不同的类中,通过组合方式来实现不同的行为。
- 装饰器模式:通过对对象进行包装,动态地添加新的行为或功能。
06.画图形案例分析
6.1 普通案例实现
假设有一个图形绘制程序,程序需要能够绘制不同形状的图形,比如矩形、圆形和三角形。最初的设计可能会像这样:
public class GraphicEditor {public void draw(Shape shape) {if (shape.m_type == 1) {drawRectangle();} else if(shape.m_type == 2) {drawCircle();}}public void drawRectangle() {System.out.println("画长方形");}public void drawCircle() {System.out.println("画圆形");}class Shape {int m_type;}class Rectangle extends Shape {Rectangle() {super.m_type=1;}}class Circle extends Shape {Circle() {super.m_type=2;}}
}
我们来看看,这个代码,初看是符合要求了,再想想,要是我增加一种形状呢? 比如增加三角形.
- 首先,要增加一个三角形的类, 继承自Shape
- 第二,要增加一个画三角形的方法drawTriage()
- 第三,在draw方法中增加一种类型type=3的处理方案
在这个设计中,每当我们需要添加新的图形类型,就需要修改 GraphicEditor 类,添加新的 if 条件。我们需要修改已有的代码来实现新功能。
这就违背了开闭原则-对扩展开发,对修改关闭。增加一个类型,修改了三处代码。
6.2 开闭原则演变
为了符合开闭原则,我们可以进行重构。首先,我们定义一个抽象类Shape
:
public class GraphicEditor {public void draw(Shape shape) {shape.draw();}interface Shape {void draw();}class Rectangle implements Shape {@Overridepublic void draw() {System.out.println("画矩形");}}class Circle implements Shape {@Overridepublic void draw() {System.out.println("画圆形");}}}
各种类型的形状自己规范自己的行为, 而GraphicEditor.draw()只负责画出来. 当增加一种类型三角形. 只需要
- 第一,增加一个三角形的类,实现Shape接口
- 第二,调用draw方法,划出来就可以了
整个过程都是在扩展,而没有修改原来的类。这个设计是符合开闭原则的。
6.3 使用例子分析
让我们来看一个具体的使用例子,展示如何遵循开闭原则来进行扩展。
public class Main {public static void main(String[] args) {Shape circle = new Circle();Shape rectangle = new Rectangle();GraphicEditor editor = new GraphicEditor();editor.drawShape(circle);editor.drawShape(rectangle);}
}
在这个例子中,我们创建了一个圆形对象和一个矩形对象,并通过 GraphicEditor 类来绘制它们。当我们需要添加新的图形类型(例如三角形)时,只需创建一个新的类实现 Shape 接口,而不需要修改现有的代码:
// 三角形类
public class Triangle implements Shape {@Overridepublic void draw() {// 绘制三角形的代码}
}// 使用新的三角形类
public class Main {public static void main(String[] args) {Shape circle = new Circle();Shape rectangle = new Rectangle();Shape triangle = new Triangle();GraphicEditor editor = new GraphicEditor();editor.drawShape(circle);editor.drawShape(rectangle);editor.drawShape(triangle);}
}
通过这种方式,我们可以在不修改 GraphicEditor 类的情况下,轻松地扩展新的图形类型,真正实现了对扩展开放,对修改关闭的设计原则。
07.银行业务案例分析
7.1 业务基础实现
比如现在有一个银行业务,存钱,取钱和转账。最初我们会怎么思考呢?
- 首先有一个银行业务类, 用来处理银行的业务
- 银行有哪些业务呢? 存钱,取钱,转账, 这都是银行要执行的操作
- 那外部说我要存钱, 我要取钱,我要转账, 通过一个类型来告诉我们
public class BankBusiness {public void operate(int type) {if (type == 1) {save();} else if(type == 2) {take();} else if(type == 3) {transfer();}}public void save(){System.out.println("存钱");}public void take(){System.out.println("取钱");}public void transfer() {System.out.println("转账");}
}
咋一看已经实现了需求. 但是现在有新的需求来了, 银行要增加功能---理财. 理财是银行业务的一种, 自然是新增一个方法.
然后在operate()方法里增加一种类型. 这就是一个糟糕的设计, 增加新功能, 但是却修改了原来的代码
7.2 开闭原则演变
设计成接口抽象的形式,利用开关原则,可以尝试改造为下面的代码。将不同对象分类的服务方法进行抽象,把业务逻辑的紧耦合关系拆开,实现代码的隔离保证了方便的扩展。
public interface Business {public void operate();
}public class Save implements Business {@Overridepublic void operate() {System.out.println("存钱业务");}
}public class Take implements Business {@Overridepublic void operate() {System.out.println("取钱业务");}
}public class Transfer implements Business {@Overridepublic void operate() {System.out.println("转账业务");}
}public class BankBusinesses {/*** 处理银行业务* @param business*/public void operate(Business business) {System.out.println("处理银行业务");business.operate();}
}
通过接口抽象的形式方便扩展, 加入要新增理财功能. 只需新增一个理财类, 其他业务代码都不需要修改.
其实, 在日常工作中, 经常会遇到这种情况. 因为我们平时写业务逻辑会更多一些, 而业务就像流水账, 今天一个明天一个一点一点的增加. 所以,当业务增加到3个的时候, 我们就要思考, 如何写能够方便扩展
08.开闭原则优缺点
8.1 开闭原则的优点
- 提高了代码的可维护性与复用性:遵循开闭原则可以让代码更加稳定和可维护,同时也使得代码更容易被复用。如果我们需要修改某个模块的行为,只需要扩展该模块而不需要直接修改源代码,这样就不会影响到其他的模块。
- 提高了代码的可扩展性:开闭原则还可以提高代码的可扩展性。通过扩展已有的代码,我们可以很容易地添加新的功能或改进现有功能,从而适应业务需求的更改。
- 提高了代码的可测试性:遵循开闭原则可以降低代码的耦合度,使得测试更加容易。因为我们只需要测试新增的代码,而不必验证已有代码的正确性。
8.2 开闭原则的缺点
- 对代码的设计要求高:遵循开闭原则需要对代码进行良好的抽象和封装,这对程序员的设计能力和经验要求较高。如果设计不好,可能会导致代码过于复杂难以维护。
- 可能会增加代码量:通过扩展已有的代码来实现新功能,可能会增加代码量,使得系统变得更加复杂。这需要我们在平衡可维护性和代码量之间做出权衡。
- 可能会带来设计上的限制:在某些情况下,为了遵循开闭原则,我们可能需要引入更多的抽象层或接口。这可能会带来一定的设计上的限制,限制了代码的表达能力和灵活性。
09.开闭原则的总结
9.1 理解开闭原则
如何理解“对扩展开放、对修改关闭”?
添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。
关于定义,我们有两点要注意。
- 第一点是,开闭原则并不是说完全杜绝修改,而是以最小的修改代码的代价来完成新功能的开发。
- 第二点是,同样的代码改动,在粗代码粒度下,可能被认定为“修改”;在细代码粒度下,可能又被认定为“扩展”。
9.2 如何做到开闭原则
如何做到“对扩展开放、修改关闭”?
我们要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。
学习设计原则,要多问个为什么。
不能把设计原则当真理,而是要理解设计原则背后的思想。搞清楚这个,比单纯理解原则讲的是啥,更能让你灵活应用原则。
9.3 开闭原则的总结
- 问题思考:开闭原则的主要用途是什么?如何才能做到对外拓展开放,对内修改关闭?你平常是如何理解开闭原则的,判断的标准是什么?
- 如何理解开闭原则:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。
- 为何开闭原则比较难学:怎样的代码改动才被定义为‘修改’?怎么才算满足或违反‘开闭原则’?如何理解大部分设计模式都是遵循开闭原则!
- 开笔原则的背景:软件/业务迭代升级中,面对代码变化,修改原来代码可能引入原有模块bug风险,因此尽量通过对外拓展来实现功能迭代。
- 实现开闭原则的方式:常用的设计技术有以下几种,1.抽象类和接口;2.策略模式;3.装饰器模式等。
- 开闭原则的案例教学:绘制圆形/矩形,通过if-else来执行不同case逻辑,新增一种类型则需要修改原逻辑。因此考虑通过接口+抽象类方式实现友好拓展。
- 开闭原则的优点:1.提高代码拓展性和可维护性;2.提高代码可测试的粒度;3.降低的代码耦合度。
- 开闭原则的缺点:1.对代码设计要求高;2.可能会导致代码量增大和变得复杂;3.可能会带来设计上的限制。
- 总结如何理解开闭原则:当需求发生变化时,我们应该通过添加新的代码来扩展功能,而不是修改已有的代码。
- 总结如何运用开闭原则:通过封装变化、使用抽象化、利用扩展点和遵循依赖倒置原则来实现。
10.更多内容推荐
模块 | 描述 | 备注 |
---|---|---|
GitHub | 多个YC系列开源项目,包含Android组件库,以及多个案例 | GitHub |
博客汇总 | 汇聚Java,Android,C/C++,网络协议,算法,编程总结等 | YCBlogs |
设计模式 | 六大设计原则,23种设计模式,设计模式案例,面向对象思想 | 设计模式 |
Java进阶 | 数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM | Java高级 |
网络协议 | 网络实际案例,网络原理和分层,Https,网络请求,故障排查 | 网络协议 |
计算机原理 | 计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理 | 计算机基础 |
学习C编程 | C语言入门级别系统全面的学习教程,学习三到四个综合案例 | C编程 |
C++编程 | C++语言入门级别系统全面的教学教程,并发编程,核心原理 | C++编程 |
算法实践 | 专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等 | Leetcode |
Android | 基础入门,开源库解读,性能优化,Framework,方案设计 | Android |
23种设计模式
23种设计模式 & 描述 & 核心作用 | 包括 |
---|---|
创建型模式 提供创建对象用例。能够将软件模块中对象的创建和对象的使用分离 |
工厂模式(Factory Pattern) 抽象工厂模式(Abstract Factory Pattern) 单例模式(Singleton Pattern) 建造者模式(Builder Pattern) 原型模式(Prototype Pattern) |
结构型模式 关注类和对象的组合。描述如何将类或者对象结合在一起形成更大的结构 |
适配器模式(Adapter Pattern) 桥接模式(Bridge Pattern) 过滤器模式(Filter、Criteria Pattern) 组合模式(Composite Pattern) 装饰器模式(Decorator Pattern) 外观模式(Facade Pattern) 享元模式(Flyweight Pattern) 代理模式(Proxy Pattern) |
行为型模式 特别关注对象之间的通信。主要解决的就是“类或对象之间的交互”问题 |
责任链模式(Chain of Responsibility Pattern) 命令模式(Command Pattern) 解释器模式(Interpreter Pattern) 迭代器模式(Iterator Pattern) 中介者模式(Mediator Pattern) 备忘录模式(Memento Pattern) 观察者模式(Observer Pattern) 状态模式(State Pattern) 空对象模式(Null Object Pattern) 策略模式(Strategy Pattern) 模板模式(Template Pattern) 访问者模式(Visitor Pattern) |