设计模式大纲

一、简述

以传统的二十三种设计模式为根基不断拓展新的设计模式,但是模式只是一种做事的方法,并不一定非要局限于一种,或者这二十三种。设计模式有很多,要学会活学活用。参考《设计模式之禅》与《Java设计模式》时其中有几种的叫法是有区别,但是含义完全一样:

设计模式之禅 Java设计模式
组合模式 合成模式
门面模式 外观模式
桥梁模式 桥接模式
中介者模式 调停者模式

根据看问题的角度不同,可以将传统的这二十三种设计模式分类如下:

  • 分类一
    • 创建型模式(五种):工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式
    • 结构型模式(七种):适配器模式、装饰器模式、代理模式、外观模式、桥接模式、组合模式、享元模式
    • 行为型模式(十一种):策略模式、模板方法模式、观察者模式、迭代子模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介模式、解释器模式
    • 其它:并发型模式和线程池模式
  • 分类二
    • 接口型模式(四种):适配器模式、外观模式、合成模式、桥接模式
    • 职责型模式(六中):单例模式、观察者模式、调停者模式、代理模式、职责链模式、享元模式
    • 构造型模式(五种):构建者模式、工厂方法模式、抽象工厂模式、原型模式、备忘录模式
    • 操作型模式(五种):模板方法模式、状态模式、策略模式、命令模式、解释器模式
    • 扩展型模式(三种):装饰器模式、迭代器模式、访问者模式

设计模式的经典名著——Design Patterns: Elements of Reusable Object-Oriented Software,中译本名为《设计模式——可复用面向对象软件的基础》的四位作者Erich Gamma、Richard Helm、Ralph Johnson,以及John Vlissides,这四人常被称为Gang of Four,即四人组,简称GoF。

二、详解

接下里将采用第二种分类来进行整理!

接口型模式

每个设计模式都旨在解决不同场景的问题。面向接口的模式适用于需要对一个类或一组类的方法进行定义或重定义的场景。例如,某个类实现了我们所需要的服务,但它的方法名称却与客户端的期望不符,这就需要运用适配器模式。

  • 适配器模式:适配器模式的意图在于,使用不同接口的类所提供的服务为客户端提供它所期望的几口。
  • 门面(外观)模式:外观模式的意图是为子系统提供一个接口,便于它的使用。
  • 组合(组合)模式:组合模式的意图是为了保证客户端调用单对象与组合对象的一致性。
  • 桥梁模式:桥接模式的意图是将抽象与抽象方法的实现相互分离来实现解耦,以便二者可以相互独立地变化。
如果你期望 可运用模式
适配类的接口以匹配客户端期待的接口 适配器模式
为一组类提供一个简单接口 外观模式
为单个对象与复合对象提供统一的接口 合成模式
解除抽象与实现之间的耦合,使得二者能够独立烟花 桥接模式

职责型模式

每种设计模式的意图是为了解决特定场景下的问题。如果需要违背通常的原则,尽可能早地分离职责,那么就是面向职责模式粉墨登场的时候了。

  • 单例模式:单例模式的意图是为了确保一个类有且仅有一个实例,并为他提供一个全局访问点。
  • 观察者模式:观察者模式的意图是在多个对象之间定义一对多的依赖关系,当一个对象的状态发生改变时,会通知依赖于它的对象,并根据新状态做出相应的反应。
  • 调停者(中介者)模式:调停者模式的意图是定义一个对象,封装一组对象的交互,从而降低对象间的耦合度,避免了对象间的显示引用,并且可以独立地改变对象的行为。
  • 代理模式:代理模式的意图是通过提供一个代理(Proxy)或者占位符来控制对该对象的访问。
  • 职责链模式:职责链模式的目的是通过给予多个对象处理请求的机会,以解除请求的发送者与接收者之间的耦合。
  • 享元模式:享元模式的意图是通过共享来有效地支持大量细粒度的对象。
如果你期望 可运用模式
将责任集中到某个类的单个实例中 单例模式
将对象从依赖于它的对象中解耦 观察者模式
将职责集中在某个类,该类可以监督其他对象的交互 调停者模式
让一个对象扮演其他对象的行为 代理模式
允许将请求传递给职责连的其他对象,知道这个请求被某个对象处理 职责链模式
将共享的、细粒度的对象职责集中管理 享元模式

构造型模式

