原则的重要性
项目要想设计相对合理,结构清晰,易开发、易维护,就一定要搞懂设计模式,要想搞懂设计模式,就一定要清除设计模式的六大原则。
推荐看一下《设计模式之禅》,下面都是我看过之后的一些总结。
单一职责原则
思想:一个类的改变原因不能超过一个。
就是说设计中一个接口、类或者方法尽可能的只干一件事情,反过来说如果我们有一个全能类,能干我们任何想干的是,不用想这个类肯定让人头大,频繁的改动重度的耦合。单一职责原则是帮助我们在设计中如何抽象的一个基本原则。
比如,一个手机有打电话、MP3听音乐、牌照功能,让我们做抽象设计,如果不考虑单一职责原则很简单。下图(图一),直接就是在一个类中定义几个方法就都实现了,稍微懂一点面向对象的人多能看明白这个类一点设计和抽象都没有,全部都耦合到一个类中,要是我碰见这样的类肯定非常抵触。
在根据单一职责原则做个设计。图二通过接口实现完成功能扩展,图三通过组合关系实现功能。
很容易就能看出来,每个功能都是独立的类,互不影响。度以后的开发维护只要针对相应的功能类来修改就可以了,基本符合一个类的改变原因不能超过一个思想,为什么说基本符合,因为单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,比如MP3、通话、拍照我在这个设计里面抽象成独立的功能接口了,还想抽象这几个功能还可以抽象出更细致的一些功能接口。在实际中具体抽象到什么程度,这个就要根据我们的产品的现状、未来的规划、开发的经验来确定了,适合自己的项目就好。所以只是基本符合单一原则。
总结单一原则的好处:
1.类的复杂性降低,实现什么职责都有清晰明确的定义;
2.可读性提高,复杂性降低,那当然可读性提高了;
3.可维护性提高,可读性提高,那当然更容易维护了;
4. 变更引起的风险降低,变更是必不可少的,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。
里式替换原则
搞懂里式替换原则之前一定要弄清楚面向对象的继承,Java中通过extend关键是实现类的继承。
继承给我们开发带来的好处:
1.代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性;
2.提高代码的重用性;
3.子类可以形似父类,但又异于父类,“龙生龙,凤生凤,老鼠生来会打洞”是说子拥有父的“种”,“世界上没有两片完全相同的叶子”是指明子与父的不同;
4.提高代码的可扩展性,很多开源框架的扩展接口都是通过继承父类来完成的;
5.提高产品或项目的开放性。
有好处必然就有坏处:
1.继承是侵入性的。只要继承,就必须拥有父类的所有属性和方法;
2.降低代码的灵活性。子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束;
3.增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改,而且在缺乏规范的环境下,这种修改可能带来非常糟糕的结果——大段的代码需要重构。
回过来说里式替换原则,里式替换原则就是来帮助我们更好更合理的使用继承。里式替换原则定义了四个规范:
子类必须完全实现父类的方法
子类可以有自己的个性
覆盖或实现父类的方法时输入参数可以被放大
覆写或实现父类的方法时输出结果可以被缩小
字面意思,再用继承的时候考虑一下上面四个规范。
依赖倒置原则
思想:
高层模块不应该依赖低层模块,两者都应该依赖其抽象:模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
抽象不应该依赖细节:接口或抽象类不依赖于实现类;
细节应该依赖抽象:实现类依赖接口或抽象类。
后两个没说么好说的, 接口和实现类,抽象类和子类之间一定是实现的依赖抽象的。 重点说一下模块和模块的依赖,双模块存在依赖关系一定是,通过接口/抽象类来实现依赖到,而不是实现类和实现类直接依赖。 举例说明:
有一个司机他之前每天都是开奔驰车的,类图如下:图四
有一天他感觉奔驰车好是好,做起来也很舒服,但是想买一个操控性更好的宝马来开开,这时候就发现有个问题,我的Driver司机目前只能开奔驰,不能开宝马,这肯定不科学奔驰能开宝马也可肯定能开啊。修改如下:图五
这么来看,根据依赖倒置原则设计,模块和模块之间应该通过接口依赖,司机就是开车的了不受品牌的限制了 ,能开宝马又能开奔驰。
接口隔离原则
思想:
客户端不应该依赖它不需要的接口
类间的依赖关系应该建立在最小的接口上面
比较抽象把上面两句简单概述一下, 尽量创建单一接口,不要创建臃肿庞大的接口,就是接口尽量细化里面的方法尽量要少。都是单一和单一职责原则有什么不同,单一职责原则要求的是类和接口的职责上的单一,注重的是职责,接口隔离原则是业务逻辑上的划分,要求的是接口方法尽可能的少。满足接口之间和模块之间的隔离。
假如一个接口有十个方法在单一职责原则里面是服务原则的,在接口隔离原则里面是不允许的,这不符合接口隔离原则,因为接口在提供给替他模块依赖的时候我可能只需要其中一个或者两个方法其它方法是不需要的,所以在创建接口的时候尽量创建专门的接口,让外部依赖。
举个例子,有个星探类,他要选择美女培养,评判美女的标准有,好看外貌(goodlooking)、窈窕身材(nicefigure)、有气质(greatTemperament),设计如图六
这么看没有什么问题,星探依赖美女接口,show查看美女外貌、身材、气质。但是有没有考虑到美女接口有点过于臃肿了,如果每个星探关注点不一样,有的看中长相关注外貌和身材,有的只看重气质。这样美女这个抽象接口就显得很不灵活,根据接口隔离原则星探抽象类应该只依赖我关注的内容,而美女接口把所有的都封装到一个接口里面了。所以根据接口隔离原则修改如图七
把一个臃肿的接口变更为两个独立的接口所依赖的原则就是接口隔离原则,让星探AbstractSearcher依赖两个专用的接口比依赖一个综合的接口要灵活。接口是我们设计时对外提供的契约,通过分散定义多个接口,可以预防未来变更的扩散,提高系统的灵活性和可维护性。
接口隔离规范:
接口要尽量小,根据接口隔离原则拆分接口时,首先必须满足单一职责原则。
接口要高内聚,提高接口、类、模块的处理能力,减少对外的交互。
定制服务,只提供访问者需要的方法
接口设计是有限度的,接口设计一定要注意适度,接口力度太细项目结构必定复杂,接口太臃肿接口不灵活,开发同学就要根据经验来把握好这个”度“。
迪米特法则
思想:一个类应该对自己需要耦合或调用的类知道得越少越好,内部是如何复杂都和我没关系,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。
迪米特法则对类的低耦合提出了明确的要求:
1.只与直接的朋友通信,每个对象都必然会与其他对象有耦合关系,两个对象之间的耦合就成为朋友关系,这种关系的类型有很多,例如组合、聚合、依赖等。
2.朋友间也是有距离的,朋友之间暴露的方法尽量的是一个完整的功能,不要让调用者产于被调者内部的逻辑,迪米特法则要求类,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、package-private、protected等访问权限。
3.如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。
举个例子:在体育可上老师让体育委员,清点一下全班,男生女生各来了多少人。设计图八
上面的设计根据迪米特法则,第一个错误就是teacher就是要通清点学生只需要找体育委员groupleader就可以,但是需要老师把学生告诉体育委员。触犯了只与直接朋友通信的要求。第二个错误体育委员在统计的时候需要老师把男同学和女同学分好,触犯了朋友之间距离要求,教师干了体育委员的工作,在封装的过程中这个体育委员完全可以自己处理,老师不关注这些体育委员干好活我等结果就可以了。
所以修改设计,图九
老师和只体育委员有关系,体育委员和学生有关系,满足只和直接朋友交流。在统计的时候不需要教师帮忙来区分男女,体育委员自己来处理,满足朋友间距离要求。
迪米特法则的核心观念就是类间解耦,弱耦合,只有弱耦合了以后,类的复用率才可以提高。
开闭原则
思想:一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。含义是说一个软件实体应该通过扩展来实现变化,而不是通过修改已有的代码来实现变化。
举例说明:有个图书商店BookStore,图书接口IBook(名字,价格,作者三个属性),小说类图书具体实现类NovelBook,类结构图十
一个简单的图书商店设计,现在我们要增加一个需求就是搞一个打折活动。三种方案实现:
方案一,修改接口的方式:在IBook接口和NoveIBook实现类上增加getOffPrice()方法,提供打折后金额。这个方案很明显得一个问题就是增加getOffPrice()方法,会到导致所有的类和接口都跟着改动,IBook作为接口应该是稳定且可靠的,不应该经常发生变化,否则接口作为契约的作用就失去了效能。因此,该方案否定。
方案二,修改实现类的方式,NoveIBook在getPrice()直接返回打折后的金额,但是原价金额就不存在了,也不太合适PASS掉。
方案三,通过扩展的方式,来实现变化。增加一个子类OffNovelBook,覆写getPrice方法,高层次的模块通过OffNovelBook类产生新的对象,完成业务变化对系统的最小化开发。好办法,修改也少,风险也小,修改后的类图,图十一
开闭原则的好处:
1.测试只需要关注扩展点就可以了, 不影响原有逻辑,如果动过修改来实现变化,必然对又有程序有影响,就要回归测试
2.可以提高类的复用性
3.提高项目的可维护性