设计模式六大原则(转载)
原作者博客链接:http://www.cnblogs.com/dolphin0520/p/3919839.html
1: 单一职责: 一个类只负责一个功能领域中的相应职责,或者可以定义为:就一个类而言,应该只有一个引起它变化的原因。
一个类承担的职责越多,它被复用的可能性越低。一个类职责过多,就相当于将这些职责耦合在一起,当其中一个职责变化,可能会影响其他职责运作,所以要将职责分离,就是将不同变化的原因封装到不同的类中,如果多个职责总是同时改变则可以将它们封装在统一类中。
单一职责原则是实现高内聚、低耦合的指导方针,它是最简单但又最难运用的原则,需要设计人员发现类的不同职责并将其分离,而发现类的多重职责需要设计人员具有较强的分析设计能力和相关实践经验。
下面是一个简单的例子:CustomerDataChart类中的方法说明如下:getConnection()用于连接数据库,findCustomers()用于查询所有的客户信息,createChar()用于创建图标,displayChart()用于显示图表。CustomerDataChart职责太多,既包含与数据库相关的方法,又包含与图表生成和显示相关的方法。其他类中可能也会用到链接数据库和findCustomers()方法,难以实现代码的复用。这里如果修改数据库连接方式,还是修改图标显示方式都要修改这个类,它不止一个引起变化的原因,违反了单一职责原则。
可以重构如下:
(1)DBUtil: 负责连接数据库。
(2)CustomerDao: 负责操作数据库中的Customer表,增/删/改/查等方法。
(3)CustomerDataChart: 负责图标的生成和现实。
2: 开闭原则: 一个软件实体应当对扩展开放,对修改关闭。即软件实体应尽量在不修改原有代码的情况下进行扩展。
任何软件都面临一个很重要的问题,即他们的需求会随着时间的推动而发生变化。当软件系统需要面对新的需求时,尽量保证系统的设计框架是稳定的。如果一个软件设计符合开闭原则,就可以方便的进行扩展,扩展时无须修改现有的代码。
为了满足开闭原则,需要对系统进行抽象❀设计,抽象化是开闭原则的关键。一般可以为系统定义一个相对稳定的抽象层,而将不同的实现行为移到具体的实现层中完成。如果要修改系统的行为,无须对抽象层进行任何修改,只需要增加新的具体类来实现新的业务功能即可。
如下图:一个系统显示各种类型的图表,如饼状图和柱状图等,为了支持更多图标显示方式,原始的方案如下:
在ChartDisplay中,根据type类型用switch语句来生成不同的具体类。
在现有的系统中,由于chartDisplay类的display()方法中针对每一个图表类变成,因此增加新类的图表不得不修改源代码。可以通过抽象化的方式对系统进行重构,使之增加新类时无须修改源代码,满足开闭原则。
重构:
(1) 增加一个抽象图标类abstractChart,将各种具体图表类作为其子类。
(2)ChartDisplay类针对抽象图表类进行编程,由客户端决定使用哪种具体图表。
注意:因为XML和Properties等格式的配置文件是纯文本文件,修改不用编译,因此在软件开发中,一般不把对配置文件的修改认为是对源代码的修改。如果一个系统在扩展时只涉及到修改配置文件,该系统可以认为是符合开闭原则的系统。 比如Spring 对具体类的注入的配置可在XML文件中进行。
3: 里氏替换:所有引用基类的地方必须能透明地使用其子类的对象。
在软件中将一个基类对象替换成它的子类对象,程序将不会产生任何错;反之,不成立。
里氏替换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。
使用里氏替换原则时需要注意如下几个问题:
(1)如果一个方法只在子类中有,在父类中没有声明,则无法在一父类定义的对象中使用该方法。
(2)在运用里氏替换原则时,尽量把父类设计为抽象类或接口,让子类继承父类或实现父接口。
如下图:客户(Customer)可以分为VIP客户(VIPCustomer)和普通客户(CommonCustomer)两类,系统需要提供一个发送Email的功能,原始的方案如下:
对这个系统分析发现,无论是普通客户还是VIP客户,发送邮件的过程是相同的,也就是说两个send()方法中的代码重复,而且系统中再增加新类型的客户,还需要
写更多的send(), 会出现大量的重复代码。
重构:
增加一个新的抽象客户类Customer,将CommonCustomer和VIPCustomer类作为其子类,邮件发送类EmailSender类针对抽i昂客户类Customer编程,更具里氏替换原则,能够接受基类对象的地方必然能接受子类对象。
4: 依赖倒置: 抽象不应该依赖于细节,细节应该依赖于抽象。要对接口编程,而不是针对实现编程。
依赖倒置原则要求我们在程序代码中传递参数时或在关联关系中,尽量引用层次高的抽象层类,即使用接口和抽象类进行变量类型声明,参数类型声明,方法返回类型声明,以及数据类型转换等,而不要用具体类来做这些事情。
在引入抽象层后,系统将具有很好的灵活性,在程序中尽量使用抽象层进行编程,将具体类写在配置文件中。系统发生变化的,只需对抽象层进行扩张,并修改配置文件,而无需修改原有系统代码。
在实现依赖倒置原则时,我们要对抽象层编程,将具体类的对象通过依赖注入的方式注入到其他对象中,有三种方式:构造注入,设值注入和接口注入。
如下图:系统经常需要将存储在TXT或Excel文件中的客户信息转存到数据库中,因此需要进行数据格式转换。在CustomerDAO中将调用数据格式转换类的方法实现格式转换和数据库插入操作。
存在的问题:每次数据来源不一样,因此需要有多个数据转化类,CustomerDAO中直接引用具体的转化类,导致每增加转换类,都要修改代码。违反了开闭原则。
重构:如下图,引入抽象数据转换类DataConvertor,CustomerDAO针对抽象类DataConvertor编程,而将具体数据转换类名称存储在配置文件中,程序运行时根据配置文件来用具体的类。不用修改现有代码,符合依赖倒置原则。热
开闭原则是目标,里氏代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致,只是分析问题时所站角度不同而已。
5: 接口隔离原则:使用多个专门的接口,而不是使用单一的总接口,即客户端不应该依赖那些它不需要的接口。
每一个接口应该承担一种相对独立的角色,不干不该干的事,该干的事都要干 。
在面向对象编程语言中,实现一个借口就需要实现该接口中定义的所有方法,因此大的总接口使用起来不一定很方便,接口的职责要单一。需要将大接口中的方法根据职责不同分别放在不同的小接口中,以确保每个接口使用起来都较为方便,并承担某一单一角色。
如下图:dataRead()用于从文件中读取数据,方法transformToXML()用于将数据转化成XML格式,方法createChart()用于创建图表,方法displayChart()用于显示图表,方法createReport()用于创建文字报表,displayRprot()用于显示文字报表。
存在的问题:接口很不灵活,如果一个具体的数据显示类无需进行数据转换,但是由于实现了该接口,将不得不实现其中声明transformToXML()方法;如果需要创建和现实图表,除了许需实现与图表相关的方法外,还需要实现创建和显示文字报表的方法。
重构:按照接口隔离原则和单一职责原则进行重构,将其中的一些方法封装在不同的小接口中,确保每一个接口使用起来都较为方便,并都承担某一单一角色。
ConcreateClass可以选择实现一个或多个接口。
6: 迪米特法则:一个软件实体尽可能少于其它实体发生相互作用。
如果一个系统符合迪米特法则,那么当其中某一个模块发生修改时,就尽量少的影响其他模块,扩展会相对容易,这是对软件实体之间通信的限制,迪米特法则要求限制软件实体之间通信的宽度和深度。
迪米特法则可以降低系统的耦合度,使类与类之间保持松散的耦合关系。
迪米特法则还有几种定义形式:不要和“陌生人”说话、只与你的直接朋友通信等,在迪米特法则中,”朋友“是这样定义的:
(1) 当前对象本身(this);
(2) 以参数形式传入到当前对象方法中的对象;
(3) 当前对象的成员对象;
(4) 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
(5) 当前对象创建的对象。
满足上述条件的是“朋友”,否则就是“陌生人”。
迪米特法则要求:尽量减少对象之间的耦合,可以通过引入一个合理的第三方来降低现有对象之间的耦合度。
在类的划分上,应当尽量创建松散耦合的类;在类的结构设计上,尽量降低成员变量和成员函数的访问权限;在类的设计上,尽量将类设计成不变类;一个对象对其他对象的引用应当降到最低。
如下图:一个控件事件的触发将导致多个其它界面控件产生响应,例如,当一个Button被单机时,对应的list,combox,textbox,label都发生改变,在初始设计方案中,界面控件之间的交互关系如下图:
由于空间之间交互关系复杂,导致在该窗口中增加新的界面控件时需要修改与之交互的其他空间的远大嘛。扩展性差。增加和删除控件不方便。
重构:引入一个专门用于控制界面控件交互的中间类(Mediator)来降低界面控件之间的耦合度。引入中间类后,界面控件之间不再发生直接引用,而是将请求发给中间类,由中间类来完成对其他空间的嗲用。当增加和删除控件时,只需要修改中间类就行。不用修改已经有的控件的源代码。