您的位置:首页 > 编程语言 > Java开发

java的23种设计模式法则(权威版)

2014-09-30 10:30 85 查看
设计模式(Design Patterns)具体设计模式讲解后序陆续发布,敬请关注我的博客!

——可复用面向对象软件的基础

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。 毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。本章系Java之美[从菜鸟到高手演变]系列之设计模式,我们会以理论与实践相结合的方式来进行本章的学习,希望广大程序爱好者,学好设计模式,做一个优秀的软件工程师!

一、设计模式的分类

总体来说设计模式分为三大类:

创建型模式,共五种:工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式。

结构型模式,共七种:适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式。

行为型模式,共十一种:策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式、解释器模式。

其实还有两类:并发型模式和线程池模式。用一个图片来整体描述一下:

二、设计模式的六大原则

1、开闭原则(Open Close Principle)


开闭原则就是说对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。所以一句话概括就是:为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,我们需要使用接口和抽象类,后面的具体设计中我们会提到这点。

2、里氏代换原则(Liskov Substitution Principle)

里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

☆ 里氏替换原则由Barbara Liskov提出,它的严格表达是,如果对每一个类型为T1的对象o1,都由类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。换句话说,一个软件实体如果使用的是一个积累大话,那么一定适用于其子类,而且它根本不能察觉出基类对象和子类对象的区别。

☆ 里氏替换原则的反命题是不成立的。

ξ 7.3 里氏替换原则在设计模式中的体现

☆ 策略模式、合成模式和代理模式是对该原则的最好诠释。

ξ 7.4 从代码重构的角度理解

☆ 对违反里氏替换原则的设计的重构

① 创建一个新的抽象类,作为两个具体类的基类,将两个具体类的共同行为移动到抽象类中;

② 将两个具体类的继承关系改写为委派关系。

关键知识点:

☆ 里氏替换原则的概念,基类可以出现的地方,派生类同样可以出现;

☆ 里氏替换原则的反命题不成立;

☆ 对于A、B两个类,B由A派生,如果这种继承违反里氏替换原则,可以采用如下方法进行重构:将A、B的共同行为抽象出来,建立一个抽象类C,A和B都是C的派生类,如下图所示:

3、依赖倒转原则(Dependence Inversion Principle)

这个是开闭原则的基础,具体内容:针对接口编程,依赖于抽象而不依赖于具体。

☆ 依赖倒转原则,要依赖于抽象,而不要依赖于具体实现。

ξ 8.3 依赖倒转原则

☆ 三种耦合关系

① 零耦合关系,如果两个类没有耦合关系,就称之为零耦合;

② 具体耦合,具体耦合发生在两个具体的类之间,经由一个类对另外一个具体类的直接引用造成的。

③ 抽象耦合关系,抽象耦合关系发生在一个具体类和一个抽象类之间,使用两个必须发生关系的类之间存在有最大的灵活性。

☆ 依赖倒转原则的另外一种表述是:

要针对接口编程,不要针对实现编程(Program to an interface, not an implementation)[GOF95]。同样,在处理类之间的耦合关系时,尽量使用抽象耦合的形式。

下图中左侧A和B为具体依赖关系,重构后如右图所示,变为抽象依赖:

☆ 里氏替换原则是依赖倒转原则的基础。

☆ 工厂模式、模板模式、迭代子模式都是对依赖倒转原则的体现。

ξ 8.5 java对抽象类型的支持

☆ 在设计中,常用的抽象方式为:先抽象出逻辑上固定的东西,定义出接口,再定义一个抽象类实现该接口,作为该接口默认实现的抽象类应该实现大部分公共的代码,具体的类通常由该抽象类派生。

下图以java下的容器举例说明:

4、接口隔离原则(Interface Segregation Principle)

这个原则的意思是:使用多个隔离的接口,比使用单个接口要好。还是一个降低类之间的耦合度的意思,从这儿我们看出,其实设计模式就是一个软件的设计思想,从大型软件架构出发,为了升级和维护方便。所以上文中多次出现:降低依赖,降低耦合。

