定义
开闭原则定义:
Software entities like classes,modules and functions should be open for extension but closed for modifications.(一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。)
案例
开闭原则的定义告诉我们:软件实体应该对扩展开放,对修改关闭,其含义是说一个软件应该通过扩展来实现变化,而不是通过修改已有代码来实现变化。
软件实体:模块、抽象和类、方法。
唯一不变的就是变化,真实反映了一个软件产品只要在生命周期内,都会发生变化。既然变化是不可避免的,那么我们就应该在设计系统时尽量适应这些变化,以提高项目的稳定性和灵活性,实现拥抱(主动)变化,而不是应对(被动)变化。
什么是开闭原则,以天猫手机店铺销售手机为例,其类图如下:
IPhone定义了数据的三个属性:品牌、价格、系统。SmartPhone是IPhone的一个实现类,是所有手机的总称,PhoneStore是手机店铺。
代码如下:
代码语言:javascript复制---------手机销售订单----------
手机品牌:APPLE 手机价格:¥5,999.00 手机系统:IOS
手机品牌:HUAWEI 手机价格:¥3,699.00 手机系统:ANDROID
手机品牌:XIAOMI 手机价格:¥2,499.00 手机系统:ANDROID
双十一要来了,各个手机品牌厂商开始年底冲业绩了,进行了价格打折。其中有价格屠夫之称的小米打折最猛,直接8折,华为也不甘示弱打9折,苹果也意思意思嘛,打95折。这个需求对于项目来说,就是一个变化,我们应该如何适应这个变化呢?有以下3种方法:
- 修改接口
可以在IPhone接口上增加一个getOffPrice()的方法,专门用于打折处理,所有实现类实现该方法。但是这个样修改的后果就是实现类SmartPhone也要修改,PhoneStore中的main方法也要修改,而IPhone作为接口应该是稳定且可靠的,显然这个方案不得行。
- 修改实现类
修改SmartPhone类中的方法,直接在getPrice()中实现打折逻辑。但是这个方法的缺点就是,店铺只在双十一进行手机打折销售,但是这样改的话相当于以后(双十一后)所有手机都要按打折后的价格销售。显然也不行。
- 通过扩展实现变化
我们增加一个OffSmartPhone类,继承SmartPhone类,重写SmartPhone的getPrice()方法,高层次的模块通过OffSmartPhone类产生新的对象,完成业务变化对系统的最小化开发。
代码语言:javascript复制---------手机销售订单----------
手机品牌:APPLE 手机价格:¥5,699.00 手机系统:IOS
手机品牌:HUAWEI 手机价格:¥3,329.00 手机系统:ANDROID
手机品牌:XIAOMI 手机价格:¥1,999.00 手机系统:ANDROID
OK,双十一的打折项目代码开发完了。当然你会问,你不是还是对已有的代码进行了修改了嘛(PhoneStore类的static方法)。但是我想说的是,该部分属于高层次模块的代码改动,改动的比较小,影响范围小。
我们可以把变化归纳为以下三种类型:
- 逻辑变化
只变化一个逻辑,而不涉及其他模块,比如原有的一个算法是a*b c,现在需要修改为a*b*c,可以通过修改原有类中的方法的方式来完成,前提条件是所有依赖或关联类都按照相同的逻辑处理。
- 子模块变化
一个模块变化,会对其他的模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的,刚刚的书籍打折处理就是类似的处理模块,该部分的变化甚至会引起界面的变化。
- 可见视图变化
可见视图是提供给客户使用的界面,如JSP程序、Swing界面等,该部分的变化一般会引起连锁反应(特别是在国内做项目,做欧美的外包项目一般不会影响太大)。如果仅仅是界面上按钮、文字的重新排布倒是简单,最司空见惯的是业务耦合变化,什么意思呢?一个展示数据的列表,按照原有的需求是6列,突然有一天要增加1列,而且这一列要跨N张表,处理M个逻辑才能展现出来,这样的变化是比较恐怖的,但还是可以通过扩展来完成变化,这就要看我们原有的设计是否灵活。
开闭原则的重要性
开闭原则是非常重要的,可通过以下几个方面来理解其重要性。
1.开闭原则对测试的影响
所有已经投产的代码都是有意义的,并且都受系统规则的约束,这样的代码都要经过“千锤百炼”的测试过程,不仅保证逻辑是正确的,还要保证苛刻条件(高压力、异常、错误)下不产生“有毒代码”(Poisonous Code),因此有变化提出时,我们就需要考虑一下,原有的健壮代码是否可以不修改,仅仅通过扩展实现变化呢?否则,就需要把原有的测试过程回笼一遍,需要进行单元测试、功能测试、集成测试甚至是验收测试,现在虽然在大力提倡自动化测试工具,但是仍然代替不了人工的测试工作。
2.开闭原则可以提高复用性
在面向对象的设计中,所有的逻辑都是从原子逻辑组合而来的,而不是在一个类中独立实现一个业务逻辑。只有这样代码才可以复用,粒度越小,被复用的可能性就越大。那为什么要复用呢?减少代码量,避免相同的逻辑分散在多个角落,避免日后的维护人员为了修改一个微小的缺陷或增加新功能而要在整个项目中到处查找相关的代码,然后发出对开发人员“极度失望”的感慨。那怎么才能提高复用率呢?缩小逻辑粒度,直到一个逻辑不可再拆分为止。
3.开闭原则可以提高可维护性
一款软件投产后,维护人员的工作不仅仅是对数据进行维护,还可能要对程序进行扩展,维护人员最乐意做的事情就是扩展一个类,而不是修改一个类,甭管原有的代码写得多么优秀还是多么糟糕,让维护人员读懂原有的代码,然后再修改,是一件很痛苦的事情,不要让他在原有的代码海洋里游弋完毕后再修改,那是对维护人员的一种折磨和摧残。
4.面向对象开发的要求
万物皆对象,我们需要把所有的事物都抽象成对象,然后针对对象进行操作,但是万物皆运动,有运动就有变化,有变化就要有策略去应对,怎么快速应对呢?这就需要在设计之初考虑到所有可能变化的因素,然后留下接口,等待“可能”转变为“现实”。
设计模式|理解单一职责原则
设计模式|LSP(里氏替换)原则
设计模式|依赖倒置原则