设计模式六大原则(6):开闭原则

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.一个层次是对抽象定义的修改:如对象公开的接口,包括方法的名称、参数与返回类型。

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

[java]  view plain copy
  1. 1//定义1    
  2. 2. bool Connect(string userName, string password, string ftpAddress, int port);    
  3. 3//定义2    
  4. 4. bool Connect(Account account);    
  5. 5public class Account    
  6. 6. {    
  7. 7.     public string UserName { get; set; }    
  8. 8.     public string Password { get; set; }    
  9. 9.     public string FtpAddress { get; set; }    
  10. 10.     public string int Port { get; set; }    
  11. 11. }   

相比较前者,后者虽然多了一个Account类的定义,但Connect()方法却明显更加稳定。倘若需要为Connect()方法提供一个Ftp服务器的主目录名,定义1必须修改该方法的接口对应的,所有调用Connect()方法的对象都会受到影响定义2只需要修改Account类,由于Connect()方法的接口保持不变,只要Connect()方法的调用者并不需要主目录名,这样的修改就完全不会影响调用者。即使需要主目录名,我们也可以在Account类的构造函数中为主目录名提供默认的实现,从而降低需求变化带来的影响。我认为,这样的设计对修改就是封闭的。定义2 良好!

2.另一个层次是指对具体实现的修改。

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

2.3 扩展

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

     

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

2.4 开放封闭原则还可以统一起来理解。

对扩展实现了开放,才能够保证对修改是封闭的开放利用了对象的抽象封闭则在一定程度上利用了封装

最佳的做法仍然是要做到分离对象的变与不变

     将对象不变的部分封装起来,并遵循良好的设计原则以保障接口的稳定;

     至于对象中可能变的部分,则需要进行抽象,以建立松散的耦合关系。

我们遵循设计模式前面5大原则,以及使用23种设计模式目的就是遵循开闭原则。也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的“平均得分”,前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。 

开闭原则无非就是想表达这样一层意思:用抽象构建框架,用实现扩展细节

      因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。 

回忆前面的5个原则,OCP恰恰告诉我们:用抽象构建框架,用实现扩展细节的注意事项而已:

  1. 单一职责原则告诉我们实现类要职责单一;
  2. 里氏替换原则告诉我们不要破坏继承体系;
  3. 依赖倒置原则告诉我们要面向接口编程;
  4. 接口隔离原则告诉我们要在设计接口时要精简单一;
  5. 迪米特法则告诉我们要降低耦合。
  6. 开闭原则总纲告诉我们要对扩展开放,对修改关闭。
      最后说明一下如何去遵守这六个原则。 对这六个原则的遵守并不是是和否的问题,而是多和少的问题 ,也就是说,我们一般不会说有没有遵守,而是说遵守程度的多少。任何事都是过犹不及,设计模式的六个设计原则也是一样,制定这六个原则的目的并不是要我们刻板的遵守他们,而需要根据实际情况灵活运用。对他们的遵守程度只要在一个合理的范围内,就算是良好的设计。我们用一幅图来说明一下。
         

图中的每一条维度各代表一项原则,我们依据对这项原则的遵守程度在维度上画一个点,则如果对这项原则遵守的合理的话,这个点应该落在红色的同心圆内部;如果遵守的差,点将会在小圆内部;如果过度遵守,点将会落在大圆外部。一个良好的设计体现在图中,应该是六个顶点都在同心圆中的六边形。

在上图中,设计1、设计2属于良好的设计,他们对六项原则的遵守程度都在合理的范围内;设计3、设计4设计虽然有些不足,但也基本可以接受;设计5则严重不足,对各项原则都没有很好的遵守;而设计6则遵守过渡了,设计5和设计6都是迫切需要重构的设计。

3.问题由来:

在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

[解决方案]当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。


4.使用LoD的好处:

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

5.难点:

如何遵循抽象约束:

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

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

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

