设计模式六大原则例子(六)-- 开放封闭原则(OCP)例子

版权声明:本文为博主原创文章,未经博主允许不得转载。转载请注明出处:http://blog.csdn.net/yabay2208 https://blog.csdn.net/yabay2208/article/details/73927991
之前我们对设计模式的六大原则做了简单归纳,这篇博客是对开放封闭原则进行的举例说明。

开放封闭原则的意义

软件实体应该对扩展开放,对修改关闭,其含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。
软件实体包括以下几个部分:
1.项目或软件产品中按照一定的逻辑规则划分的模块
2.抽象和类
3.方法

当我们修改原有代码的时候,可以分成两个方面去看:

关于修改,两方面:

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

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 良好!

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

扩展

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

开放封闭原则的优点

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

开放封闭原则的例子

1.扩展实现(书店售书的经典例子,下为其类图):
这里写图片描述

源代码如下:
书籍接口以及书籍类别:
这里写图片描述
书店实现:
这里写图片描述
运行结果:
这里写图片描述

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

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

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

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

源代码如下:
书籍接口以及书籍类别:
这里写图片描述
书店实现:
这里写图片描述
运行结果:
这里写图片描述

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

2.扩展接口再扩展实现:
上例中,书店又增加了计算机类书籍,该类书还有一个独特特性:面向的是什么领域,修改后的类图如下:
这里写图片描述
计算机书籍接口以及计算机书籍类:
这里写图片描述
书店实现:实现IcomputerBook就可以,而BookStore类没有做任何的修改,只是在static静态模块中增加一条数据,代码如下:
这里写图片描述
运行结果:
这里写图片描述

展开阅读全文

没有更多推荐了,返回首页