设计模式概述
软件设计模式的产生背景
“设计模式”最初并不是出现在软件设计中,而是被用于建筑领域的设计中。
1997年,美国著名建筑大师、加利福尼亚大学伯克利分校环境结构中心主任克里斯托夫·亚历山大(Christopher Alexander)在他的著作《建筑模式语言:城镇、建筑、构造(A Pattern Language: Towns Building Construction)中描述了一些常见的建筑设计问题,并提出了 253 种关于对城镇、邻里、住宅、花园和房间等进行设计的基本模式。
直到 1990 年,软件工程界才开始研讨设计模式的话题,后来召开了多次关于设计模式的研讨会。
1995 年,艾瑞克·伽马(ErichGamma)、理査德·海尔姆(Richard Helm)、拉尔夫·约翰森(Ralph Johnson)、约翰·威利斯迪斯(John Vlissides)等 4 位作者合作出版了《设计模式:可复用面向对象软件的基础》(Design Patterns: Elements of Reusable Object-Oriented Software)一书,在本教程中收录了 23 个设计模式,这是设计模式领域里程碑的事件。
GoF反复向你强调一个宗旨:要让你的程序尽可能的可重用。
什么是设计模式
设计模式(Design Pattern)是一种模式,是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案,它是思想的体现,而非具体的实现。
这 23 种设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解(即包含了面向对象的精髓)。
我们为什么要学习设计模式
事实上,我们可能很多情况下都是在不了解设计模式或者了解的不多的情况下作为程序员工作了多年。也在不经意间实现了一些设计模式。
那么我们为什么还要学习设计模式?
一方面,设计模式教我们如何使用面向对象设计的原则解决各种问题;
另一方面,设计模式定义了一种通用语言(跟框架一样,统一一个标准),让大家在做系统设计的时候,能更有效的交流。
我们学习设计模块的核心思想是解耦合,并不是消除耦合,而是把耦合控制在一定范围,保证这个范围的整洁。
使用设计模式的步骤可以总结成两句话:
1)设计模式中提到一句很精髓的话:找到稳定点和变化点,运用抽象,把变化点隔离起来。
2)先满足设计原则,再迭代出设计模式。
接口与抽象类
在学习设计模式前,我们先来复习一下什么是接口、什么是抽象类。
在面向对象的设计领域里,有时候会用is-a、has-a、like-a来描述他们之间的关系。例如:A is-a B,代表B是A的父类;A has-a B,那么B就是A的组成部分。A like-a B,则代表B就是A的接口。
设计模式并非只针对于java,而是所有面向对象语言的设计模式,在接下来的设计模式讲解中,我们以java为例。为什么选择java?java作为一门集大成的语言,博纳众多语言之所长,是最优秀的面向对象语言之一。
什么是接口
接口通常代表一种承诺或者规范,是对象必须遵守的承诺,即使实现类发生再大的变化,也能保证所有的实现类都有相关的方法可供调用;也就是说,实现类有责任去编写实现我接口中的方法,即使是一个空方法。
java将接口的概念升为独立的结构,体现了接口与实现的分离。
什么是抽象类
在面向对象的概念中,我们所有的对象都是通过类来描述的。而反过来确不是这样的,并不是所以的类都是用来描绘对象的,如果一个类中没有足够的信息来描绘一个具体的对象,那么这样的类就是抽象的。
抽象类除了不能实例化对象之外,类的其它功能依然存在。抽象类更多的是对通用的、基础的方法封装,让子类复用,避免在子类开发重复的代码。子类只需实现抽象方法,也可以有选择的覆盖抽象父类的方法。
接口与抽象类的区别
一个类可以实现多个接口,但是只能继承一个抽象类;
接口都是抽象方法,而抽象类中既可以有抽象方法,也可以有实例(具体)方法;
接口中的变量都是public static final;而抽象类中的变量可以被任何通用修饰符修饰;
接口的方法都是public;抽象类中的方法可以是public、protected、private或者默认的package;
接口不能定义构造函数,但抽象类可以。
设计模式简介
GoF中一共收录了23个设计模式,每个设计模式都旨在解决不同场景的问题。
设计模式分类
所有模式都可以按其意图或目的进行分类。
按意图划分
按目的划分
设计模式按目的来划分可以分为三大类,分为创建型模式、结构型模式和行为型模式 3 种。
如果还需要分的更细,根据模式是主要用于类上还是主要用于对象上来分,又可分为类模式和对象模式两种。
单例模式Singleton
单例模式:是一种创建型设计模式,一个类只有一个实例,同时提供对该实例的全局访问点。
单例模式的应用场景:
1)需要频繁实例化或被共享的场合。比如:日志记录、缓存和线程池;
2)控制硬件级别的操作。比如:驱动程序对象。
3)单例模式也可以用于其他设计模式:比如抽象工厂模式、建造者模式、原型模式即门面模式都可以作为单例实现。
原型模式Prototype
原型设计模式是创建模式的一种,因此它提供了一种对象创建机制。它允许用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型相同或相似的新对象。
原型模式的应用场景:
1)创建对象成本比较大(比如:初始化时间比较长,占用 太多的CPU资源等),新的对象可以通过原型模式对已有对象进行赋值来获取,如果相似对象,则可以对其成员变量稍作修改即可。
2)系统想保存对象的状态,而对象的状态变化很小,或者对象占用内存较少的,可以使用原型模式配合备忘录模式来实现。
3)逃避构造函数的约束。
建造者模式Builder
建造者模式是一种创建型模式,可让您逐步构建复杂的对象。将一个复杂的对象的构建与其表示分离,使得同样的构建过程可以创建不同的表示。建造者(Builder)模式由抽象建造者、具体建造者、产品、导向器等 4 个要素构成。
应用场景:
相同的方法,不同的执行顺序,产生不同的结果。
产品类非常复杂,或者产品类中不同的调用顺序产生不同的作用。
初始化一个对象特别复杂,参数多,而且很多参数都具有默认值。
适配器模式Adapter
适配器模式,也称为包装器模式,是一种结构型设计模式,它允许具有不兼容接口的对象进行协作。适配器模式分为类适配器模式、对象适配器模式两种。
技术源于生活,也服务于生活。现实生活中就有很多这样的例子,比如:用直流电的笔记本电脑接交流电源时需要一个电源适配器,安卓耳机接苹果手机时需要一个转接头等等,都是适配器的应用。
适用场景:
1)以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
2)使用第三方提供的组件,但组件接口定义和系统要求的接口定义不同。
工厂模式Factory
①简单工厂,是工厂方法的一种特例,简单工厂模式也叫静态工厂模式,就是工厂类(一般使用静态方法)通过接收的参数来区分并返回不同的对象实例。
简单工厂的弊端:每增加一个产品就要增加一个具体产品类和修改工厂类,这增加了系统的复杂度,违背了“开闭原则”。
总之,简单工厂就是一个工厂接收不同参数返回不同对象实体。
②工厂方法模式,虽然简单工厂模式解决了调用者和创建者之间的耦合,但是工厂和创建者之间依然存在着耦合。这样的设计违背了我们的“开闭原则”,未来新产品的扩展并不灵活。而“工厂方法模式”是对简单工厂模式的进一步抽象化,其好处是可以使系统在不修改原来代码(比如工厂类)的情况下引进新的产品,即满足开闭原则。我们的工厂方法模式就是做这个的。
总时,工厂方法就是针对一个对象提供一个工厂类,不同工厂类创建不同的产品实例。
③抽象工厂模式,是一种为访问类提供一个创建一组相关或相互依赖对象的接口,且访问类无须指定所要产品的具体类就能得到同族的不同等级的产品的模式结构。
简单的说:就是用来生产不同产品族的全部产品的,抽象工厂模式是工厂方法模式的升级版本,在有多个业务品种、业务分类时,通过抽象工厂模式产生需要的对象是一种非常好的解决方式。
总之,抽象工厂就是对于一个产品族都有一个工厂类,但产品族内增加其他新产品需要修改代码。
代理模式Proxy
代理模式是一种结构型设计模式,可以让你为另一个对象提供替代或占位符。代理控制对原始对象的访问,允许您在请求到达原始对象之前或之后执行某些操作。
当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。
应用场景:
1)安全代理:屏蔽对真实角色的直接访问;
2)远程代理:通过代理类处理远程方法调用(RMI);
3)延迟加载:先加载轻量级的代理对象,真正需要时再加载真实对象。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。
4)记录请求:想要保留对服务对象的请求历史记录的时候可以使用。
桥接模式Bridge
桥接模式是一种结构型设计模式,它允许您将一个大类或一组密切相关的类拆分为两个独立的层次结构——抽象和实现——使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
其中一个层次结构(通常称为抽象)将获得对第二层次结构(实现)的对象的引用。
应用场景:
1)当一个类存在两个独立变化的维度,且这两个维度都需要进行扩展时,可以使用桥接模式可以解耦这些变化的维度;
2)当一个系统需要在构件的抽象化角色和具体化角色之间增加更多的灵活性时,可以使用桥接模式。
组合模式Composite
组合模式:是一种结构型设计模式,有时也叫合成模式。它允许您将对象组合成树结构,用来表示“整体-部分”的关系,从而使客户端可以使用统一的方式处理部分对象和整体对象(这些对象具体一致的访问性)。
什么意思呢?就是当我们的应用程序的核心模型可以表示为树时,用组合模式才有意义。比如,大多数国家的军队都是按等级划分的,一支军队由几个师组成、一个师是一组旅、…一个连由排组成,排可以分解为班;最后,一个班是一小群真正的士兵,命令在层次结构的顶部下达,并传递到每个级别,直到每个士兵都知道需要做什么。
组合模式的意图在于:为了保证客户端用单个对象与组合对象的一致性。
组合模式的核心角色:
抽象构件(Component)角色:定义了叶子构件和容器构件的公共特点,即声明公共接口。
叶子构件(Leaf)角色:是组合中的叶节点对象,它没有子节点,用于继承或实现抽象构件。
容器构件(Composite)角色:有容器的特征,可以包含子节点,主要作用是存储和管理子部件,通常包含 add()、remove()、getChild() 等方法。
在组合模式中,整个树形结构中的对象都属于同一种类型,最大好处是您不需要关心构成树的对象的具体类(不需要辨别是容器构件(分支节点)还是叶子节点)对象本身会将请求向下传递到树中,给用户的使用带来极大的便利。
应用场景:
一般涉及到数据结构方面的内容且符合树型结构的首先可以想到组合模式。
当您需要实现树状结构时,请使用组合模式。比如:操作系统资源管理器、OA系统的组织结构、XML文件解析等;
外观模式Facade
外观(Facade)模式又叫作门面模式,是一种通过为多个复杂的子系统提供统一的入口,封装子系统的复杂性,便于客户端调用。
外观(Facade)模式是“迪米特法则”的典型应用,在日常编码工作中,我们都在有意无意的大量使用外观模式。
只要是高层模块需要调度多个子系统(这里的子系统可以是一个完整的系统,也可以是模块或者更细粒度的类的对象),我们都会自觉地创建一个新的类封装这些子系统,提供精简的接口(只暴露有限的必要接口),让高层模块可以更加容易地间接调用这些子系统的功能,说白了就是封装系统的底层实现,隐藏系统的复杂性,提供一组更加简单易用、更高层的接口。比如:我们平时写的一些Utils类(DButils)可以理解为Facade;Linux 系统调用函数,它封装了底层更基础的 Linux 内核调用。
外观模式更像是客户端应用程序的助手,其本质就是整合接口,封装低层实现细节,为客户端提供一个更简洁的接口,实现了子系统与客户端间的松耦合关系。
应用场景:
1)当您需要一个有限但直接的接口来连接复杂的子系统时,可以使用外观模式;
2)当客户端与多个子系统之间存在很大的联系时,引入外观模式可将它们分离,从而提高子系统的独立性和可移植性。
3)防止客户端将对象转换为更底层的真实对象,隐藏底层不必要暴露给客户端的一些方法或属性。
装饰器模式Decorater
装饰器(Decorator)模式一种结构型设计模式,是一种用于代替继承的技术,指在不改变现有对象结构的情况下,动态地给当前对象添加一些额外的功能。
装饰器模式的意图在于:运行时修改对象的功能,比生成子类更加灵活。
装饰器提供运行时修改能力,因此更加灵活。当可供选择的数量越多时,使用装饰器模式会更加灵活。
应用场景:
1)当您需要能够在运行时为对象分配额外的行为而不破坏使用这些对象的代码时,请使用装饰器模式。
2)当需要给一个现有类添加附加职责,而又不能采用生成子类的方法进行扩充时(生成子类会产生大量子类),请使用装饰器模式。
3)对现有的一组基本功能进行排列组合而产生非常多的功能时,采用继承关系很难实现,而采用装饰器模式却很好实现时。
享元模式Flyweight
享元模式:“享元”,顾名思义就是被共享的单元,是一种结构型设计模式。以共享的方式高效的支持大量细粒度的对象的重用。
享元模式的定义提出了两个要求,细粒度和共享对象。因为要求细粒度,所以不可避免地会使对象数量多且性质相近,此时我们就将这些对象的信息分为两个部分:内部状态和外部状态。
内部状态:可以共享,不会随环境变化而改变。
外部状态:不可以共享,会随环境变化而改变。
比如,连接池中的连接对象,保存在连接对象中的用户名、密码、URL等信息,在创建对象的时候就设置好了,不会随环境的改变而改变,这些为内部状态,而不需要把这些数据保留在每个对象中。而当每个连接要被回收利用时,我们需要将它标记为不可用状态,这些为外部状态。
享元模式的意图在于:通过共享来缓存对象,降低内存消耗,前提是享元的对象是不可变对象。
享元模式可以使你共享地访问那些大量出现的细粒度的对象,有人会觉得这不是和单例模式很像吗?只不过是享元模式有多个对象共享。但是他们的设计意图两个不同的出发点,享元模式是为了对象复用,节省内存;而单例模式则是为了限制对象的个数,享元对象不可变,而单例对象是可变的。
应用场景:
1)应用程序需要产生大量类似的对象,包含可以在多个对象之间提取和共享的重复状态时可以使用享元模式。
命令模式Command
命令模式是一种行为设计模式,它将请求转换为包含有关请求信息的独立对象,将请求和与执行请求的职责分离,方便对请求进行储存、传递、调用的管理,使其可以对请求进行排队、存储请求历史记录或撤销请求等操作。
命令模式可以存储请求对象信息,这些对象可以作为方法参数传递、延迟或排队去执行,所以也可以定位到之前的操作。举个例子:
去餐馆吃饭,顾客找服务员点菜,每一个顾客的点菜都是一个请求,服务员将点菜信息传递给厨师,服务员并不关心菜如何做、谁来做,当顾客一多,就会出现排队的现象,这时候服务员(顾客点菜)和厨师(做菜)各司其职,井井有条才能控制整体的效率。
命令模式的意图在于:将一个请求封装为一个对象,将请求和与执行请求的职责分离,方便对请求进行储存、传递、调用的管理。
使用命令模式,可以将任何操作转换为对象。这种转换使得我们可以推迟操作的执行、对其进行排队、存储命令的历史记录、将命令发送到远程服务等。
应用场景:
1)当你想要将操作参数化对象时,可以使用命令模式将特定的方法调用变成一个独立的对象;
2)当你想要实现可逆操作时,可以使用命令模式。比如:数据库的事务机制就是命令模式的应用;
3)当系统要执行一组操作时,命令模式可以定义宏命令来实现该功能。
责任链模式ChainOfResponsibility
责任链模式是一种行为设计模式,为了避免发送者与多个请求处理者耦合在一起,将能够处理同一类请求的对象连成一条链,所提交的请求沿着处理程序链传递,如果能处理则处理,如果不能处理则传递给链上的下一个对象。
责任链模式的意图在于:将特定行为转换为处理程序的独立对象,以解除请求的发送者与接受者之间的耦合。也就是说请求只需要发送到责任链上,无须关心请求的处理细节和请求的传递过程,请求会自动处理。
在运用责任链模式时,客户端不必事先知道对象集合中的哪个对象可以提供自己需要的服务。当客户端发出请求后,该请求会沿着职责链转发请求,直到找到能够提供该服务的对象为止。这就实现了请求者与执行者间的解耦。
应用场景:
1)如果希望以各种方式处理不同类型的请求,并且不知道请求的确切类型时,可以使用责任链模式;
2)当以特定顺序执行多个处理程序时,可以使用 模式;(由于你可以按某种顺序链组合,所有的请求都会按你指定的计划执行);
3)当你的应用程序可能需要动态指定一组对象处理请求时,或者希望添加新的处理者时,可以使用责任链模式;
状态模式State
状态模式是一种行为设计模式,它允许对象在其内部状态发生变化时改变其行为,即把复杂的“判断逻辑”转移到独立的类中,以表示对象的状态。
状态模式的意图在于:将表示对象状态的逻辑分散到代表状态的不同类中,用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。
状态模式,可以看作是策略模式的延伸。两种模式都是基于组合的,它们通过将一些工作委托给辅助对象来改变上下文的行为,但是策略模式对Strategy的具体实现类有绝对的控制权,即Context要感知Strategy具体类型。而状态模式,Context不需要感知State的具体实现,只需要调用自己的方法,然后委托给State来完成,State会在相应的方法调用时,自动设置状态,这个过程对Context来说是透明的。
应用场景:
1)当一个对象的行为取决于它的状态,并且它必须在运行时根据状态改变它的行为时,可以考虑使用状态模式;
2)一个操作中含有庞大的分支结构,并且这些分支决定于对象的状态时,可以使用状态模式。
观察者模式Observer
观察者模式是一种行为设计模式,指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
这种模式和发布-订阅模式很像,不同的是,发布-订阅模式,一般有一个调度中心,调度中心有拉模式和推模式来推送消息。
观察者模式的意图在于:主要解决多个对象间存在一对多的依赖关系,当一个对象状态改变给其他对象通知的问题,让主题和观察者之间松耦合。
观察者模式的在对象间定义一对多的依赖,当一个对象改变状态,依赖的对象都会收到通知,并自动更新。Swing、JavaBeans、RMI中都有观察者模式的应用。
应用场景:
1)对象间存在一对多关系,一个对象的状态发生改变会影响其他对象时,可以使用观察者模式;
2)当应用程序中的某些对象必须观察其他对象时,可以使用观察者模式。
中介者模式Mediator
中介者模式又叫调停模式,是一种行为设计模式,中介者使各个对象不需要显示的相互引用,而是通过一个特殊的中介者对象使程序组件间接通信,从而减少程序组件之间的耦合,它是迪米特法则的典型应用。
该模式限制了对象之间的直接通信,并迫使它们仅通过中介对象进行协作,类似于MVC模式中的 Controller 部分,控制器(C)就是模型(M)和视图(V)的中介者。
中介者模式的意图在于:减少对象之间的混乱关系(大量多对多的关系)时,通过中介者统一管理这些对象,将对象之间的交互封装在中介者的对象中,从而减少对象间的耦合。
中介者模式的主要目的就是消除一组系统组件之间的相互依赖关系,而变成依赖于单个中介对象,它集中了系统组件之间的通信,组件只知道中介对象,而很难直接与真正的组件(房东)通信。
应用场景:
1)许多对象以复杂的方式交互时的依赖关系使得系统难以理解和维护时,可以使用中介者模式;
2)一个对象引用其他很多对象,导致难以复用该对象时,可以使用中介者模式。
迭代器模式Iterator
迭代器模式是一种行为设计模式,提供一个对象来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示(列表、堆栈、树…)的情况下遍历集合的元素。
迭代器模式不仅仅是遍历一个集合,我们可以根据我们的需求提供不同类型的迭代器。不过我们很少会自己实现一个迭代器,java API中提供的迭代器完全够用了。
迭代器模式的意图在于:提供一种在不暴露其底层表示的情况下访问聚合对象的元素的方法。
迭代器模式是通过将聚合对象的遍历行为分离出来,抽象成迭代器类来实现的,其目的是在不暴露聚合对象的内部结构的情况下,让外部代码透明地访问聚合的内部数据。
应用场景:
1)如果希望提供一种标准方法来迭代集合并隐藏客户端程序的实现逻辑时,可以使用迭代器模式;
2)当需要为遍历不同的聚合结构提供一个统一的接口时,可以使用迭代器模式。
访问者模式Visitor
访问者模式是一种行为设计模式,在GoF的《Design Pattern》中的定义是:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
简单的说,访问者模式就是把作用于元素(数据结构/算法)的操作分离出来封装成独立的类,使得操作集可以相对自由的实现,新增操作时不违背开闭原则。
访问者模式的意图在于:将数据结构与作用于结构上的操作进行解耦,使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。
是不是觉得访问者模式有点像模板方法模式,都是封装了固定不变的东西,开放了可变的东西。模板方法模式为可变部分预留扩展点,而访问者模式,将可变点分离出来由访问者来决定做一些处理,但是它们还是有一点区别的,访问者模式在变化(访问者)与固定(被访问者)之间,是组合关系,而模板方法模式的变化与固定之间是继承关系。
访问者模式在迭代器模式下做了进一步的分离,它是对迭代器模式的扩充,可以遍历不同的对象,也可以在遍历的同时执行一些其他的操作。
应用场景:
1)如果需要对复杂对象结构或对象结构比较稳定,但需要对所有元素执行操作时,可以使用访问者模式;
2)可以将所有其他行为提取到一组访问者类中,使您的应用程序的主要类更加专注于它们的主要工作;
3)可以充当拦截器角色。
备忘录模式Memento
备忘录模式是一种行为设计模式,又叫快照模式,是指在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后需要时能将该对象恢复到原先保存的状态。
通俗地说,备忘录模式就是一个对象的备份模式,提供了一种程序数据的备份方法,具体采用哪种方法来存储对象状态,取决于对象需要保存时间的长短。
备忘录模式的意图在于:为对象状态提供存储和恢复功能,对象的已保存状态数据在对象外部不可访问。
借助备忘录模式,可以捕获对象的状态,对象的已保存状态数据在对象外部是不可访问,这保护了已保存状态数据的完整性,这也是备忘录模式的优势所在。
我们可能用的比较多的就是把对象的状态存储在另外一个对象中或者是为了支持对象跨多个会话的持久性存储,使用对象序列化来存储对象信息,也就是我们前面学习的原型模式,原型模式可以是备忘录模式的替代。所以备忘录模式并不常用。
应用场景:
1)当您想要生成对象状态的快照以便能够恢复对象的先前状态时,可以使用备忘录模式;
2)当直接访问对象的字段getter、setter 违反其封装时,可以使用备忘录模式。
解释器模式Interpreter
解释器模式是一种行为设计模式,指给定一种语言,定义其语法的表示形式,以及使用该表示形式来解释该语言中句子。
解释器模式的意图在于:让你根据实现定义好的一些语法规则,组合成和执行的对象。
在软件构建过程中,如果某一特定领域的问题比较复杂且类似的情况不断的重复出现,但是使用普通的编程方式来实现可能非常繁琐且不是那么灵活,面临非常频繁的修改,这种情况下使用解释器模式可能会是一种更好的选择。
其实,以前在开发中也用过这种模式去处理打印数据,将一些打印数据的转换、截取、换行写在配置文件中,用一串规则写在配置文件中作为一种通用的处理(只不过没有按解释器模式这种套路去写的那么规范),实在是处理不了的,才会去单独在代码中重写打印方法去处理打印数据,所以在写代码时设计模式并不是要完全套用的。
应用场景:
当一个语言需要解释执行时,并且你可以将该语言中的句子表达为一个抽象的语法树时,可以使用解释器模式。