Jack Frost

设计模式-六大设计原则(附加实际开发建议以及计算器例子)

使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。设计模式使代码编制真正工程化,是软件工程的基石脉络,如同大厦的结构一样。

文章结构:1.单一职责原则(SRP);2.里氏替换原则(LSP);3.依赖倒置原则(DIP);4.接口隔离原则(ISP);5.迪米特原则(LoD);6.开闭原则(OCP)。7.以计算器设计为例子综合展示各大原则以及相关设计模式。


一、单一职责原则(Single Responsibility Principle,SRP原则):

定义:应该有且仅有一个原因引起类的变更。也就是一个接口或类只负责一件事。

使用原因:假设一个类负责两大职责,那么当业务变化,一个职责需要变化时就要修改这个类,所以就可能导致另一个职责会发生变化。

优点:1.类的复杂度降低,实现什么职责都又清晰明确的定义; 2.可读性高,复杂性降低;3.可维护性高,易维护;4.变更引起的风险降低。(一个接口修改只对应的实现类有影响,对其他的接口无影响,对系统扩展性、维护性有多大帮助)。

例子:

单一职责原则使用建议:接口一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

但是呢,类的设计也要结合实际,很多时候非单一职责的情况的,我们就要去妥协一下子:

1.只有逻辑足够简单,才可以在代码级别上违背SRP;
2.只有类中方法数量足够少,才可以在方法级别上违背SRP;
3.实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是要遵循SRP。

二、里氏变换原则(Liskov Substitution Principle,LSP原则):

定义:所有引用基类的地方必须能透明地使用其子类对象。即:只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或寻常。(子类可完整实现父类的业务)。

设计原因:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

这个定义有四层含义:

### (一)、子类必须完全实现父类的方法:

也就是在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明违背LSP原则。(在编写类的实现时用父类对象,调用时传入其子类实现向上转型即可完成设计。是传入子类对象,在方法实现向上转型成父类)。

针对这一含义,实际的开发有些注意点:

1.业务增多时,类的设计思路:(1)(不推荐)在moudle类中增加instanceof的判断(即每增加所有与这个类的父类有关系的类都要修改)。(2)(推荐,维护人员经常干)新增类脱离继承,简历一个独立的父类,可与原来的那个父类建立关联委托关系。

2.具体应用场景类的设计关键:子类是否能完整地实现父类的业务。(如果子类不能完整实现父类的方法,或者父类的某些方法在子类中已经发生变化,则应该简历断开父子继承关系,采用依赖、聚集、组合等关系。)

(二)子类可以有自己的个性:

也就是子类可以有自己的方法和属性。

注意:在调用那个类时,不要使用父类对象传递。因为向下转型是不安全的。

(三)覆盖或实现父类的方法时输入参数可以被放大。

相关概念:(1)方法重载(Overload)子类拥有父类的所有属性和方法,方法名相同,但输入参数又不相同。要求方法的输入参数类或数量不相同,在LSP原则下,就是子类的输入参数宽于或等于父类的输入参数。(2)方法覆写:父类和子类的同名方法的输入参数是相同的,而且两个方法的范围值为子类的小于父类的。

注意

1.子类中方法的前置条件(传递的参数)必须与父类中被覆写的方法的前置条件相同或者更宽松。

2.如果父类的输入参数类型宽于子类的输入参数类型,那么会出现父类存在的地方,子类就未必可存在。因为一旦把子类作为参数传入,调用者可能进入子类的方法。

(四)、覆写或实现父类的方法时输出结果可以被缩小:

含义:父类的一方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么LSP原则要求S必须小于或等于T。也就是说S和T是同一类型或者S是T的子类。

LSP原则的目的:增强程序的健壮性,使版本升级时,即使增加子类,原有的子类还可以继续运行。

实际项目做法:项目中,每个子类对应不同的业务含义,使用父类作为参数,传递不同的子类完成不同的业务逻辑。可是!!使用LSP原则的时候要尽量避免子类的“个性”,因为LSP原则只可以正着用不能反着用。

例子:


三、依赖倒置原则(Dependence Inversion Principle,DIP原则):

