设为首页 收藏本站
查看: 1672|回复: 0

[经验分享] 六大原则之“开闭原则(OCP)“笔记

[复制链接]

尚未签到

发表于 2015-11-8 14:18:22 | 显示全部楼层 |阅读模式

1.开闭原则:Open Closed Principle, OCP)




定义:Software entities like classes, modules and functions should be open for extension but closed for modifications.(一个软件实体如类,模块和函数应该对扩展开放,对修改关闭。)






2.理解:


2.1 软件实体应该对扩展开放,对修改关闭,其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。


软件实体包括以下几个部分:




  • 项目或软件产品中按照一定的逻辑规则划分的模块;
  • 抽象和类;
  • 方法。
  

2.2 修改:  可以分为两个层次来分析。一个层次是对抽象定义的修改,如对象公开的接口,包括方法的名称、参数与返回类型。


我们必须保证一个接口,尤其要保证被其他对象调用的接口的稳定;否则,就会导致修改蔓延,牵一发而动全身。从某种程度上讲,接口就是标准,要保障接口的稳定,就应该对对象进行合理的封装。一般的设计原则之所以强调方法参数尽量避免基本类型,原因正在于此。比较如下两个方法定义:



1. //定义1  
2. bool Connect(string userName, string password, string ftpAddress, int port);  
3. //定义2  
4. bool Connect(Account account);  
5. public class Account  
6. {  
7.     public string UserName { get; set; }  
8.     public string Password { get; set; }  
9.     public string FtpAddress { get; set; }  
10.     public string int Port { get; set; }  
11. }
  
  相比较前者,后者虽然多了一个Account类的定义,但Connect()方法却明显更加稳定。倘若需要为Connect()方法提供一个Ftp服务器的主目录名,定义1必须修改该方法的接口对应的,所有调用Connect()方法的对象都会受到影响而定义2只需要修改Account类,由于Connect()方法的接口保持不变,只要Connect()方法的调用者并不需要主目录名,这样的修改就完全不会影响调用者。即使需要主目录名,我们也可以在Account类的构造函数中为主目录名提供默认的实现,从而降低需求变化带来的影响。我认为,这样的设计对修改就是封闭的。定义2
良好!

  另一个层次是指对具体实现的修改。"对修改封闭"是开放封闭原则的两个要素之一。原则上,要做到避免对源代码的修改,即使仅修改具体实现,也需要慎之又慎。这是因为具体实现的修改,可能会给调用者带来意想不到的结果,这一结果并非我们预期的,甚至可能与预期相反。如果确实需要修改具体的实现,就需要做好达到测试覆盖率要求的单元测试。根据我的经验,设计要做到完全对修改封闭,几乎是不可能完成的任务。我们只能尽量将代码修改的影响降到最低,其核心指导原则就是封装与充分的测试。

2.3 扩展

  "对扩展开放"的关键是"抽象",而对象的多态则保证了这种扩展的开放性。开放原则首先意味着我们可以自由地增加功能,而不会影响原有系统。这就要求我们能够通过继承完成功能的扩展。其次,开放原则还意味着实现是可替换的。只有利用抽象,才可以为定义提供不同的实现,然后根据不同的需求实例化不同的实现子类。例如排序算法的调用,对照图1与图2之间的区别。

DSC0000.jpg       DSC0001.jpg   
图1的设计无法支持排序算法的扩展,因为Client直接调用了冒泡排序算法实现的BubbleSort类,一旦要求支持快速排序算法,就束手无策了。图2由于引入了排序算法的共同抽象ISortable接口,只要排序算法实现了该接口,就可以被Client调用。

2.4 开放封闭原则还可以统一起来理解。
由于我们对扩展实现了开放,才能够保证对修改是封闭的。开放利用了对象的抽象,封闭则在一定程度上利用了封装。最佳的做法仍然是要做到分离对象的变与不变,将对象不变的部分封装起来,并遵循良好的设计原则以保障接口的稳定;至于对象中可能变的部分,则需要进行抽象,以建立松散的耦合关系。
回忆前面的5个原则,OCP恰恰告诉我们:用抽象构建框架,用实现扩展细节的注意事项而已:
  


  • 单一职责原则告诉我们实现类要职责单一;
  • 里氏替换原则告诉我们不要破坏继承体系;
  • 依赖倒置原则告诉我们要面向接口编程;
  • 接口隔离原则告诉我们要在设计接口时要精简单一;
  • 迪米特法则告诉我们要降低耦合。

而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。  

  3.问题由来:
  在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。[解决方案]当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
  



4.使用LoD的好处:



  • 使单元测试也能够OCP;
  • 帮助缩小逻辑粒度,以提高可复用性;
  • 可以使维护人员只扩展一个类,而非修改一个类,从而提高可维护性;
  • 在设计之初考虑所有可能变化的因素,留下接口,从而符合面向对象开发的要求;
  

5.难点:
如何遵循抽象约束:

a)     通过接口或抽象类约束扩展,对扩展进行边界限定,不允许出现在接口或抽象类中的不存在的public方法;

b)    参数类型、引用对象尽量使用接口或者抽象类,而不是实现类;

c)     抽象层尽量保持稳定,一旦确定即不允许修改。

封装变化:

a)     将相同的变化封装到一个接口或抽象类中;

b)    将不同的变化封装到不同的接口或抽象类中,不应该有两个不同的变化出现在同一接口或抽象类中。(23设计模式也是从各个不同的角度对变化进行封装的




6.最佳实践:
       封装变化:按可能变化的不同去封装变化;

      
抽象约束:抽象层尽量保持稳定,一旦确定即不允许修改。




7.范例:  7.1扩展实现(书店售书例,下为其类图)
DSC0002.png


  

代码清单如下:
public interface IBook {   
//书籍有名称
public String getName();   
//书籍有售价
public int getPrice();   
//书籍有作者
public String getAuthor();
}
小说书籍的源代码如下:
public class NovelBook implements IBook {
//书籍名称
private String name;   
//书籍的价格
private int price;   
//书籍的作者
private String author;
//通过构造函数传递书籍数据
public NovelBook(String _name,int _price,String _author){
this.name = _name;
this.price = _price;
this.author = _author;
}
//获得作者是谁
public String getAuthor() {
return this.author;
}
//书籍叫什么名字
public String getName() {
return this.name;
}
//获得书籍的价格
public int getPrice() {
return this.price;
}
}
//售书
public class BookStore {
private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
//静态模块初始化,项目中一般是从持久层初始化产  
static{
bookList.add(new NovelBook(&quot;天龙八部&quot;,3200,&quot;金庸&quot;)); ////
bookList.add(new NovelBook(&quot;巴黎圣母院&quot;,5600,&quot;雨果&quot;)); ////
bookList.add(new NovelBook(&quot;悲惨世界&quot;,3500,&quot;雨果&quot;)); ////
bookList.add(new NovelBook(&quot;金瓶梅&quot;,4300,&quot;兰陵笑笑生&quot;)); ////
}
//模拟书店买书
public static void main(String[] args) {
NumberFormat formatter = NumberFormat.getCurrencyInstance();
formatter.setMaximumFractionDigits(2);
System.out.println(&quot;------------书店买出去的书籍记录如下:---------------------&quot;);
for(IBook book:bookList){
System.out.println(&quot;书籍名称:&quot; + book.getName()+&quot;\t书籍作者:&quot; +
book.getAuthor()+ &quot;\t书籍价格:&quot; + formatter.format(book.getPrice()/100.0)+&quot;元&quot;);
}
}
}

  项目投产,书店盈利,但为扩大市场,书店决定,40元以上打9折,40元以下打8 折。如何解决这个问题呢?
  修改接口。IBook上新增加一个方法getOffPrice(),专门进行打折,所有实现类实现这个方法。但是这样修改的后果就是实现类NovelBook要修改,BookStore中的main方法也修改,同时Ibook作为接口应该是稳定且可靠的,不应该经常发生变化,否则接口做为契约的作用就失去了效能,因此该方案被否定。
  修改实现类。修改NovelBook 类中的方法,直接在getPrice()中实现打折处理,这个应该是大家在项目中经常使用的就是这样办法,通过class文件替换的方式可以完成部分业务(或是缺陷修复)变化,该方法在项目有明确的章程(团队内约束)或优良的架构设计时,是一个非常优秀的方法,但是该方法还是有缺陷的,例如采购书籍人员也是要看价&#26684;的,由于该方法已经实现了打折处理价&#26684;,因此采购人员看到的也是打折后的价&#26684;,这就产生了信息的蒙蔽效果,导致信息不对称而出现决策失误的情况。该方案也不是一个最优的方案。
  通过扩展实现变化。增加一个子类 OffNovelBook,覆写getPrice方法,高层次的模块(也就是static静态模块区)通过OffNovelBook类产生新的对象,完成对业务变化开发任务。好办法,风险也小,我们来看类图:

DSC0003.png
OffNovelBook类继承了NovelBook,并覆写了getPrice方法,不修改原有的代码。我们来看看新增加的子类OffNovelBook:
  

public class OffNovelBook extends NovelBook {
public OffNovelBook(String _name,int _price,String _author){
super(_name,_price,_author);
}
//覆写销售价格
@Override
public int getPrice(){
//原价
int selfPrice = super.getPrice();
int offPrice=0;
if(selfPrice>4000){  //原价大于40元,则打9折
offPrice = selfPrice * 90 /100;
}else{
offPrice = selfPrice * 80 /100;
}
return offPrice;
}
}
很简单,仅仅覆写了getPrice方法,通过扩展完成了新增加的业务。 然后我们来看BookStore类的修改:
public class BookStore {
private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
//静态模块初始化,项目中一般是从持久层初始化产  
static{
bookList.add(new OffNovelBook(&quot;天龙八部&quot;,3200,&quot;金庸&quot;));
bookList.add(new OffNovelBook(&quot;巴黎圣母院&quot;,5600,&quot;雨果&quot;));
bookList.add(new OffNovelBook(&quot;悲惨世界&quot;,3500,&quot;雨果&quot;));
bookList.add(new OffNovelBook(&quot;金瓶梅&quot;,4300,&quot;兰陵笑笑生&quot;));
}
//模拟书店买书
public static void main(String[] args) {
NumberFormat formatter = NumberFormat.getCurrencyInstance();
formatter.setMaximumFractionDigits(2);
System.out.println(&quot;------------书店买出去的书籍记录如下:---------------------&quot;);
for(IBook book:bookList){
System.out.println(&quot;书籍名称:&quot; + book.getName()+&quot;\t书籍作者:&quot; +
book.getAuthor()+ &quot;\t书籍价格:&quot; + formatter.format(book.getPrice()/100.0)+&quot;元&quot;);
}
}
}

  归纳变化:
  逻辑变化。只变化一个逻辑,而不涉及到其他模块,比如原有的一个算法是a*b&#43;c,现在要求a*b*c,可能通过修改原有类中的方法方式来完成,前提条件是所有依赖或关联类都按此相同逻辑处理。
  子模块变化。一个模块变化,会对其他模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的,刚刚的书籍打折处理就是类&#20284;的处理模块,该部分的变化甚至引起界面的变化。
  可见视图变化。可见视图是提供给客户使用的界面,该部分的变化一般会引起连锁反应(特别是在国内做项目,做欧美的外包项目一般不会影响太大),如果仅仅是界面上按钮、文字的重新排布倒是简单,最司空见惯的是业务耦合变化,什么意思呢?一个展示数据的列表,按照原有的需求是六列,突然有一天要增加一列,而且这一列要跨度N张表,处理M个逻辑才能展现出来,这样的变化是比较恐怖的,但是我们还是可以通过扩展来完成变化,这就依赖我们原有的设计是否灵活。
  


  
  
  7.2扩展接口再扩展实现
  在上例中,书店又增加了计算机类书籍,该类书还有一个独特特性:面向的是什么领域,修改后的类图如下:

DSC0004.png   增加了一个接口IcomputerBook和实现类ComputerBook,而BookStore不用做任何修改就可以完成书店销售计算机书籍的业务,我们来看源代码:


  public interface IComputerBook extends IBook{   
//计算机书籍是有一个范围
public String getScope();
}
很简单,计算机数据增加了一个方法,就是获得该书籍的范围,同时继承IBook接口,毕竟计算机书籍也是书籍。其实现类如下:
public class ComputerBook implements IComputerBook {
private String name;
private String scope;
private String author;
private int price;
public ComputerBook(String _name,int _price,String _author,String _scope){
this.name=_name;
this.price = _price
this.author = _author;
this.scope = _scope;
}
public String getScope() {
return this.scope;
}
public String getAuthor() {
return this.author;
}
public String getName() {
return this.name;
}
public int getPrice() {
return this.price;
}
}
也很简单,实现IcomputerBook就可以,而BookStore类没有做任何的修改,只是在static静态模块中增加一条数据,代码如下:
public class BookStore {
private final static ArrayList<IBook> bookList = new ArrayList<IBook>();
//静态模块初始化,项目中一般是从持久层初始化产  
static{
bookList.add(new OffNovelBook(&quot;天龙八部&quot;,3200,&quot;金庸&quot;));
bookList.add(new OffNovelBook(&quot;巴黎圣母院&quot;,5600,&quot;雨果&quot;));
bookList.add(new OffNovelBook(&quot;悲惨世界&quot;,3500,&quot;雨果&quot;));
bookList.add(new OffNovelBook(&quot;金瓶梅&quot;,4300,&quot;兰陵笑笑生&quot;));
//增加计算机书籍
bookList.add(new ComputerBook(&quot;Think in Java&quot;,4300,&quot;Bruce Eckel&quot;,&quot;编程语言&quot;));
}
//模拟书店买书
public static void main(String[] args) {
NumberFormat formatter = NumberFormat.getCurrencyInstance();
formatter.setMaximumFractionDigits(2);
System.out.println(&quot;------------书店买出去的书籍记录如下:---------------------&quot;);
for(IBook book:bookList){
System.out.println(&quot;书籍名称:&quot; + book.getName()+&quot;\t书籍作者:&quot; +
book.getAuthor()+ &quot;\t书籍价格:&quot; + formatter.format(book.getPrice()/100.0)+&quot;元&quot;);
}
}
}

  



版权声明:本文为博主原创文章,未经博主允许不得转载。

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-136646-1-1.html 上篇帖子: Oracle 10g DBA OCP 认证只需要两门课程 下篇帖子: 浅谈设计原则OCP
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表