☆ 接口隔离原则,使用多个专门的接口比使用单一的总接口要好。换句话说,从一个客户角度讲:一个类对另外一个类的依赖应当是建立在最小接口上的。

ξ 9.1 什么是接口隔离原则

☆ 接口隔离原则讲的是为同一个角色提供宽、窄不同的接口,以应对不同客户端的需求,下例以set为例讲解:

在该UML图中,TreeSet是一种使用树状数据结构的可排序的Set容器,它既实现了Set接口(通过继承AbstractSet),又实现了SortedSet接口。这里并没有提供一个总的既有排序功能又有Set功能的总接口,而是针对不同的需求,将两种角色分别定义成两种接口,这样的设计,是符合接口隔离原则。

☆ 接口污染

将不同角色的接口合并为一个臃肿的接口就是对接口的污染。这种做法同时违反了可变性封装原则,它将不同的可变性封装到了同一个软件实体中。

☆ 对接口隔离原则的具体应用可以参考备忘录模式和迭代子模式。

5、迪米特法则(最少知道原则)(Demeter Principle)

为什么叫最少知道原则,就是说:一个实体应当尽量少的与其他实体之间发生相互作用,使得系统功能模块相对独立。

☆ 迪米特法则,又叫最少知识原则,就是说,一个对象应当对其他对象有尽可能少的了解。

ξ 11.1 迪米特法则的各种表述

① 只与你直接的朋友们通信;

② 不要跟“陌生人”说话;

③ 每一个软件单位对其他的单位都只有最少的知识,而且局限于那些与本单位密切相关的软件单位。

ξ 11.2 狭义的迪米特法则

☆ 如果两个类不必彼此直接通信,那么这两个类就不应当发生直接的相互作用。如果其中的一个类需要调用另外一个类的某一个方法,可以通过第三者转发这个调用。

参考下例,Someone、Friend和Stranger三个类。

Someone类有一个方法接受一个Friend类型的变量:

public class Someone

{

public void operation1( Friend friend )

{

Stranger stranger = friend.provide() ;

stranger.operation3() ;

}

}

所以Someone和Friend是朋友类(直接通讯的类)。

同理,Friend类持有一个Stranger类的私有对象,他们是朋友类:

public class Friend

{

private Stranger stranger = new Stranger() ;

public void operation2(){}

public Stranger provide()

{

return stranger ;

}

}

在这里,Someone类和Stranger类不是朋友类,但Someone类却通过Friend类知道了Stranger类的存在,这显然违反迪米特法则。

现在,我们对Someone和Friend类进行重构。首先在Friend类里添加一个方法,封装对Stranger类的操作:

public class Friend

{

private Stranger stranger = new Stranger() ;

public void operation2(){}

public Stranger provide()

{

return stranger ;

}

public void forward()

{

stranger.operation3() ;

}

}

然后,我们重构Someone的operation1方法,让其调用新提供的forward方法:

public class Someone

{

public void operation1( Friend friend )

{

friend.forward() ;

}

}

现在Someone对Stranger的依赖完全通过Friend隔离,这样的结构已经符合狭义迪米特法则了。

仔细观察上述结构,会发现狭义迪米特法则一个明显的缺点:会在系统里造出大量的小方法,散落在系统的各个角落。这些方法仅仅是传递间接的调用,因此与系统的商务逻辑无关,当设计师试图从一张类图看出总体的框架时,这些小的方法会造成迷惑和困扰。遵循迪米特法则会使一个系统的局部设计简化,因为每一个局部都不会和远距离的对象有直接关联。但是,这也会造成系统的不同模块之间的通信效率降低,也会使系统的不同模块之间不容易协调。

结合依赖倒转原则,我们对代码进行如下重构来解决这个问题,首先添加一个抽象的Stranger类,使Someone依赖于抽象的“Stranger”角色,而不是具体实现:

public abstract class AbstractStranger

{

abstract void operation3() ;

}

然后,让Stranger从该类继承:

public class Stranger extends AbstractStranger

{

public void operation3() {}

}