定义:简而言之,就是面向接口编程。(1)模块间的依赖通过抽象发生,实现类之间不发生之间的依赖关系,其依赖关系是通过接口或抽象类产生的;(2)接口或抽象类不依赖于实现类;(3)实现类依赖接口或抽象类。

设计原因:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。

DIP原则优点:减少类间的耦合性,提高系统的稳定性,降低并行开发引起的风险,提高代码的可读性和可维护性。

相关概念: java中一个变量有两种类型:表面类型和实际类型。表面类型是定义的时候赋予的类型,实际类型是对象的类型。

使用:分析!!分析两个类是否有依赖关系,有则制定出两者之间的接口(或抽象类)即可独立开发。

依赖的三种写法 :

(一)、在类中通过构造函数声明依赖对象。(按照依赖注入的说法,叫做构造函数注入)。


(二)在抽象中设置Setter方法声明依赖关系(依照依赖注入的说法,这叫Setter依赖注入)。


(三)接口声明依赖对象

#####

使用DIP原则的规则以及注意点

(一)、每个类尽量都又接口或抽象类,或者两者都具备。

(依赖倒置的基本要求,有了抽象才可能依赖倒置)

(二)变量的表面类型尽量是接口或者抽象类。

(但不是所有类型都要抽象,比如一个工具类一般不需接口或抽象类)

(三)任何类都不应该从具体类派生。

(比如:项目处于开发状态,不应从具体类派生出子类。但有维护的口占情况,这时则可以不考虑,但要求不超过两层继承)。

(四)尽量不要覆写基类的方法

(如果基类是一个抽象类,而且方法已经实现,子类应尽量不要覆写。类间依赖的是抽象,覆写抽象方法对依赖的稳定性有影响。)

(五)结合里氏替换原则使用

(原则:接口负责定义public属性和方法,并且声明与其他对象的的依赖关系,抽象类负责公共构造部分的实现,是实现类准确地实现业务逻辑,同时在适当的时候对父类进行细化)。

DIP原则是6个设计原则中最难实现的原则,是实现开闭原则的重要途径。


四、接口隔离原则(Interface Segregation Principle,ISP原则):

定义:客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。(一句话:接口尽量细化,同时接口中的方法尽量少。)

相关定义:接口分类:(一)实例接口。在Java中声明一个类,然后new产生一个实例,它是对一个类型的事物的描述(一个角度:Java中的类也是一种接口)。(二)类接口。Java中经常使用的interface关键字定义的接口。

做法:把大接口分解为几个小接口。(例子:美女的接口分解为外貌美女、气质型美女)

注意:单一职责要求的是类和接口职责单一,注重的是职责!!!这属于业务逻辑的划分。

接口隔离原则是对接口进行规范约束:(四层含义)开发注意点

(1)接口要尽量小。

根据接口隔离原则拆分接口时,首先必须满足单一职责原则!

(2)接口要高内聚。

高内聚:提高接口、类、模块的处理能力,减少对外的交互。要求接口–尽量少公布public方法,接口是对外的承诺,承诺越少对系统的开发越有利。

(3)定制服务:

原因:模块之间必然会有耦合,有耦合就要有相互访问的接口(可能是Java中的interface,也可能为一个类或单纯数据交换)设计时需要为各个访问者(客户端)定制服务。
定义:就是单独为一个getItem提供优良的服务
要求做法:只提供访问者需要的方法。

项目开发时接口设计规则:

(一)一个接口只服务于一个子模块或业务逻辑

(二)通过业务逻辑压缩接口中的public方法,接口时常去回顾,尽量让接口达到“满身筋骨肉“,而不是肥嘟嘟的一堆方法。

(三)已经被污染了的接口,尽量去修改,若变更的风险较大,则采用适配器模式进行转化处理。

(四)了解环境,不要盲从。每个项目或产品都有特定的环境因素,别看到大师做接口就做接口。环境不同,接口拆分的标准不同,有一点点的妥协原则的。

例子:


(五)迪米特原则(Law Of Demeter,LOD原则):