每种设计模式的意图是为了解决某种场景下的问题。构造型模式的设计就是为了让客户类不通过类构造函数来创建对象。例如,当你需要逐步获取对象的初始值,则可能需要使用构建者模式。

  • 构建者模式:构建者模式的意图是将类的构建逻辑转移到类的实例化外部。
  • 工厂方法模式:工厂模式的意图是定义一个用于创建对象的接口,并控制返回哪个类的实例。
  • 抽象工厂模式:抽象工厂模式又名工具箱,其意图是允许创建一族相关或相互依赖的对象。
  • 原型模式:原型模式的意图是通过复制一个现有的对象来生成新的对象,而不是通过实例化的方法。
  • 备忘录模式:备忘录模式的意图是为对象状态提供存储和恢复功能。
如果你期望 可运用模式
在请求创建对象前,逐步收集创建对象需要的信息 构建者模式
决定推迟实例化类对象 工厂方法模式
创建一族具有某些共同特征的对象 抽象工厂模式
根据现有对象创建一个新的对象 原型模式
通过包含了对象内部状态的静态版本重新构建一个对象 备忘录模式

操作型模式

面向操作的模式适合于这类场景:设计中需要多个具有相同签名的方法。例如,模板方法模式允许子类重新实现及调整父类已经定义好的方法。

  • 模板方法模式:模板方法的意图是在一个方法里实现一个算法,并推迟定义算法中的某些步骤,从而让其他类重新定义它们。
  • 状态模式:状态模式的意图是将表示对象状态的逻辑分散到代表状态的不同类中。
  • 策略模式:策略模式的意图是将可互换的方法封装在各自独立的类中,并且让每个方法都实现一个公共的操作。
  • 命令模式:命令模式的意图是将请求封装在对象内部。
  • 解释器模式:解释器模式的意图是让你根据事先定义好的一系列组合规则,组合可执行对象。
如果你期望 可运用模式
在方法中实现算法,推迟对算法步骤的定义使得子类能够重新实现。 模板方法模式
在操作分散,使得每个类都能够表示不同的状态 状态模式
封装操作,使得实现是可以相互替换的 策略模式
用对象来封装方法调用 命令模式
将操作分散,使得每个实现能够运用到不同类型的集合中 解释器模式

扩展型模式

  • 装饰器模式:装饰器模式的意图是在运行时组合操作的新变化。
  • 迭代器模式:迭代器模式的意图是为顺序访问集合元素提供一种方式。
  • 访问者模式:访问者模式的意图是在不改变层次结构的前提下,对该层次结构进行扩展。
如果你期望 可运用模式
让开发者动态组合对象的行为 装饰器模式
提供一个方法来顺序访问集合中的元素 迭代器模式
允许开发者定义一个新的操作,而无须改变分层体系中的类 访问者模式

三、设计模式的六大原则

1. 开闭原则

开闭原则(Open Close Principle):一个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

低层模块指的是每一个逻辑实现的原子逻辑;高层模块指的是原子逻辑的在组装。

注意:开闭原则对扩展开放,对修改关闭,并不意味着不做任何修改,低层模块的变更,必然要有高层模块进行耦合,否则就是一个孤立无意义的代码片段。

2. 单一职责原则

单一职责原则(Single Responsibility Principle,SRP):应该有且仅有一个原因引起类的变更。

不要存在多于一个导致类变更的原因,也就是说每个类应该实现单一的职责,如若不然,就应该把类拆分。

优点:

  1. 类的复杂性降低实现什么职责都有清晰明确的定义;
  2. 可读性提高,复杂性降低,那当然可读性提高了;
  3. 可维护性提高,可读性提高,那当然更容易维护了;
  4. 变更引起的风险降低,变更时必不可少的,如果接口的单一职责做的好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大的帮助。

单一职责原则最难划分的就是职责。一个职责一个借口,但问题是“职责”没有一个量化的标准,一个类到底负责哪些职责?这些职责怎么细化?细化后是否都要有一个接口或类?
打电话例子:拨号 → 通话 → 挂机

这是很简单的,在此就不去编写这段代码的实现了,只是用UML来简述。

IPhone这样的接口是我们大家第一反应所可能设计的接口模型,完全没有问题,但是其针对单一职责原则来说,就不那么完美了。

IPhone这个接口包含了两个职责:协议管理与数据传递。简单来说:你使用移动还是联通,这与你通话内容有必然联系吗?

那么我们将这两个这则拆分成两个接口:

这是一个简洁清晰、职责分明的电话类图,实现类将两个职责融合在一个类中。

然而针对面向接口编程,其对外公布的是接口而不是实现类,因此类也应该是单一职责,所以我们需要使用组合模式,设计如下:

但是这种设计会引起类间耦合过重、类的数量增加等问题,人为的增加了设计的复杂性。

因此我们需要从实际的项目去考虑,从功能上来说,定义一个IPhone接口也没有错,实现了电话的功能而且设计还很简单。但是从“学究”理论上来分析,有两个变化的原因放到了一个接口中,这就为以后的变化带来了风险。

