设计模式
(Design pattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的。
设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理地运用设计模式可以完美地解决很多问题,每种模式在现实中都有相应的原理来与之对应,每种模式都描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是设计模式能被广泛应用的原因。
设计模式六大原则主要是指:
开闭原则(Open
Closed Principle);
单一职责原则(Single Responsibility Principle);
依赖倒置原则(Dependence Inversion Principle)。
接口隔离原则(Interface Segregation Principle);
迪米特法则(Law of Demeter),又叫“最少知道法则”;
里氏替换原则(Liskov Substitution Principle);
把这6 个原则的首字母(里氏替换原则和迪米特法则的首字母重复,只取一个)联合起来就是:SOLID(稳定的),其代表的含义也就是把这6 个原则结合使用的好处:建立稳定、灵活、健壮的设计
[if !supportLists]一、 [endif]开闭原则
[if !supportLists]1. [endif]定义:
开闭原则,在面向对象编程领域中,规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的”,这意味着一个实体是允许在不改变它的源代码的前提下变更它的行为。该特性在产品化的环境中是特别有价值的,在这种环境中,改变源代码需要代码审查,单元测试以及诸如此类的用以确保产品使用质量的过程。遵循这种原则的代码在扩展时并不发生改变,因此无需上述的过程。
开闭原则的核心思想是面向抽象编程,定义出接口并实现其方法,通过继承方式进行扩展,都可以体现出开闭原则。
[if !supportLists]2. [endif]模拟场景
在实际开发中常常会遇到这样的问题,从别人那里接手过来的代码,还没来的及熟悉代码,项目就催着赶紧升级,当你想使用一个功能的时候,你可能发现项目里有相关代码,但是你又不敢用,你怕改出来问题,所以一般可能都会采取新增一块功能一样的代码。其实这就是简单的遵循了开闭原则。
一个软件实体,如类,模块和函数应该对外扩展开发,对内修改关闭。
解读:用抽象构建框架,用实现扩展细节。不以改动原有类的方式来实现新需求,而是应该以实现事先抽象出来的接口(或具体类继承抽象类)的方式来实现。
优点:开闭原则的优点在于可以在不改动原有代码的前提下给程序扩展功能。增加了程序的可扩展性,同时也降低了程序的维护成本。
[if !supportLists]二、 [endif]单一职责原则
[if !supportLists]1. [endif]定义
单一职责原则(SRP:Single responsibility principle)又称单一功能原则。
它规定一个类应该只有一个发生变化的原因。该原则由罗伯特·C·马丁(Robert
C. Martin)于《敏捷软件开发:原则、模式与实践》一书中给出的。马丁表示此原则是基于汤姆·狄马克(Tom DeMarco)和Meilir
Page-Jones的著作中的内聚性原则发展出的。
所谓职责是指类变化的原因。如果一个类有多于一个的动机被改变,那么这个类就具有多于一个的职责。而单一职责原则就是指一个类或者模块应该有且只有一个改变的原因。
一个类只允许有一个职责,即只有一个导致该类变更的原因。
解读:类职责的变化往往就是导致类变化的原因:也就是说如果一个类具有多种职责,就会有多种导致这个类变化的原因,从而导致这个类的维护变得困难。往往在软件开发中,随着需求的不断增加,可能会给原来的类添加一些本来不属于它的一些职责,从而违反了单一职责原则。如果我们发现当前类的职责不仅仅有一个,就应该将本来不属于该类真正的职责分离出去。不仅仅是类,函数也要遵循单一职责原则,即一个函数制作一件事情。如果发现一个函数里面有不同的任务,则需要将不同的任务以另一个函数的形式分离出去。
优点:如果类与方法的职责划分的很清晰,不但可以提高代码的可读性,更实际性地更降低了程序出错的风险,因为清晰的代码会让bug无处藏身,也有利于bug的追踪,也就是降低了程序的维护成本。
[if !supportLists]三、 [endif]依赖倒置原则
[if !supportLists]1. [endif]依赖倒置原则的概念
依赖倒置原则(Dependence Inversion Principle)是程序要依赖于抽象接口,不要依赖于具体实现。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。
依赖倒置原则的具体含义:
1、高层模块不应该依赖底层模块,二者都应该依赖抽象。
2、抽象不应该依赖细节,细节应该依赖抽象。
3、依赖倒置的中心思想是面向接口编程。
4、依赖倒置原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础搭建的架构要稳定的多。
5、使用接口或抽象类的目的是指定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类来完成。
2.依赖倒置原则的目的
首先在面向对象的开发,上层调用下层,上层依赖于下层,当下层剧烈变动时上层也要跟着变动,这就会导致模块的复用性降低而且大大提高了开发的成本。
其次在面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。这大大降低了客户程序与实现细节的耦合度。
面向过程思想的结构图如下:
依赖抽象而不是依赖实现。抽象不应该依赖细节,细节应该依赖抽象。高层模块不能依赖低层模块,二者都应该依赖抽象。
解读:针对接口编程,而不是针对实现编程。尽量不要从具体的类派生,而是以继承抽象类或实现接口来实现。关于高层模块与低层模块的划分可以按照决策能力的高低进行划分。业务层自然就处于上层模块,逻辑层和数据层自然就归类为底层。
优点:通过抽象来搭建框架,建立类和类的关联,以减少类间的耦合性。而且以抽象搭建的系统要比以具体实现搭建的系统更加稳定,扩展性更高,同时也便于维护。
[if !supportLists]四、 [endif]接口分离原则
[if !supportLists]1. [endif]定义
接口隔离原则(Interface Segregation Principle, ISP)是指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。
[if !supportLists]2. [endif]接口隔离原则描述
根绝接口隔离原则,当一个接口太大时,我们需要将它分割成一些细小的接口,使用该接口的客户端只需知道与之相关的方法即可。每一个接口应该承担一种相对独立的角色,不干不该干的事情,干该干的事请。这里的"接口"往往有两种不同的定义:一种是指一个类型所具有的方法特征的集合,仅仅是一种逻辑上的抽象;另外一种是某种语言上具体的"接口"定义,比如Java语言的interface。对于这两种不同的含义,接口隔离原则表达以及含义所以不同:
[if !supportLists]Ø [endif]当把”接口“理解成一个类型所具有的方法特性的集合时,就是一种逻辑上的概念,接口的划分将直接带来类的划分。可以把接口理解成角色,一个接口只能代表一个角色,每个角色都有它特定一个接口,此时这个原则可以叫做角色隔离原则。
[if !supportLists]Ø [endif]如果把"接口"理解成狭义的特定语言接口,那么接口隔离原则表达的意思就是接口仅仅提供客户端需要的行为,客户端不需要的行为则隐藏起来,应当为客户端提供尽可能小的接口,而不要提供大的总接口。在面向对象编程语言中,实现一个接口类就要实现该接口定义的所有方法,因此大的总接口使用起来不一定很方便,为了使接口的职责单一,需要把大接口中的方法根据其职责不同放到不同的小接口中,确保每个接口使用起来都很方便,并都承担某一单一角色。接口应该尽量细化,同时接口中的方法应该尽量少,每个接口中只包含一个客户端(如子模块或者业务逻辑类)所需的方法即可,这种机制也称为“定制服务”,即为不同的客户端提供宽窄不同的接口。
这个原则指导我们在设计接口时应当注意以下几点:
(1)一个类对另一个类的依赖应该建立在最小的接口之上。
(2)建立单一接口,不要建立庞大臃肿的接口。
(3)尽量细化接口,接口中的方法尽量少(不是越少越好,一定要适度)。
接口隔离原则符合我们常说的高内聚、低耦合的设计思想,可以使类具有很好的可读性、可扩展性和可维护性。我们在设计接口的时候,要多花时间去思考,要考虑业务模型,包括对以后有可能发生变更的地方还要做一些预判。所以,对于抽象、对于业务模型的理解是非常重要的。下面我们来看一段代码,对一个动物行为进行抽象描述。
[if !supportLists]五、 [endif]迪米特法则
[if !supportLists]2. [endif]定义
迪米特法则(Law of Demeter, LoD)是1987年秋天由lan holland在美国东北大学一个叫做迪米特的项目设计提出的,它要求一个对象应该对其他对象有最少的了解,所以迪米特法则又叫做最少知识原则(Least Knowledge
Principle, LKP)。
[if !supportLists]3. [endif]意义
迪米特法则的意义在于降低类之间的耦合。由于每个对象尽量减少对其他对象的了解,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。
值得一提的是,这一法则却不仅仅局限于计算机领域,在其他领域也同样适用。比如,美国人就在航天系统的设计中采用这一法则。
[if !supportLists]4. [endif]实践
那么在实践中如何做到一个对象应该对其他对象有最少的了解呢?如果我们把一个对象看作是一个人,那么要实现“一个人应该对其他人有最少的了解”,做到两点就足够了:1.只和直接的朋友交流;2.减少对朋友的了解。下面就详细说说如何做到这两点。
[if !supportLists]5. [endif]只和直接的朋友交流
迪米特法则还有一个英文解释是:talk only to your immediate friends(只和直接的朋友交流)。什么是朋友呢?每个对象都必然会与其他的对象有耦合关系,两个对象之间的耦合就会成为朋友关系。那么什么又是直接的朋友呢?出现在成员变量、方法的输入输出参数中的类就是直接的朋友。迪米特法则要求只和直接的朋友通信。
注意:只出现在方法体内部的类就不是直接的朋友,如果一个类和不是直接的朋友进行交流,就属于违反迪米特法则。
[if !supportLists]6. [endif]减少对朋友的了解
迪米特法则的目的,是把我们的类变成一个个“肥宅”。“肥”在于一个类对外暴露的方法可能很少,但是它内部的实现可能非常复杂(这个解释有点牵强~)。“宅”在于它只和直接的朋友交流。在现实生活中“肥宅”是个贬义词,在日本“肥宅”已经成为社会问题。但是在程序中,一个“肥宅”的类却是优秀类的典范。
因此只要做到只和直接的朋友交流和减少对朋友的了解,就能满足迪米特法则。
[if !supportLists]7. [endif]注意
迪米特法则的核心观念就是类间解耦,弱耦合。只有弱耦合了之后,类的复用才可以提高,类变更的风险才可以减低。但解耦是有限度的,除非是计算机的最小单元--二进制的0和1,否则都是存在耦合的。所以在实际项目中,需要适度地参考这个原则,避免过犹不及。
[if !supportLists]六、 [endif]里氏替换原则
[if !supportLists]1. [endif]里式替换原则定义
里式替换原则是用来帮助我们在继承关系中进行父子类的设计。
里氏替换原则(Liskov Substitution principle)是对子类型的特别定义的. 为什么叫里式替换原则呢?因为这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara
Liskov)提出来的。
里氏替换原则主要阐述了有关继承的一些原则,也就是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的原理。里氏替换原是继承复用的基础,它反映了基类与子类之间的关系,是对开闭原则的补充,是对实现抽象化的具体步骤的规范。
里式替换原则有两层定义:
定义1
If S is
a subtype of T, then objects of type T may be replaced with objects of type S,
without breaking the program。
如果S是T的子类,则T的对象可以替换为S的对象,而不会破坏程序。
定义2:
Functions
that use pointers of references to base classes must be able to use objects of
derived classes without knowing it。
所有引用其父类对象方法的地方,都可以透明的替换为其子类对象
这两种定义方式其实都是一个意思,即:应用程序中任何父类对象出现的地方,我们都可以用其子类的对象来替换,并且可以保证原有程序的逻辑行为和正确性。
[if !supportLists]2. [endif]里氏替换原则有至少有两种含义
[if !supportLists]1. [endif]里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事。
[if !supportLists]2. [endif]如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。
不符合LSP的最常见的情况是,父类和子类都是可实例化的非抽象类,且父类的方法被子类重新定义,这一类的实现继承会造成父类和子类间的强耦合,也就是实际上并不相关的属性和方法牵强附会在一起,不利于程序扩展和维护。
[if !supportLists]3. [endif]使用里式替换原则的目的
采用里氏替换原则就是为了减少继承带来的缺点,增强程序的健壮性,版本升级时也可以保持良好的兼容性。即使增加子类,原有的子类也可以继续运行
[if !supportLists]4. [endif]里式替换的规则
里式替换原则的核心就是“约定”,父类与子类的约定。里氏替换原则要求子类在进行设计的时候要遵守父类的一些行为约定。这里的行为约定包括:函数所要实现的功能,对输入、输出、异常的约定,甚至包括注释中一些特殊说明等。
[if !supportLists]4.1 [endif]子类方法不能违背父类方法对输入输出异常的约定
1. 前置条件不能被加强
前置条件即输入参数是不能被加强的 。
也就是说子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。
2. 后置条件不能被削弱
后置条件即输出,假设我们的父类方法约定输出参数要大于0,调用父类方法的程序根据约定对输出参数进行了大于0的验证。而子类在实现的时候却输出了小于等于0的值。此时子类的涉及就违背了里氏替换原则
3. 不能违背对异常的约定
在父类中,某个函数约定,只会抛出ArgumentNullException 异常, 那子类的设计实现中只允许抛出ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。
[if !supportLists]4.2 [endif]子类方法不能违背父类方法定义的功能
验证子类设计是否符合里氏替换原则其实有一个小技巧,那就是你可以使用父类的单测来运行子类的代码,如果不可以正常运行,那么你就要考虑一下自己的设计是否合理了
[if !supportLists]4.3 [endif]子类必须完全实现父类的抽象方法
开发中,经常定义接口或抽象类,然后编码实现,调用类则直接传入接口或抽象类,其实这里已经使用了里氏替换原则。
[if !supportLists]4.4 [endif].子类可以有自己的个性
子类当然可以有自己的行为和外观了,也就是方法和属性,那这里为什么要再提呢?是因为里氏替换原则可以正着用,但是不能反过来用。在子类出现的地方,父类未必就可以胜任
[if !supportLists]5. [endif]里氏替换原则的作用
[if !supportLists]1. [endif]里氏替换原则是实现开闭原则的重要方式之一。
[if !supportLists]2. [endif]它克服了继承中重写父类造成的可复用性变差的缺点。
[if !supportLists]3. [endif]它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。
[if !supportLists]4. [endif]加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性,降低需求变更时引入的风险。
尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承
[if !supportLists]6. [endif]里氏替换原则的实现方法
里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
根据上述理解,对里氏替换原则的定义可以总结如下:
[if !supportLists]1. [endif]子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
[if !supportLists]2. [endif]子类中可以增加自己特有的方法
[if !supportLists]3. [endif]当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
[if !supportLists]4. [endif]当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等
通过重写父类的方法来完成新的功能写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。
如果程序违背了里氏替换原则,则继承类的对象在基类出现的地方会出现运行错误。这时其修正方法是:取消原来的继承关系,重新设计它们之间的关系。
关于里氏替换原则的例子,最有名的是“正方形不是长方形”。当然,生活中也有很多类似的例子,例如,企鹅、鸵鸟和几维鸟从生物学的角度来划分,它们属于鸟类;但从类的继承关系来看,由于它们不能继承“鸟”会飞的功能,所以它们不能定义成“鸟”的子类。同样,由于“气球鱼”不会游泳,所以不能定义成“鱼”的子类;“玩具炮”炸不了敌人,所以不能定义成“炮”的子类等。
[if !supportLists]7. [endif]总结:
面向对象的编程思想中提供了继承和多态是我们可以很好的实现代码的复用性和可扩展性,但继承并非没有缺点,因为继承的本身就是具有侵入性的,如果使用不当就会大大增加代码的耦合性,而降低代码的灵活性,增加我们的维护成本,然而在实际使用过程中却往往会出现滥用继承的现象,而里式替换原则可以很好的帮助我们在继承关系中进行父子类的设计。
[if !supportLists]七、 [endif]总结六大设计原则
1.单一职责原则:一个类或接口只承担一个职责。
2.里氏替换原则:在继承类时,务必重写(override)父类中所有的方法,尤其需要注意父类的protected方法(它们往往是让你重写的),子类尽量不要暴露自己的public方法供外界调用。
3.依赖倒置原则:高层模块不应该依赖于低层模块,而应该依赖于抽象。抽象不应依赖于细节,细节应依赖于抽象。
4.接口隔离原则:不要对外暴露没有实际意义的接口。
5.迪米特法则:尽量减少对象之间的交互,从而减小类之间的耦合。
6.开闭原则:对软件实体的改动,最好用扩展而非修改的方式。