软件设计的七大原则

软件设计的七大原则
设计模式遵循的一般原则:

1.开-闭原则(Open-Closed Principle, OCP):一个软件实体应当对扩展开发,对修改关闭.说的是,再设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展.换言之,应当可以在不必修改源代码的情况下改变这个模块的行为,在保持系统一定稳定性的基础上,对系统进行扩展。这是面向对象设计(OOD)的基石,也是最重要的原则。

2.里氏代换原则(Liskov Substitution Principle,常缩写为.LSP)
(1).由Barbar Liskov(芭芭拉.里氏)提出,是继承复用的基石。
(2).严格表达:如果每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换称o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型.
换言之,一个软件实体如果使用的是一个基类的话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别.只有衍生类可以替换基类,软件单位的功能才能不受影响,基类才能真正被复用,而衍生类也能够在基类的基础上增加新功能。
(3).反过来的代换不成立
(4).<墨子.小取>中说:"白马,马也; 乘白马,乘马也.骊马(黑马),马也; 乘骊马,乘马也."
(5).该类西方著名的例程为:正方形是否是长方形的子类(答案是"否")。类似的还有椭圆和圆的关系。
(6).应当尽量从抽象类继承,而不从具体类继承,一般而言,如果有两个具体类A,B有继承关系,那么一个最简单的修改方案是建立一个抽象类C,然后让类A和B成为抽象类C的子类.即如果有一个由继承关系形成的登记结构的话,那么在等级结构的树形图上面所有的树叶节点都应当是具体类; 而所有的树枝节点都应当是抽象类或者接口.
(7)."基于契约设计(Design By Constract),简称DBC"这项技术对LISKOV代换原则提供了支持.该项技术Bertrand Meyer伯特兰做过详细的介绍:
使用DBC,类的编写者显式地规定针对该类的契约.客户代码的编写者可以通过该契约获悉可以依赖的行为方式.契约是通过每个方法声明的前置条件(preconditions)和后置条件(postconditions)来指定的.要使一个方法得以执行,前置条件必须为真.执行完毕后,该方法要保证后置条件为真.就是说,在重新声明派生类中的例程(routine)时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件.

3.依赖倒置原则(Dependence Inversion Principle),要求客户端依赖于抽象耦合.
(1)表述:抽象不应当依赖于细节,细节应当依赖于抽象.(Program to an interface, not an implementaction)
(2)表述二:针对接口编程的意思是说,应当使用接口和抽象类进行变量的类型声明,参量的类型声明,方法的返还

类型声明,以及数据类型的转换等.不要针对实现编程的意思就是说,不应当使用具体类进行变量的类型声明,参量类型声明,方法的返还类型声明,以及数据类型的转换等.
要保证做到这一点,一个具体的类应等只实现接口和抽象类中声明过的方法,而不应当给出多余的方法.
只要一个被引用的对象存在抽象类型,就应当在任何引用此对象的地方使用抽象类型,包括参量的类型声明,方法返还类型的声明,属性变量的类型声明等.
(3)接口与抽象的区别就在于抽象类可以提供某些方法的部分实现,而接口则不可以,这也大概是抽象类唯一的优点.如果向一个抽象类加入一个新的具体方法,那么所有的子类型一下子就都得到得到了这个新的具体方法,而接口做不到这一点.如果向一个接口加入了一个新的方法的话,所有实现这个接口的类就全部不能通过编译了,因为它们都没有实现这个新声明的方法.这显然是接口的一个缺点.
(4)一个抽象类的实现只能由这个抽象类的子类给出,也就是说,这个实现处在抽象类所定义出的继承的登记结构中,而由于一般语言都限制一个类只能从最多一个超类继承,因此将抽象作为类型定义工具的效能大打折扣.
反过来,看接口,就会发现任何一个实现了一个接口所规定的方法的类都可以具有这个接口的类型,而一个类可以实现任意多个接口.
(5)从代码重构的角度上讲,将一个单独的具体类重构成一个接口的实现是很容易的,只需要声明一个接口,并将重要的方法添加到接口声明中,然后在具体类定义语句中加上保留字以继承于该接口就行了.
而作为一个已有的具体类添加一个抽象类作为抽象类型不那么容易,因为这个具体类有可能已经有一个超类.这样一来,这个新定义的抽象类只好继续向上移动,变成这个超类的超类,如此循环,最后这个新的抽象类必定处于整个类型等级结构的最上端,从而使登记结构中的所有成员都会受到影响.
(6)接口是定义混合类型的理想工具,所为混合类型,就是在一个类的主类型之外的次要类型.一个混合类型表明一个类不仅仅具有某个主类型的行为,而且具有其他的次要行为.
(7)联合使用接口和抽象类:
由于抽象类具有提供缺省实现的优点,而接口具有其他所有优点,所以联合使用两者就是一个很好的选择.
首先,声明类型的工作仍然接口承担的,但是同时给出的还有一个抽象类,为这个接口给出一个缺省实现.其他同属于这个抽象类型的具体类可以选择实现这个接口,也可以选择继承自这个抽象类.如果一个具体类直接实现这个接口的话,它就必须自行实现所有的接口; 相反,如果它继承自抽象类的话,它可以省去一些不必要的的方