注意:单一职责原则提出了一个编写程序的标准,用“职责”和“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。

单一职责适用于接口、类和方法。对于单一职责原则,建议接口与方法一定要做到单一职责,类的设计尽量做到只有一个原因引起变化。

对于类来说,生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为的增加系统的复杂性。本来一个类可以实现的行为硬要拆成来那个累,然后在使用聚合或组合的方式耦合在一起,人为制造了系统的复杂性,这又是何必呢。所以灵活运用才是王道。

3. 里氏替换原则

里氏代换原则(Liskov Substitution Principle,LSP):所有引用基类的地方必须能透明地使用其子类的对象。(只要弗雷出现的地方,都可以用子类去替换,并且不会抛出异常。但是反过来是不可以的)

里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而基类与子类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

里氏替换原则中,子类对父类的方法尽量不要重写和重载。因为父类代表了定义好的结构,通过这个规范的接口与外界交互,子类不应该随便破坏它。

定义中的四层含义:

  1. 子类必须完全实现父类的方法
    系统设计中,我们经常定义一个接口或者抽象类,然后编码实现,调用类则直接传入接口或抽象类,这就是里氏替换原则。
    注意:
    1. 在类中调用其他类时务必使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
    2. 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生了“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合关系替代继承。
  2. 子类可以有自己的个性
    子类是可以有自己的特有的方法或属性,这也是为什么里氏替换原则可以正着用,但不可以反着用的原因。
  3. 覆盖或实现父类的方法时输入参数可以被放大
    从字面上来理解就是将父类方法中的参数往大了扩充,但是参数改变了,这就不是覆盖了,而是重载了。

    import java.util.*;
    public class Client{
        public static void main(String[] args){
            Father f = new Father();
            HashMap fmap = new HashMap();
            f.doSomething(fmap);
            //根据里氏替换原则,父类出现的地方子类就可以出现
            System.out.println("------------分割------------");
            Son s = new Son();
            HashMap smap = new HashMap();
            s.doSomething(smap);
        }
    }
    
    /*父类的参数范围比子类小*/
    class Father{
        //将HashMap转换为Collection集合类型
        public Collection doSomething(HashMap map){
            System.out.println("父类被执行了。。。。");
            return map.values();
        }
    }
    
    class Son extends Father{
        //放大输入参数类型
        public Collection doSomething(Map map){
            System.out.println("子类被执行了。。。。");
            return map.values();
        }
    }
    /*
    输出结果:
    父类被执行了。。。。
    ------------分割------------
    父类被执行了。。。。
    */

    转换一下:

    import java.util.*;
    public class Client{
        public static void main(String[] args){
            Father f = new Father();
            HashMap fmap = new HashMap();
            f.doSomething(fmap);
            //根据里氏替换原则,父类出现的地方子类就可以出现
            System.out.println("------------分割------------");
            Son s = new Son();
            HashMap smap = new HashMap();
            s.doSomething(smap);
        }
    }
    
    /*父类的参数范围比子类大*/
    class Father{
        //将HashMap转换为Collection集合类型
        public Collection doSomething(Map map){
            System.out.println("父类被执行了。。。。");
            return map.values();
        }
    }
    
    class Son extends Father{
        //放大输入参数类型
        public Collection doSomething(HashMap map){
            System.out.println("子类被执行了。。。。");
            return map.values();
        }
    } 
    /*
    输出结果:
    父类被执行了。。。。
    ------------分割------------
    子类被执行了。。。。
    */

    很诧异啊?子类在没有覆写父类的方法的前提下,子类方法被执行了,这回引起业务逻辑混乱,因为在实际应用中父类一般都是抽象类,子类是实现类,你传递一个这样的实现类就会“歪曲”了父类的意图,引起一堆意想不到的业务逻辑混乱,所以子类中方法的前置条件必须与超类中被复写的前置条件相同或者更宽松。

  4. 覆写或实现父类的方法时输入结果可以被缩小
    父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么里氏替换原则就要求S必须小于等于T,也就是说,要么S和T是同一个类型,要么S是T的子类。因为返回值类型可以向上转型,但是不能向下转型。
    在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了,把子类当做父类用,子类的“个性”被抹杀;把子类单独作为一个业务来使用,则会让代码间的耦合关系变得扑朔迷离——缺乏类替换的标准。

4. 依赖倒置原则

依赖倒置 (Dependence Inversion Principle,DIP):高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。

低层模块指的是每一个逻辑实现的原子逻辑;高层模块指的是原子逻辑的在组装。