定义:一个对象应该只对其他对象有最少的了解。(也就是说你的类多复杂都不关我的事,我只关心你的public事物)

设计原因:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。

含义有四层:

(一)只和朋友交流:类与类的联系不要跨越。(也就是我想叫我的朋友的朋友去干活,就必须通过我的朋友去告诉我的朋友的朋友)

类与类之间的关系是建立在类间的,而不是方法之间,因此一个方法尽量不引入一个类中不存在的对象。

(二)朋友之间也是有距离的:一个雷公开的public属性或方法越多,修改时设计的面也就越大,变更引起的风险扩散也越大。

注意:尽量不公布太多public方法和非静态的public变量,多使用private–>类的高内聚。

技巧:一个类暴露过多方法,而发现这堆方法有关系时,可以设计一个public方法,把那堆方法统率在新的public方法,并设置为private。

(三)是自己的就是自己的:

方法设计原则:如果一个方法放在本类中,即不增加类间的关系,也对本类不产生负面影响,就可以放置在本类中。

(四)谨慎使用Serializable(序列化机制,具体看我另一篇博客输入输出机制)。

例子:一个项目使用RMI(远程方法调用)方式传递VO值对象,其实就是个网络请求过程,那么这个对象就必须实现Serializable接口。(也就是把需要网络传输的对象进行序列化,否则会出现Not Serializable Exception)。

核心观念:类间解耦,实现弱耦合。

可能导致的后果:然而其结果会产生大量中转类或跳转类,导致系统的复杂性提高,为维护带来难度。所以使用LOD原则时要反复权衡。跳转不超过两次。

例子:


六、开闭原则:

定义:一个软件实体应通过扩展来实现变化,而不是通过修改已有的代码来实现变化。

设计原因:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。

相关概念:

软件实体:(1)项目或软件产品中按照一定逻辑规则划分的模块。(2)抽象和类。(3)方法。

业务变化:(三种类型)

(1)逻辑变化。

只变化一个逻辑而不涉及其他模块,可通过修改原有类中的方法的方式来完成。但是有个前提条件:所有依赖或关联类都按照相同逻辑处理。

(2)子模块变化:

一个模块变化,必会对其他模块产生影响,特别是一个低层次的模块变化必然引起高层模块的变化。

(3)可见视图变化:

可见视图是提供给客户使用的界面,该部分的变化一般会引起连锁反应。因为多了一项显示数据,意味着你的其他部分代码也要增多一项这样的数据。

#####

针对软件产品的需求业务变化:有三大扩展方案

(一)修改接口,增加方法:(否定)

后果:所有拥有该接口的实现类必须作修改,场景类也要修改。

否定这个方案的原因:接口应该是稳定且可靠的,不应经常发生变化,否则接口作为契约的作用就失去了。

(二)修改实现类:(不推荐)

做法:通过class文件替换的方式完成业务变化。

后果:修改了的信息可能会导致其他类获得的信息不对称。

(三)通过扩展实现变化:(推荐)

做法:增加子类,覆写方法,高层次的模块(static静态模块区)通过新增子类产生新的对象。

代码例子:

开闭原则是最基础的一个原则,也就是说前五个原则是指导设计的工具和方法,而开闭原则才是精神领袖。可以说,只要遵守前五大原则,设计出来的软件就会符合开闭原则。

思想核心:用抽象构建框架,用实现扩展细节。

联系其他原则:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。


七、计算器(结合了六大设计原则与工厂模式)


好了,设计模式-六大设计原则(附加实际开发建议以及计算器例子)讲完了。本博客借鉴了一部分设计模式之禅,这是一本很好的书,只是我在这里总结以及写出自己的理解。欢迎在下面指出错误,共同学习!

转载请注明:【JackFrost的博客】

更多内容,可以访问JackFrost的博客

本博主最近会一直更新Java知识、数据结构与算法、设计模式的文章(因为最近在学习,哈哈哈),欢迎关注。

码字很辛苦,转载请注明来自JackFrost《设计模式-六大设计原则(附加实际开发建议以及计算器例子)》

Leave a Reply

Your email address will not be published. Required fields are marked *