OO设计原则SOLID之浅分析

要完成一个小小的课题研究,正愁不知道怎么记录,并且博客空的时间有点长,所以就把这个课题放到这里当成随笔跟大家分享一下。嘛,对我来说是

一举两得了。

研究课题:

OO的五大设计原则:

(SRP) The Single Responsibility Principle 单一责任原则;

(OCP) The Open-Closed Principle 开放封闭原则;

(LSP) The Liskov Substitution Principle Liskov替换原则;

(DIP) The Dependency Inversion Principle 依赖转置原则;

(ISP) The Interface Segregation Principle 接口聚合原则;

通过查阅资料,了解五种设计原则的含义和基本思想,它们共同应对软件设计中面临的什么NFR,并通过例子解释说明。
若违反它们,会带来何种危害?
首先解释一下SOLID设计原则的含义和基本思想:
单一责任原则:
当需要修改某个类的时候原因有且只有一个(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE)。换句话说就是让一个类
只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。 
开放封闭原则
软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。这个原则是诸多面向对象编程原则中最抽象、最难理解
的一个。
Liskov替换原则:
当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系 
依赖转置原则:
1. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
2. 抽象不应该依赖于细节,细节应该依赖于抽象 
接口聚合原则:
 不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。
 很明显上述解释是大牛的解释(转自:http://www.cnblogs.com/shanyou/archive/2009/09/21/1570716.html),但是课题还要求解释它们共同应
对的NFR特性以及违反他们所带来的危害,说到这里,要是大牛能写全该多好~~
    考虑到非功能需求,我们可以列举出:完备性、正确性、健壮性、可靠性、效率、可用性、可维护性、可移植性、清晰性、安全性、兼容性、
经济性、商业质量等。
  S原则:
保持一个类专注于单一功能点上,它会使软件中的类更加健壮,正确性、可靠性、可维护性都会提高。举个例子:想象一个软件程序里有一个用于
编辑和打印报表的模块。这样的一个模块存在两个改变的原因。第一,报表的内容可以改变(编辑)。第二,报表的格式可以改变(打印)。
这两方面会的改变因为完全不同的起因而发生:一个是本质的修改,一个是表面的修改。单一功能原则认为这两方面的问题事实上是两个分离的功能,
因此它们应该分离在不同的类或者模块里。把有不同的改变原因的事物耦合在一起的设计是糟糕的。如果有一个对于报表编辑流程的修改,那么将存在
极大的危险性,假使这两个功能存在于同一个类中的话,打印功能的代码会因此不工作。
O原则:
开闭原则意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要
代码审查,单元测试以及诸如此类的用以确保产品使用质量的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。由此可见,遵守
开闭原则会使软件的正确性、健壮性、可靠性、可维护性、安全性、兼容性、经济性、商业质量有效提高。

 Sunny软件公司开发的CRM系统可以显示各种类型的图表,如饼状图和柱状图等,为了支持多种图表显示方式,原始设计方案如图1所示:

初始设计方案结构图

      在ChartDisplay类display()方法中存在如下代码片段:

[java] view plaincopy
  1. ......  
  2. if (type.equals("pie")) {  
  3.     PieChart chart = new PieChart();  
  4.     chart.display();  
  5. }  
  6. else if (type.equals("bar")) {  
  7.     BarChart chart = new BarChart();  
  8.     chart.display();  
  9. }  
  10. ......  

      在该代码中,如果需要增加一个新的图表类,如折线图LineChart,则需要修改ChartDisplay类的display()方法的源代码,增加新的判断逻辑,违反了

开闭原则。

      现对该系统进行重构,使之符合开闭原则。

       在本实例中,由于在ChartDisplay类的display()方法中针对每一个图表类编程,因此增加新的图表类不得不修改源代码。可以通过抽象化的方式对系统进行重构,使之

增加新的图表类时无须修改源代码,满足开闭原则。具体做法如下:

      (1) 增加一个抽象图表类AbstractChart,将各种具体图表类作为其子类;

      (2)  ChartDisplay类针对抽象图表类进行编程,由客户端来决定使用哪种具体图表。

      重构后结构如图2所示:

图2 重构后的结构图

      在图2中,我们引入了抽象图表类AbstractChart,且ChartDisplay针对抽象图表类进行编程,并通过setChart()方法由客户端来设置实例化的具体图表对象,在

ChartDisplaydisplay()方法中调用chart对象的display()方法显示图表。如果需要增加一种新的图表,如折线图LineChart,只需要将LineChart也作为AbstractChart的子类,

在客户端向ChartDisplay中注入一个LineChart对象即可,无须修改现有类库的源代码。     

       注意:因为xmlproperties等格式的配置文件是纯文本文件,可以直接通过VI编辑器或记事本进行编辑,且无须编译,因此在软件开发中,一般不把对配置文件的修

改认为是对系统源代码的修改。如果一个系统在扩展时只涉及到修改配置文件,而原有的Java代码或C#代码没有做任何修改,该系统即可认为是一个符合开闭原则的系统。

   L原则:
这个有点模糊,直接上例子:

 在Sunny软件公司开发的CRM系统中,客户(Customer)可以分为VIP客户(VIPCustomer)和普通客户(CommonCustomer)两类,系统需要提供一个发送Email的功能,原始设计方案如图1所示:

1原始结构图

      在对系统进行进一步分析后发现,无论是普通客户还是VIP客户,发送邮件的过程都是相同的,也就是说两个send()方法中的代码重复,而且在本系统中还将增加新类型的客户。为了让系统具有更好的扩展性,同时减少代码重复,使用里氏代换原则对其进行重构。

      在本实例中,可以考虑增加一个新的抽象客户类Customer,而将CommonCustomerVIPCustomer类作为其子类,邮件发送类EmailSender类针对抽象客户类Customer

编程,根据里氏代换原则,能够接受基类对象的地方必然能够接受子类对象,因此将EmailSender中的send()方法的参数类型改为Customer,如果需要增加新类型的客户

,只需将其作为Customer类的子类即可。重构后的结构如图2所示:


图2  重构后的结构图

通过实例可以发现,使用里氏代换原则可以有效提高软件的效率、可用性、可维护性、可扩展性、清晰性、经济性、商业质量等;反之,会造成软件开发过程中的代码
重复,增加软件的复杂性。
D原则:
 依旧直接上例子:

  Sunny软件公司开发人员在开发某CRM系统时发现:该系统经常需要将存储在TXTExcel文件中的客户信息转存到数据库中,因此需要进行数据格式转换。在客户数据操作类中将调用数据格式转换类的方法实现格式转换和数据库插入操作,初始设计方案结构如图1所示:

初始设计方案结构图

      在编码实现图1所示结构时,Sunny软件公司开发人员发现该设计方案存在一个非常严重的问题,由于每次转换数据时数据来源不一定相同,因此需要更换数据转换类,如有时候需要将TXTDataConvertor改为ExcelDataConvertor,此时,需要修改CustomerDAO的源代码,而且在引入并使用新的数据转换类时也不得不修改CustomerDAO的源代码,系统扩展性较差,违反了开闭原则,现需要对该方案进行重构。

      在本实例中,由于CustomerDAO针对具体数据转换类编程,因此在增加新的数据转换类或者更换数据转换类时都不得不修改CustomerDAO的源代码。我们可以通过

引入抽象数据转换类解决该问题,在引入抽象数据转换类DataConvertor之后,CustomerDAO针对抽象类DataConvertor编程,而将具体数据转换类名存储在配置文件中,

符合依赖倒转原则。根据里氏代换原则,程序运行时,具体数据转换类对象将替换DataConvertor类型的对象,程序不会出现任何问题。更换具体数据转换类时无须修改

源代码,只需要修改配置文件;如果需要增加新的具体数据转换类,只要将新增数据转换类作为DataConvertor的子类并修改配置文件即可,原有代码无须做任何修改,

满足开闭原则。重构后的结构如图2所示:

2重构后的结构图

     

      在上述重构过程中,我们使用了开闭原则、里氏代换原则和依赖倒转原则,在大多数情况下,这三个设计原则会同时出现,开闭原则是目标,里氏代换原则是基础

,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。

此外,通过实例可以发现,使用依赖倒转原则可以有效提高软件的健壮性、可靠性、效率、可用性、可维护性、安全性、兼容性、经济性、商业质量等。反之,违反该

原则会导致软件系统中高低层模块联系过于紧密,降低了系统的健壮性,可维护性等。

I原则:

   

  Sunny软件公司开发人员针对某CRM系统的客户数据显示模块设计了如图1所示接口,其中方法dataRead()用于从文件中读取数据,方法transformToXML()用于将数据转换成XML格式,方法createChart()用于创建图表,方法displayChart()用于显示图表,方法createReport()用于创建文字报表,方法displayReport()用于显示文字报表。

初始设计方案结构图

      在实际使用过程中发现该接口很不灵活,例如如果一个具体的数据显示类无须进行数据转换(源文件本身就是XML格式),但由于实现了该接口,将不得不实现其中声明的transformToXML()方法(至少需要提供一个空实现);如果需要创建和显示图表,除了需实现与图表相关的方法外,还需要实现创建和显示文字报表的方法,否则程序编译时将报错。

      现使用接口聚合原则对其进行重构。

      在图1中,由于在接口CustomerDataDisplay中定义了太多方法,即该接口承担了太多职责,一方面导致该接口的实现类很庞大,在不同的实现类中都不得不实现接口中

定义的所有方法,灵活性较差,如果出现大量的空方法,将导致系统中产生大量的无用代码,影响代码质量;另一方面由于客户端针对大接口编程,将在一定程序上破坏

程序的封装性,客户端看到了不应该看到的方法,没有为客户端定制接口。因此需要将该接口按照接口聚合原则和单一职责原则进行重构,将其中的一些方法封装在不同

的小接口中,确保每一个接口使用起来都较为方便,并都承担某一单一角色,每个接口中只包含一个客户端(如模块或类)所需的方法即可。

      通过使用接口聚合原则,本实例重构后的结构如图2所示:

重构后的结构图

     在使用接口聚合原则时,我们需要注意控制接口的粒度,接口不能太小,如果太小会导致系统中接口泛滥,不利于维护;接口也不能太大,太大的接口将违背接口

隔离原则,灵活性较差,使用起来很不方便。一般而言,接口中仅包含为某一类用户定制的方法即可,不应该强迫客户依赖于那些它们不用的方法。

此外,通过实例可以发现,使用接口聚合原则可以有效提高软件的健壮性、可靠性、效率、可用性、可维护性、经济性、商业质量等。反之,会破坏系统的封装性以及

产生大量与需求无关的代码,严重影响效率。

 以上,为这次课题的全部内容,由于篇幅限制,很多具体解释都简略了,具体信息可以参考:http://blog.csdn.net/lovelion/article/details/7562842(本篇及向上3篇)

你可能感兴趣的:(随笔)