在Java语言中抽象就是指接口或抽象类,两者都是不能直接被实例化的;实现接口或抽象类而产生的类就是细节,也就是通过关键字new产生的一个对象。

依赖倒置是相对于“正置”这个概念的,“正置”指的是需要的时候在进行关联,例如我要开奔驰车就依赖奔驰车等等。“倒置”是指将现实事务抽象出来,通过抽象将业务逻辑实现,针对这个业务逻辑来进行实现,好处在于具体实现的改变完全不影响业务逻辑,而且规避了一些非技术因素引起的问题,同时业务逻辑的契约的存在,使得我们完全不必去按照顺序开发,而是可以并行的开发各个模块。

通过采用依赖倒置设计的接口或抽象类对实现类进行了约束,可以减少需求变化引起的工作量剧增的情况。

依赖倒置原则在Java语言中的表现就是:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
  • 接口或抽象类不依赖于实现类;
  • 实现类依赖接口或抽象类;

依赖倒置原则的有点:

  1. 减少类之间的耦合性
  2. 提高系统的稳定性
  3. 降低并行开发引起的风险
  4. 提高代码的可读性和可维护性

测试驱动开发(Test-Driven Development,TDD)是依赖倒置原则的最高级应用。

依赖三种写法:

  • 构造函数传递依赖对象
    在类中通过构造函数声明依赖对象,按照依赖注入的说法,这种方式叫做构造函数注入。
  • 方法传递依赖对象
    在抽象中设置Setter方法声明依赖关系,依照依赖注入的说法,这是Setter依赖注入。(与上面的类同,只不过不是在构造函数中,而是通过方法的形式)
  • 接口声明依赖对象
    简单来说就是在接口中定义时,参数使用的是另外的接口,这也叫做接口注入。

依赖倒置原则的本质是通过抽象(接口或抽象类)使各个类或者模块的实现彼此分离,不互相影响,实现模块间的松耦合。

在实际开发中,想要做到这一点,则需要做到以下几点:

  • 每个类尽量都有接口或抽象类,或者抽象类和接口两者都具备
  • 变量的表面类型尽量是接口或者是抽象类
  • 任何类都不应该从具体类派生
  • 尽量不要覆写基类的方法
  • 结合里氏替换原则使用(接口负责定义public属性和方法,并且声明与其他对象的依赖关系,抽象类负责公共构造部分的实现,实现类准确的实现业务逻辑,同时在适当的时候对父类进行细化)

依赖倒置原则是6个设计原则中最难以实现的原则,它是实现开闭原则的重要途径,依赖倒置原则没有实现,就别想实现对扩展开发,对修改关闭。在项目中,我们需要注意的是“面向接口编程”就是以依赖倒置原则为核心的。

5. 接口隔离原则

接口隔离原则( Interface Segregation Principle):类之间的依赖关系应该建立在最小的接口上。(类不应该依赖它不需要的接口)

重点:接口尽量细化,同时接口中的方法尽量少。

注意:接口隔离原则与单一职责原则是不同的,他们审视角度是不同的,单一职责原则要求的是类和接口职责单一,注重的是职责,这是业务逻辑上的划分,而接口隔离原则要求接口的方法尽量少。

将过于臃肿的接口变更为多个独立的接口所依赖的原则就是接口隔离原则。

接口隔离原则是对接口进行规范约束,其包含4层含义:

  1. 接口要尽量小(但是不能违反单一职责原则)
  2. 接口要高内聚
  3. 定制服务
  4. 接口设计是有限度的

每个接口中不存在子类用不到却必须实现的方法,如果不然,就要将接口拆分。使用多个隔离的接口,比使用单个接口(多个接口方法集合到一个的接口)要好。

6. 迪米特法则

迪米特法则(Low Of Demeter,LOD)也称为最少知道原则(Least Knowledge Principle):一个对象应该对其他对象有最少的了解。(一个类对自己需要耦合或调用的类知道得最少)

迪米特法则的形象的解释是:只与直接的朋友通信。

成员朋友类:出现在成员变量、方法的输入输出参数中的类,而出现在方法体内部的类不属于朋友类。

一个类公开的方法或属性越多,修改时涉及的面也就越大,变更引起的风险扩散也越广。所以尽量减少public的使用。

如果一个方法放在本类中,既不增加类间的关系,对本类不产生负面影响,就放置在本类中。

7.合成复用原则

合成复用原则又称为组合/聚合复用原( Composite Reuse Principle)原则:尽量首先使用合成/聚合的方式,而不是使用继承。

参考资料:

赞赏

猜你喜欢

转载自blog.csdn.net/fanxiaobin577328725/article/details/81805071