封装变化:

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

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


6.最佳实践:

       封装变化:按可能变化的不同去封装变化;

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


7.范例:

7.1扩展实现(书店售书例,下为其类图)


[java]  view plain copy
  1. 代码清单如下:  
  2. public interface IBook {     
  3.   //书籍有名称   
  4.   public String getName();     
  5.   //书籍有售价   
  6.   public int getPrice();     
  7.   //书籍有作者   
  8.   public String getAuthor();   
  9. }  
  10.   
  11. 小说书籍的源代码如下:  
  12. public class NovelBook implements IBook {   
  13.   //书籍名称   
  14.   private String name;     
  15.   //书籍的价格   
  16.   private int price;     
  17.   //书籍的作者   
  18.   private String author;   
  19.      
  20.   //通过构造函数传递书籍数据  
  21.   public NovelBook(String _name,int _price,String _author){   
  22.    this.name = _name;   
  23.    this.price = _price;   
  24.    this.author = _author;   
  25.  }   
  26.     
  27.   //获得作者是谁   
  28.   public String getAuthor() {   
  29.    return this.author;   
  30.  }   
  31.    
  32.   //书籍叫什么名字   
  33.   public String getName() {   
  34.    return this.name;   
  35.  }   
  36.    
  37.   //获得书籍的价格   
  38.   public int getPrice() {   
  39.    return this.price;   
  40.  }   
  41.    
  42. }  
  43.   
  44. //售书  
  45. public class BookStore {   
  46.   private final static ArrayList<IBook> bookList = new ArrayList<IBook>();  
  47.     
  48.   //静态模块初始化,项目中一般是从持久层初始化产    
  49.   static{   
  50.    bookList.add(new NovelBook("天龙八部",3200,"金庸"));   
  51.    bookList.add(new NovelBook("巴黎圣母院",5600,"雨果"));   
  52.    bookList.add(new NovelBook("悲惨世界",3500,"雨果"));   
  53.    bookList.add(new NovelBook("金瓶梅",4300,"兰陵笑笑生"));   
  54.  }   
  55.     
  56.   //模拟书店买书   
  57.   public static void main(String[] args) {   
  58.     NumberFormat formatter = NumberFormat.getCurrencyInstance();   
  59.     formatter.setMaximumFractionDigits(2);   
  60.     System.out.println("------------书店买出去的书籍记录如下:---------------------");   
  61.       
  62.     for(IBook book:bookList){  
  63.     System.out.println("书籍名称:" + book.getName()+"\t书籍作者:" +   
  64.        book.getAuthor()+ "\t书籍价格:" + formatter.format(book.getPrice()/100.0)+"元");   
  65.   }   
  66.  }   
  67. }  

项目投产,书店盈利,但为扩大市场,书店决定,40元以上打9折,40元以下打8 折。如何解决这个问题呢?

修改接口。IBook上新增加一个方法getOffPrice(),专门进行打折,所有实现类实现这个方法。但是这样修改的后果就是实现类NovelBook要修改,BookStore中的main方法也修改,同时Ibook作为接口应该是稳定且可靠的,不应该经常发生变化,否则接口做为契约的作用就失去了效能,因此该方案被否定。

修改实现类。修改NovelBook 类中的方法,直接在getPrice()中实现打折处理,这个应该是大家在项目中经常使用的就是这样办法,通过class文件替换的方式可以完成部分业务(或是缺陷修复)变化,该方法在项目有明确的章程(团队内约束)或优良的架构设计时,是一个非常优秀的方法,但是该方法还是有缺陷的,例如采购书籍人员也是要看价格的,由于该方法已经实现了打折处理价格,因此采购人员看到的也是打折后的价格,这就产生了信息的蒙蔽效果,导致信息不对称而出现决策失误的情况。该方案也不是一个最优的方案。