随后,我们重构Someone使其依赖抽象的Stranger角色:

public class Someone

{

public void operation1( Friend friend )

{

AbstractStranger stranger = friend.provide() ;

stranger.operation3() ;

}

}

最后,我们重构Friend的provide方法,使其返回抽象角色:

public class Friend

{

private Stranger stranger = new Stranger() ;

public void operation2(){}

public AbstractStranger provide()

{

return stranger ;

}

}

现在,AbstractStranger成为Someone的朋友类,而Friend类可以随时替换掉AbstractStranger的实现类,Someone不再需要了解Stranger的内部实现细节。下图是重构后的UML类图:

ξ 11.3 迪米特法则与设计模式

对迪米特法则的最好描述,可以参考门面模式和调停者模式。

ξ 11.4 广义迪米特法则

☆ 在将迪米特法则运用到系统的设计中时,应注意的几点:

① 在类的划分上,应该创建有弱耦合的类;

② 在类的结构设计上,每一个类都应当尽量降低成员的访问权限;

③ 在类的设计上,只要有可能,一个类应当设计成不变类;

④ 在对其他类的引用上,一个对象对其它对象的引用应当降到最低;

⑤ 尽量降低类的访问权限;

⑥ 谨慎使用序列化功能;

⑦ 不要暴露类成员,而应该提供相应的访问器(属性)。

6、合成复用原则(Composite Reuse Principle)

原则是尽量使用合成/聚合的方式,而不是使用继承。

☆ 合成/聚合复用原则经常又叫做合成复用原则。该原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分:新的对象通过向这些对象的委派达到复用已有功能的目的。

ξ 10.1 合成与聚合的区别

合成和聚合均是关联的特殊情况。聚合用来表示“拥有”关系或者整体与部分的关系;而合成则用来表示一种强得多的“拥有”关系。在一个合成关系里面,部分和整体的生命周期是一样的。一个合成的新的对象完全拥有对其组成部分的支配权,包括它们的创建和销毁等。使用程序语言的术语来说,组合而成的新对象对组成部分的内存分配、内存释放有绝对的责任。

ξ 10.2 复用的基本种类

☆ 合成/聚合复用

① 优点:

? 新对象存取成分对象的唯一方法是通过成分对象的接口;

? 这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的;

? 这种复用支持包装;

? 这种复用所需的依赖较少;

? 每一个新的类可以将焦点集中在一个任务上;

? 这种复用可以在运行时动态进行,新对象可以使用合成/聚合关系将新的责任委派到合适的对象。

② 缺点:

? 通过这种方式复用建造的系统会有较多的对象需要管理。

☆ 继承复用

① 优点:

新的实现较为容易,因为基类的大部分功能可以通过继承关系自动进入派生类;

修改或扩展继承而来的实现较为容易。

② 缺点:

继承复用破坏包装,因为继承将基类的实现细节暴露给派生类,这种复用也称为白箱复用;

如果基类的实现发生改变,那么派生类的实现也不得不发生改变;

从基类继承而来的实现是静态的,不可能在运行时发生改变,不够灵活。

ξ 10.3 对违反里氏替换原则的另外一种重构方案

下图左面的UML是违反里氏替换原则的。雇员、经理和学生从人派生,而实际上这三种派生类应该描述一种角色,一个人可能同时是经理和学生,也可能同时是雇员和经理,这个时候通过继承来实现复用显然是不合适的。右图通过重构抽象出角色接口,雇员、经理和学生都是角色接口的实现,然后采用合成/聚合的方式进行复用:

☆ Coad条件是判断是否使用继承复用的通俗描述,只有在所有条件满足时,才应该考虑使用继承,它的内容是:

① 派生类是基类的一个特殊种类,而不是基类的一个角色,即要分清"Has-A"和"Is-A"的区别;

② 永远不会出现需要将派生类换成另一个类的派生类的情况;

③ 派生类具有扩展基类的责任,而不是具有置换或者注销掉基类的责任;

④ 只有在分类学角度有意义时,才可以使用继承。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: