转:设计模式出现之前的几大原则

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

设计模式属于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种设计模式哪种设计模式没有用到接口和抽象类?我常常在想,你的设计模式,我的设计模式,大家的设计模式应当都遵循同一种原则。这种原则就是对不同的可变性进行封装,说到底,还是对可变性的封装。

posted @ 2011-05-12 09:07  babykick  阅读(218)  评论(0编辑  收藏  举报