通过扩展实现变化。增加一个子类 OffNovelBook,覆写getPrice方法,高层次的模块(也就是static静态模块区)通过OffNovelBook类产生新的对象,完成对业务变化开发任务。好办法,风险也小,我们来看类图:

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

[java]  view plain copy
  1. public class OffNovelBook extends NovelBook {   
  2.   public OffNovelBook(String _name,int _price,String _author){   
  3.    super(_name,_price,_author);   
  4.  }   
  5.     
  6.   //覆写销售价格   
  7.   @Override   
  8.   public int getPrice(){   
  9.    //原价   
  10.    int selfPrice = super.getPrice();   
  11.    int offPrice=0;   
  12.    if(selfPrice>4000){  //原价大于40元,则打9折   
  13.       offPrice = selfPrice * 90 /100;   
  14.   }else{   
  15.       offPrice = selfPrice * 80 /100;   
  16.   }   
  17.      
  18.    return offPrice;   
  19.  }   
  20.     
  21. }  
  22. 很简单,仅仅覆写了getPrice方法,通过扩展完成了新增加的业务。 然后我们来看BookStore类的修改:  
  23. public class BookStore {   
  24.   private final static ArrayList<IBook> bookList = new ArrayList<IBook>();   
  25.     
  26.   //静态模块初始化,项目中一般是从持久层初始化产    
  27.   static{   
  28.    bookList.add(new OffNovelBook("天龙八部",3200,"金庸"));   
  29.    bookList.add(new OffNovelBook("巴黎圣母院",5600,"雨果"));   
  30.    bookList.add(new OffNovelBook("悲惨世界",3500,"雨果"));   
  31.    bookList.add(new OffNovelBook("金瓶梅",4300,"兰陵笑笑生"));   
  32.  }   
  33.     
  34.   //模拟书店买书   
  35.   public static void main(String[] args) {   
  36.     NumberFormat formatter = NumberFormat.getCurrencyInstance();   
  37.     formatter.setMaximumFractionDigits(2);   
  38.     System.out.println("------------书店买出去的书籍记录如下:---------------------");   
  39.     for(IBook book:bookList){   
  40.     System.out.println("书籍名称:" + book.getName()+"\t书籍作者:" +   
  41.        book.getAuthor()+ "\t书籍价格:" + formatter.format(book.getPrice()/100.0)+"元");   
  42.   }   
  43.  }   
  44. }  

归纳变化:

逻辑变化。只变化一个逻辑,而不涉及到其他模块,比如原有的一个算法是a*b+c,现在要求a*b*c,可能通过修改原有类中的方法方式来完成,前提条件是所有依赖或关联类都按此相同逻辑处理。

子模块变化。一个模块变化,会对其他模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化,因此在通过扩展完成变化时,高层次的模块修改是必然的,刚刚的书籍打折处理就是类似的处理模块,该部分的变化甚至引起界面的变化。

可见视图变化。可见视图是提供给客户使用的界面,该部分的变化一般会引起连锁反应(特别是在国内做项目,做欧美的外包项目一般不会影响太大),如果仅仅是界面上按钮、文字的重新排布倒是简单,最司空见惯的是业务耦合变化,什么意思呢?一个展示数据的列表,按照原有的需求是六列,突然有一天要增加一列,而且这一列要跨度N张表,处理M个逻辑才能展现出来,这样的变化是比较恐怖的,但是我们还是可以通过扩展来完成变化,这就依赖我们原有的设计是否灵活。

7.2扩展接口再扩展实现

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

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