法,因为它可以从抽象类中自动得到这些方法的缺省实现; 如果需要向接口加入一个新的方法的话,那么只要同时向这个抽象类加入这个方法的一个具体实现就可以了,因为所有继承自这个抽象类的子类都会从这个抽象类得到这个具体方法.这其实就是缺省适配器模式(Defaule Adapter).
(8)什么是高层策略呢?它是应用背后的抽象,是那些不随具体细节的改变而改变的真理. 它是系统内部的系统____隐喻.

4.接口隔离原则(Interface Segregation Principle, ISP)
(1)一个类对另外一个类的依赖是建立在最小的接口上。

(2)使用多个专门的接口比使用单一的总接口要好.根据客户需要的不同,而为不同的客户端提供不同的服务是一种应当得到鼓励的做法.就像"看人下菜碟"一样,要看客人是谁,再提供不同档次的饭菜.
(3)胖接口会导致他们的客户程序之间产生不正常的并且有害的耦合关系.当一个客户程序要求该胖接口进行一个改动时,会影响到所有其他的客户程序.因此客户程序应该仅仅依赖他们实际需要调用的方法.

5.合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)
在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分; 新的对象通过这些向对象的委派达到复用已有功能的目的.这个设计原则有另一个简短的表述:要尽量使用合成/聚合,尽量不要使用继承.

6.迪米特法则(Law of Demeter LoD)又叫做最少知识原则(Least Knowledge Principle,LKP),就是说,一个对象应当对其他对象有尽可能少的了了解.
迪米特法则最初是用来作为面向对象的系统设计风格的一种法则,与1987年秋天由Ian Holland在美国东北大学为一个叫做迪米特(Demeter)的项目设计提出的,因此叫做迪米特法则[LIEB89][LIEB86].这条法则实际上是很多著名系统,比如火星登陆软件系统,木星的欧罗巴卫星轨道飞船的软件系统的指导设计原则.
没有任何一个其他的OO设计原则象迪米特法则这样有如此之多的表述方式,如下几种:
(1)只与你直接的朋友们通信(Only talk to your immediate friends)
(2)不要跟"陌生人"说话(Don't talk to strangers)
(3)每一个软件单位对其他的单位都只有最少的知识,而且局限于那些本单位密切相关的软件单位.
就是说,如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用,如果其中的一个类需要调用另一个类的某一个方法的话,可以通过第三者转发这个调用。

7.单一职责原则(Simple responsibility pinciple SRP)
就一个类而言,应该仅有一个引起它变化的原因,如果你能想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责.应该把多于的指责分离出去,分别再创建一些类来完成每一个职责.



另外:常说的OO五大原则就是指其中的 :1、单一职责原则;2、开放闭合原则;3、里氏替换原则;4、依赖倒置原则;5、接口隔离原则。




=========================
=========================


最近在学习DP,嗯,很浅显,只是花两个小时去认识每个模式,自己没有在实际项目中有过真正的运用,文中有些话参考了java与模式那本书,剩下的是自己小小的一点点感悟,我贴出来是想让大家看看我的理解是否正确,还是有所偏差?各位OO大虾,轻拍,我每个模式都记下了自己的感悟,拍重了我怕得了害怕发帖的病,我更多的是想看到大家对我的理解的指正,谢谢~



2010年8月30日22:54:24,修改:为避免大家“太长不看的反应”,我把我的思想用粗黑高亮,大家可以直接看那部分文字即可

============================================================



最难的不是理解设计模式,而是在实际项目中灵活应用它们,设计模式看似简单,用起来却不知从何下手。理论是用来实践的,实践才能出真理。

设计模式属于OO的一部分, Gof的23种模式只不过是设计模式的沧海一粟,不同的领域都会产生不同的设计模式,当然你也可以总结出自己的设计模式。

对于学习设计模式的方法,我们不需要一开始就花很长的时间把它们都读通读透,只要花个十几天每天两个小时对每个模式有个简单的印象就可以了,然后在实际工作中去应用,去把它们读通读透。其实,只要你的OO能力达到一定的程度,设计模式都是无师自通的。

学习设计模式几个重要的问题?

每一类设计模式都是为了解决某类特定的问题而产生的,要了解这些设计模式,就必须先清楚驱动这些模式产生的需求是什么?(即是它们具体解决什么样的问题),凡事只要我们搞清楚了需求(为什么),就能使我们更加专注于需求的实现。

每个模式的简单类图?学习设计模式,最重要的是在实际应用中去发现它们的好处,从而加深我们对设计模式的理解,自己以后才能在设计中更加OO,更加具有可维护性和可扩展性。那么为了避免遗忘,使用类图能够帮助我们最快的理解某个设计模式的精髓,当然,亲自动手做一本设计模式速查手册也不免是一个不错的学习方法。

每个设计模式的优点是什么?缺点又是什么?每个设计模式都是为了解决某一类问题而反复被人们总结、推敲凝练而成。那么除了此之外,它还具有其他一些优点,同时不可避免也会带来一些缺点,掌握这些能够帮助我们知道何时采用何种设计模式。

它著名的应用举例?java_api里面使用了很多设计模式,我们可以在里面找到这些模式的踪影,

这对于理解某个模块的设计原理再好不过了。



OCP(开闭原则,对扩展开发,对修改关闭)
OCP原则就是在不修改源代码的情况下,设计方案能适应于各种扩展的需求(当然这是最理想的情况)。做到OCP有两点:抽象、对可变性封装。

抽象,java里给我们提供了两种途径,抽象类和接口,拿接口来说,它固定了子类的方法特征,所有实现它的子类必须实现具体的方法,这是一种约定,这种约定就是一种抽象层,我们认为它定义的方法特征预见了任何可能的扩展,因此,这种约束在任何情况下不得改变,那么,我们可以理解为:它是对修改的关闭,遵循了OCP的第二条原则。同时,由于子类实现接口的方法可以又不同的具体表现形式,这些不同的形式足以对系统产生不同的行为,因此,系统的设计对扩展是开放的,这就满足了OCP的第一条原则。

对可变性的封装原则,很早以前我知道了设计应该将不可变的和可变的分开,这里偏离一下主题,组件设计(也成框架设计)通常将可变性放在配置文件中(通常是文件系统,比如XML),将不可变性形成一套模板,或者是一套流程,通过解析配置文件,将业务逻辑载入系统,使系统按照用户需求的业务逻辑去运行,尽管业务逻辑经常变化,但是我们通过简单的配置文件就可以解决,这不得不说可变性和不可变形的分离的确值得我们好好思考。下面我们回到主题,对可变性的封装,将会驱使你找到系统中的可变因素,将它封装起来。怎么去封装?不是把你的可变因素散落在代码的很多角落里,它应该是封装成为一个类,同一种可变性的具体表现映射为同一个抽象类(或者接口)的不同子类。想象一下,我们经常说接口犹如插座,插座就是一种可变性,不同的可变性犹如不同的颜色,不同的大小等等,这些可变性我们应该把它们封装成不同子类里。继承应当被看做是封装变化的方法,而不当认为是从一般对象生成特殊对象的方法。我们通常认为凤凰是从鸟继承过来是因为凤凰是特殊的,其实不然,凤凰对象只是封装了鸟类的可变性因素,麻雀也是鸟,它明显区别于凤凰的可变性因素,因为麻雀永远不会变成凤凰。

里氏代换原则(LSP)
任何基类可以出现的地方,子类一定可以出现(反过来不成立)。

这好像是描述了继承的一种原则,确实,在实现继承的时候,我们尽量考虑一下,java编译器能够检查语法上对里氏代换原则的支持,但是并不能支持商业逻辑上的LSP。考虑一个比较著名的长方形与正方形的问题,它能帮助我们更加深刻的理解LSP原则。

通常,在数学上来看,正方形确

实属于长方形的一种,依照这种思维,正方形继承于长方形也是自然不过了。但是别忘了,长方形的定义是什么?长方形的高小于等于长方形的宽,下面是针对LSP原则的一段测试代码:

Public void resize(Rectangle r){

While(r.getHeight() <= r.getWidth()){

r.setWidth(r.getWidth() + 1);

}

}

试试LSP原则在这段测试代码中成不成立:当传入的是一个长方形,它的意义在于增加长方形的高度直到不大于长方形的宽度为止,如果传入的是一个正方形,长方形的子类,想象一下,这段代码会出现什么样的结果,因为正方形的高永远等于长方形的宽,这个循环永远持续下去,直到栈溢出为止。

还有一个例子,java.util库中的properties继承于hashtable,但是properties是一种特殊的hashtable,它只接受String类型的key和value,而超类hashtable的key和value却能接受任何的数据类型。这意味着,LSP在它们之间并不成立,所以,这是一个反面教材,时刻提醒我们,该在什么情况下使用继承。

依赖倒换原则(DIP)
要依赖于抽象,不要依赖于具体实现。DIP跟另一种说法含义相近:面向接口编程。

不知道何时有“层”这个说法,尽管你不会一眼看出XX软件分为几个层,但是确实这样的分层是有理由的,分开即耦合度降低,各司其责。你可以想想你所在公司的管理制度,那是一个金字塔模型。上层是高层管理人员,它们下发的命令直接影响最底层的工人,而最底层工人具体的工作内容并不影响上面的高层管理人员。商务逻辑层不能依赖于实现层,公司的架构师要负责商务逻辑层的维护,底层coder负责实现层,公司不可能将这种逻辑依赖于实现层,不仅仅是coder和架构师的素质差别,更重要的是这根本就是一个错误的逻辑。

依赖倒换原则是COM、CORBA、JavaBean以及EJB等架构设计模型背后的基本原则。

依赖的种类:零耦合关系(两个类没有发生任何相互引用)、具体耦合关系(两个具体类直接引用对方的具体类型)、抽象耦合关系(两个类的引用利用其抽象类或者接口来实现,这是DIP的核心)。

怎么实现依赖倒换原则?实现层要依赖于抽象层,那么所有的商务逻辑必须在抽象层制定好,而且要更改实现层的实现方式时,必须保证抽象层不做任何改变。通过定义抽象类和接口来实现商务逻辑的集中控制,通过面向接口编程来保证当实现层改变实现方式时,抽象层的商务逻辑不做改变。针对接口编程意思是说:应当使用抽象类和接口进行变量的类型声明、参量的类型声明、方法的返还类型声明以及数据类型的转换等。

模板方法直

观的为我们解释了通过定义抽象类和接口来实现商务逻辑的集中控制,一般通过继承抽象类来得到抽象类具体方法的使用权限,通常这些方法的定义需求是统一的商务逻辑定义。迭代子模式也做到了这点,它将公用的迭代功能定义为一个接口,实现了这个接口的集合类表示支持迭代,使用迭代子接口声明的类型,在实现过程中,当集合类发生改变时,它也能正常工作,这就是所谓的抽象耦合关系,只对迭代接口的引用。

坚持DIP原则会生产出大量的类,它们大多都是抽象类和接口,怎么处理好这种引用关系是值得思考的。另外,依赖倒换原则假定所有的具体类都是会变化的,但是实际情况中不总是这样,这需要我们实际情况实际解决。

接口隔离原则(ISP)
应当为客户端尽可能小的单独的接口,而不要提供大的总接口。

总觉得这个和“单一职责原则”很像,很多人都把它们分开来讲,我不想把它们分的太清楚,就当一种说法对另一种说法的诠释好了。

这个原则比较简单易懂,就是应该把接口细粒化,单位化,避免接口污染。这样做的好处也很明显,接口就相当于对外界的承诺,你愿意对外界承诺的更多还是更少呢?其次,从美学上讲,这是一个污染问题,虽然不可能将美学纳入设计原则中,但是鄙人就喜欢做什么都干干净净、优雅利落,是个典型的完美主义者。

虽然ISP很好理解,但是实际中却常常被忽略,我经常会在公司里的代码中看到一个总的大接口,更要命的是它是从很多单位化细粒化接口继承过来的,而往往消费这个接口需要实现它所有的方法,这就诱发程序员偷懒行为:将不需要实现的方法置为真空状态(即里面什么都不写)。

定义接口通常我们会以职责来划分,一个职责一个借口,不要将多种职责放在同一个接口里,评判的标准是:你的接口是否仅仅因为一种原因而改变?但是“职责”是一个没有标准化的术语,实际项目中,100个人可能有98个人都有不同的职责看法(另外两个人看了此文章)。下面我举几个简单的例子来展示当我们遇到问题时,接口隔离原则为我们带来的好处。

第一, 如果我将两种或两种以上的职责都放在一个大的接口里,如果有一天我发现只需要其中的一种职责就可以了,那我们难道就强制它实现这个接口,然后将它不关心的职责真空化吗(就像上面说的,将不需要实现的方法置为真空状态,那就只能说明你这个接口包含了过多的职责)?

第二, 如果哪天我们发现其中有一个职责改变了,那么我们是不是要忍着牵一发而动全身的悲

剧而修改这个接口呢?比如考虑手机通信这个接口,它有拨打电话和挂电话等操作,姑且我们把它看做协议职责,它还有通话,回应等操作,姑且我们把它看做数据传输职责,现在我们的手机都是2G的,哪天换成了3G的,视频通话的,那不仅仅是语音数据的传输,还包括视频数据的传输,那是不是我们必须要改动这个接口了呢?想想一下,有多少个类实现了这个接口我们就相应的修改这些类,如果我们开始的时候就定义为两个接口,两个职责,这个时候是不是就只需修改实现了数据传输接口的类就行了呢?

第三, 如果你在写一个API组件,需要对外界提供一个借口供二次开发人员使用,我想,你肯定不想对外界做出更多的承诺吧,那么,就把你的接口单一职责化吧。

总的来说,我觉得有一句话概括很帅气:“我单纯,所以我快乐”。简简单单,清晰明了,分工明确,细粒度低,何尝不好。这句话不仅仅适用于接口,对类和方法也适用,当然,具体情况视具体项目而定。比如考虑一个修改用户信息的方法,它根据传入的type不同,分为修改用户名,用户密码,用户…。这种方法细粒度怎样?如果这个方法出问题了,受影响的功能会波及修改用户名,修改用户密码,修改…如果你所有对用户信息的修改都放在了此方法里,那么所有的用户信息修改都将会受到影响,这不是开玩笑的。而且,这样的代码不是自解释性的,可读性也不好,也不够复用性。

举一个亲身经历的例子,我有一个需求,判断当前的设备实际类型(你可以认为是电信设备)和当前用户配置的网元类型是否一样,相信很多人都会去写一个返回true或false的方法,这是不恰当的。因为你这个方法包含了两个职责:1.得到设备实际类型2.比较当前的实际类型和用户配置的类型是否一样。所以应该只需定义一个方法即可,它仅仅是得到当前设备的实际类型,然后返回给你,你在主程序中再去判断。这样做的好处是,如果有一天,我需要在程序的另一个地方得到当前设备的实际类型,那么你写的这个方法就不能复用,必须再写一个这样的方法才行。这是多么的纠结啊,所以,我单纯,我快乐。

原则是死的,人是活的,凡事适当就好。

合成/聚合复用原则(CARP)
要尽量使用合成/聚合,而不是继承关系达到目的。

我不想去区分合成和聚合的区别。

通常如果你正在疑惑你该使用合成/聚合还是该使用继承时,我给你两种方法去判断:1.使用“Has-A”和“Is-A”来判断;

2.使用里氏代换原则来判断。

迪米特法则(LoD)
一个软件实体尽当尽可能少的与

其他软件实体发生相互作用。

LoD表述:
1.只与你直接的朋友通信;
2.不要跟“陌生人”说话;
3.每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。



在我看来,软件最重要的是可维护性和良好的扩展性(需求频繁的变化是最要命的),可变性是任何软件维护最头疼的问题,有些还是灾难性的,如何在设计之初就预料到这些问题才是最为难能可贵的。大多数人认为这依赖于构架师的经验,这一点我比较赞同,但是预料到问题之后,怎么去封装这些问题,我想我们都应该掌握。

【软件设计的七大原则】 可变性的封装,拿什么去封装?java为我们提供了两种方式,接口和抽象类,在变化之处放置一个接口或者抽象类是一个不错的办法。看一看23种设计模式哪种设计模式没有用到接口和抽象类?我常常在想,你的设计模式,我的设计模式,大家的设计模式应当都遵循同一种原则。这种原则就是对不同的可变性进行封装,说到底,还是对可变性的封装。

    推荐阅读