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

设计模式(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"的区别;
② 永远不会出现需要将派生类换成另一个类的派生类的情况;
③ 派生类具有扩展基类的责任,而不是具有置换或者注销掉基类的责任;
④ 只有在分类学角度有意义时,才可以使用继承。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值