
一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网
设计模式Git项目地址:https://github.com/yangchong211/YCDesignBlog
本文详细介绍了面向对象设计中的开闭原则(OCP),即软件实体应对扩展开放、对修改关闭。文章通过问题思考、背景分析、实现方式及案例教学,深入探讨了如何在实际开发中应用这一原则,以提高代码的可维护性和扩展性。
文中还结合图形绘制和银行业务两个具体案例,展示了如何通过抽象类、接口、策略模式等技术手段实现开闭原则。最后总结了开闭原则的优缺点,并强调了其在设计模式中的重要地位。
开闭原则的英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。
我们把它翻译成中文就是:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。
这个描述比较简略,如果我们详细表述一下,那就是,添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会将错误引入原本已经经过测试的旧代码中,破坏原有系统。
因此,当软件需要变化时,我们应该尽量通过扩展的方式来实现变化,而不是通过修改已有的代码来实现。
当然,在现实开发中,只通过继承的方式来升级、维护原有系统只是一个理想化的愿景,因此,在实际的开发过程中,修改原有代码、扩展代码往往是同时存在的。
软件开发过程中,最不会变化的就是变化本身。产品需要不断地升级、维护,没有一个产品从第一版本开发完就再没有变化了,除非在下个版本诞生之前它已经被终止。
产品需要升级,修改原来的代码就可能会引发其他的问题。那么如何确保原有软件模块的正确性,以及尽量少地影响原有模块,答案就是尽量遵守本章要讲述的开闭原则。
个人觉得,开闭原则是 SOLID 中最难理解、最难掌握,同时也是最有用的一条原则。
之所以说这条原则难理解,那是因为,“怎样的代码改动才被定义为‘扩展’?怎样的代码改动才被定义为‘修改’?怎么才算满足或违反‘开闭原则’?修改代码就一定意味着违反‘开闭原则’吗?”等等这些问题,都比较难理解。
之所以说这条原则难掌握,那是因为,“如何做到‘对扩展开放、修改关闭’?如何在项目中灵活地应用‘开闭原则’,以避免在追求扩展性的同时影响到代码的可读性?”等等这些问题,都比较难掌握。
之所以说这条原则最有用,那是因为,扩展性是代码质量最重要的衡量标准之一。在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则。
为了实现开闭原则,常用的设计技术有以下几种:
假设有一个图形绘制程序,程序需要能够绘制不同形状的图形,比如矩形、圆形和三角形。最初的设计可能会像这样:
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;
}
}
}我们来看看,这个代码,初看是符合要求了,再想想,要是我增加一种形状呢? 比如增加三角形.
在这个设计中,每当我们需要添加新的图形类型,就需要修改 GraphicEditor 类,添加新的 if 条件。我们需要修改已有的代码来实现新功能。
这就违背了开闭原则-对扩展开发,对修改关闭。增加一个类型,修改了三处代码。
为了符合开闭原则,我们可以进行重构。首先,我们定义一个抽象类Shape:
public class GraphicEditor {
public void draw(Shape shape) {
shape.draw();
}
interface Shape {
void draw();
}
class Rectangle implements Shape {
@Override
public void draw() {
System.out.println("画矩形");
}
}
class Circle implements Shape {
@Override
public void draw() {
System.out.println("画圆形");
}
}
}各种类型的形状自己规范自己的行为, 而GraphicEditor.draw()只负责画出来. 当增加一种类型三角形. 只需要
整个过程都是在扩展,而没有修改原来的类。这个设计是符合开闭原则的。
让我们来看一个具体的使用例子,展示如何遵循开闭原则来进行扩展。
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 {
@Override
public 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 类的情况下,轻松地扩展新的图形类型,真正实现了对扩展开放,对修改关闭的设计原则。
比如现在有一个银行业务,存钱,取钱和转账。最初我们会怎么思考呢?
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()方法里增加一种类型. 这就是一个糟糕的设计, 增加新功能, 但是却修改了原来的代码
设计成接口抽象的形式,利用开关原则,可以尝试改造为下面的代码。将不同对象分类的服务方法进行抽象,把业务逻辑的紧耦合关系拆开,实现代码的隔离保证了方便的扩展。
public interface Business {
public void operate();
}
public class Save implements Business {
@Override
public void operate() {
System.out.println("存钱业务");
}
}
public class Take implements Business {
@Override
public void operate() {
System.out.println("取钱业务");
}
}
public class Transfer implements Business {
@Override
public void operate() {
System.out.println("转账业务");
}
}
public class BankBusinesses {
/**
* 处理银行业务
* @param business
*/
public void operate(Business business) {
System.out.println("处理银行业务");
business.operate();
}
}通过接口抽象的形式方便扩展, 加入要新增理财功能. 只需新增一个理财类, 其他业务代码都不需要修改.
其实, 在日常工作中, 经常会遇到这种情况. 因为我们平时写业务逻辑会更多一些, 而业务就像流水账, 今天一个明天一个一点一点的增加. 所以,当业务增加到3个的时候, 我们就要思考, 如何写能够方便扩展
如何理解“对扩展开放、对修改关闭”?
添加一个新的功能,应该是通过在已有代码基础上扩展代码(新增模块、类、方法、属性等),而非修改已有代码(修改模块、类、方法、属性等)的方式来完成。
关于定义,我们有两点要注意。
如何做到“对扩展开放、修改关闭”?
我们要时刻具备扩展意识、抽象意识、封装意识。在写代码的时候,我们要多花点时间思考一下,这段代码未来可能有哪些需求变更,如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,在不改动代码整体结构、做到最小代码改动的情况下,将新的代码灵活地插入到扩展点上。
学习设计原则,要多问个为什么。
不能把设计原则当真理,而是要理解设计原则背后的思想。搞清楚这个,比单纯理解原则讲的是啥,更能让你灵活应用原则。
模块 | 描述 | 备注 |
|---|---|---|
GitHub | 多个YC系列开源项目,包含Android组件库,以及多个案例 | |
博客汇总 | 汇聚Java,Android,C/C++,网络协议,算法,编程总结等 | |
设计模式 | 六大设计原则,23种设计模式,设计模式案例,面向对象思想 | |
Java进阶 | 数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM | |
网络协议 | 网络实际案例,网络原理和分层,Https,网络请求,故障排查 | |
计算机原理 | 计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理 | |
学习C编程 | C语言入门级别系统全面的学习教程,学习三到四个综合案例 | |
C++编程 | C++语言入门级别系统全面的教学教程,并发编程,核心原理 | |
算法实践 | 专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等 | |
Android | 基础入门,开源库解读,性能优化,Framework,方案设计 |
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) |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。