开闭原则(OCP)
定义
一个软件实体应当对扩展开放,对修改关闭(对原有代码尽量少进行修改,甚至不做修改,仅做扩展)。
开闭原则在定义实体类、功能模块类等的时候会体现的较为明显。
通俗的讲,添加一个功能应该是在已有的代码基础上进行扩展,而不是修改已有的代码。
作用(目的)
我们为什么要遵循这个开闭原则呢?
可能有很多人会觉得,在原来的代码基础上进行修改,好像没有出现什么问题或者困难。
原因就是在于要么运气好没遇到,要么就是项目还是太简单了,遇到的概率自然就会小。
那么都会有哪些问题呢?
后添加的功能,可能与原来的写好的代码,使用的依赖库不一样,也可能是原来的代码中有一些判断和后添加的功能所需要的判断在逻辑上较难分析……还有很多情况。
但是这些情况都是说明了一件事情,就是如果因为后添加的功能模块或者实体类,而修改了原来的代码,都可能会造成一定的风险或者程序上编写的困难。
开闭原则好处
此时我们如果按照开闭原则,有一个接口类,我们只需要再写一个实体类或者功能模块类implements接口类就行,有什么需求在接口中定义好方法后,在派生类中进行具体实现就行。(如此一来,降低了编程难度,降低了程序修改的风险)
!!!重要的是说三遍!!!
优点:不用修改原来的代码
优点:不用修改原来的代码
优点:不用修改原来的代码
关键点
关键就是对对象进行抽象。如果几个实体共属于同一个类别,我们就可以把这个共同类别抽象出来。然后实体分别来implements这个抽象类。每当多出一个该抽象类的具体实体类的时候,我们只要再写一个类来继续implements这个抽象类即可。
在这个过程中,我们就是遵循了开闭原则。
通过案例来理解
本案例以实体类为例讲解(对于功能模块也是一样的道理)
时间段①:有一个人,叫张三,想要调查一个事情,就是现在婴儿吃饭是怎么吃的,我们就用Test类来代表张三这个个体(相当于客户端)。然后一个类叫Query的类,它可以帮助查询这个问题的答案。
常规做法:
1 | public class Test { |
似乎没毛病,一点问题都没嘚
但是,如果过了一万年(也就是你已经忘记了当时你的Query类中逻辑的时候),你遇到了新的需求:
时间段②:张三又想调查一下大学生是怎么吃饭的,张三就对这个程序进行了修改。
1 | public class Test { |
在上面的过程中,貌似只要多一个查询的目标,就要多写一个方法,进行查询,似乎没什么问题。。。。。
但是,如果,碰到一下几种情况呢?
- 如果新出现的类中,有些依赖和原来的程序中的一些依赖冲突了呢?
- 如果不是加一个方法,而是在一个方法中,进行添加一些查询条件,导致逻辑上的分析变得异常艰难呢?
此时,不难发现,已经出现了一些上面提到的问题和风险了
时间段③:张三后来又查了五六个目标人群后,也意识到了这个问题,于是就去找好朋友了,好朋友大葱鸣出面了!直接一句:“看我的!”。接着大葱鸣就给张三讲解了设计模式的开闭原则。张三顿悟!心中刹那有一妙计!
妙计为何?(解决方案)
1 | public class Test { |
小结
如果是功能模块,也和实体类类似,尽量抽象出抽象类,遵从开闭原则,允许扩展,规避修改
目的就是一个,减少因后加的需求导致的修改原来代码的操作
依赖倒置原则(DIP)
定义
高层模块不依赖于底层,底层模块依赖于高层
抽象不应该依赖于细节,细节依赖于抽象
换言之:要针对接口编程,而不是针对实现编程
依赖倒置原则和开闭原则关系
如果说,开闭原则是面向对象设计的目标,那么依赖倒置原则就是其实现机制
要求做法
在程序中传递参数时,或者在关联关系中,尽量引用高层次的抽象层类,即使用接口和抽象类进行变量类型声明、方法返回类型声明,以及数据类型的转换等,而不是用具体类来做这些事情。
一个具体类仅实现接口或抽象类中定义的方法,不额外给出多余的方法,否则将无法调用到子类中增加的方法
依赖注入三种方式
在实现依赖倒置原则时,需要针对抽象层编程,将具体的对象通过依赖注入的方式注入到其他对象中
- 构造注入:通过构造函数传入抽象类指代的具体类的对象
- setter(设值)注入:通过setter方法传入抽象类指代的具体类的对象
- 接口注入:通过在接口中声明的业务方法来传入抽象类指代的具体的对象
举例:构造注入
1 | public class Test { |
注:这些方法在定义的时候使用的都是抽象类型,在运行时,才传入具体类型的对象,由子类覆盖父类
总结
开闭原则可以说是面向对象程序设计的目的(降低风险,减少程序修改的需要),而依赖倒置原则就可以说是实现这个目标的手段