[java]  view plain copy
  1. public interface IComputerBook extends IBook{     
  2.   //计算机书籍是有一个范围   
  3.   public String getScope();   
  4. }  
  5. 很简单,计算机数据增加了一个方法,就是获得该书籍的范围,同时继承IBook接口,毕竟计算机书籍也是书籍。其实现类如下:  
  6.   
  7. public class ComputerBook implements IComputerBook {   
  8.   private String name;   
  9.   private String scope;   
  10.   private String author;   
  11.   private int price;   
  12.     
  13.   public ComputerBook(String _name,int _price,String _author,String _scope){   
  14.    this.name=_name;   
  15.    this.price = _price  
  16.    this.author = _author;   
  17.    this.scope = _scope;   
  18.  }  
  19.   
  20.     
  21.   public String getScope() {   
  22.    return this.scope;   
  23.  }   
  24.    
  25.   public String getAuthor() {   
  26.    return this.author;   
  27.  }   
  28.    
  29.   public String getName() {   
  30.    return this.name;   
  31.  }   
  32.     
  33.   public int getPrice() {   
  34.    return this.price;   
  35.  }   
  36.    
  37. }  
  38.   
  39. 也很简单,实现IcomputerBook就可以,而BookStore类没有做任何的修改,只是在static静态模块中增加一条数据,代码如下:  
  40.   
  41. public class BookStore {   
  42.   private final static ArrayList<IBook> bookList = new ArrayList<IBook>();   
  43.     
  44.   //静态模块初始化,项目中一般是从持久层初始化产    
  45.   static{   
  46.    bookList.add(new OffNovelBook("天龙八部",3200,"金庸"));   
  47.    bookList.add(new OffNovelBook("巴黎圣母院",5600,"雨果"));   
  48.    bookList.add(new OffNovelBook("悲惨世界",3500,"雨果"));   
  49.    bookList.add(new OffNovelBook("金瓶梅",4300,"兰陵笑笑生"));   
  50.    //增加计算机书籍   
  51.    bookList.add(new ComputerBook("Think in Java",4300,"Bruce Eckel","编程语言"));   
  52.  }   
  53.     
  54.   //模拟书店买书   
  55.   public static void main(String[] args) {   
  56.     NumberFormat formatter = NumberFormat.getCurrencyInstance();   
  57.    formatter.setMaximumFractionDigits(2);   
  58.    System.out.println("------------书店买出去的书籍记录如下:---------------------");  
  59.    for(IBook book:bookList){   
  60.      System.out.println("书籍名称:" + book.getName()+"\t书籍作者:" +   
  61.        book.getAuthor()+ "\t书籍价格:" + formatter.format(book.getPrice()/100.0)+"元");   
  62.   }   
  63.  }   
  64. }  
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
引用介绍了依赖倒转原则,即高层模块不应该依赖低层模块,二者都应该依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象。依赖倒转原则的中心思想是面向接口编程,以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。在Java中,抽象指的是接口或抽象类,而细节指的是具体的实现类。使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,将展现细节的任务交给实现类去完成。 引用介绍了迪米特法则,也称为最少知道原则。迪米特法则指出一个对象应该对其他对象保持最少的了解。类与类关系越密切,耦合度越大。要将逻辑封装在类的内部,对外除了提供的public方法,不对外泄露任何信息。直接的朋友指的是在成员变量、方法参数、方法返回值中出现的类,而不是局部变量中出现的类。 引用介绍了合成/聚合原则,它建议尽量使用合成/聚合的方式,而不是使用继承。合成/聚合可以通过创建一个对象同时创建另一个对象,或者使用set方法来实现。 Java设计模式六大原则包括: 1. 单一职责原则(Single Responsibility Principle):一个类应该只有一个引起变化的原因。 2. 开放封闭原则(Open Closed Principle):软件实体应该是可以扩展的,但是不可修改的。 3. 里氏替换原则(Liskov Substitution Principle):子类对象应该能够替换其基类对象,而不会影响程序的正确性。 4. 接口隔离原则(Interface Segregation Principle):应该建立单一接口,而不是多个臃肿的接口。 5. 依赖倒转原则(Dependency Inversion Principle):高层模块不应该依赖低层模块,二者都应该依赖其抽象。 6. 合成/聚合原则(Composition/Aggregation Principle):尽量使用合成/聚合的方式,而不是使用继承。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值