设计模式
本文讨论了一些常用的设计模式,重点分析各种模式适用的场景,以及需要注意的地方,理解各种模式的本质含义。
关于各种设计模式的全面介绍文章有很多,本文侧重于分析理解,省略了对各种模式的基本内容的详细介绍。文末有一些参考资料,可以与本文结合起来阅读。
1. 单实例(Singleton)
单实例是最容易使用,也最常用的设计模式,可谓价格便宜量又足。
面向对象程序设计,就像规划一个公司,有生产部、研发部、设计部、销售部、财务部等等,每个部门有特定的职责,并且互相协作来完成工作。每次需要其他部门配合时,都需要找到合作部门的接口人。
从程序设计的角度看,就是每个部门是一个实例化的对象,比如财务部m,所有需要与财务部协作的其它部门(其它对象实例a、b、c等等),都需要保存对财务部实例m的引用(知道对方的接口人是谁)。
进一步考虑到公司刚创建时,各部门还不一定有构造好(程序启动时就像从零开始创建一个公司,各部门是逐个创建出来的),那么当a想要访问m时,还需要考虑m是否已经就绪,这又额外增加了一些设计上的麻烦。
单实例模式可以简单明了的解决上述问题。它相当于一个全局的智能构造访问器,当a部门需要找m部门时,如果m部门已经创建好了,就直接把m的地址告诉a,如果m还没有创建好,就创建一个m,然后把这个m的地址告诉a。这样,a既不需要保存m的地址,也不需要关心m是否已经事先创建好。
单实例的实现方法很简单,不过有一个需要注意的问题,就是多线程访问单实例需要注意线程并发问题,多线程情况下不小心的话可能导致创建出多个实例。保障线程安全的单实例写法建议如下:
Java5以及之后的版本可以用enum来实现。该方案线程安全,并且是懒加载(Lazy loading)。
public enum SingletonEnum { INSTANCE; private int someField; public void doSomething() { // ... } }public class EnumDemo { public static void main(String[] args) { SingletonEnum singleton = SingletonEnum.INSTANCE; singleton.doSomething(); } }
采用static实例。同样线程安全,是早加载(Early loading)。
public class Foo { private static final Foo INSTANCE = new Foo(); private Foo() { if (INSTANCE != null) { throw new IllegalStateException("Already instantiated"); } } public static Foo getInstance() { return INSTANCE; } }
关于单实例的实现方案的更多分析讨论,参考:
What is an efficient way to implement a singleton pattern in Java?
2. 工厂(Factory)
工厂模式,根据复杂程度的不同,可以有 简单工厂、工厂方法、抽象工厂。
类图(来自 图说设计模式)
简单工厂
工厂方法
抽象工厂
产品
工厂模式的要点是,系统需要几种不同的产品,但这些产品有相同的使用方法。所以,可以将几种不同的具体产品提炼为一种抽象的产品,即类图中的 <<abstract>> Product,比如该产品提供 use() 方法,那么不管具体是哪种产品,都会提供 use() 功能,对于使用者来说(图中未画出),不需要知道具体是哪个产品,都是通过use()方法使该产品完成特定的功能。
举例来说,某种业务处理期间会输出信息,一方面要记录到日志文件,另一方面要传送到客户端,供管理人员监控。这里,一种抽象的输出信息处理工具,就是一个抽象的产品。对于业务处理程序而言,它只需获得一个抽象的输出信息处理工具,并将自己的信息交给该工具去处理。如果实际是一个文件工具,文件工具会将信息记录到日志,如果实际是一个信息传输工具,该工具会将信息发送到客户端去显示。
假如某一天需要将信息记录到数据库,则增加一个数据库处理工具,该工具完成将信息记录到数据库的任务。而对于业务处理程序而言,依然不需要知道到底是哪个工具、以及该工具具体是如何处理信息。
工厂
产品的特性清楚了,那么工厂的意义是什么、以及选用哪种工厂呢?
工厂的作用在于根据情况创建具体的产品。上面例子中,工厂就是负责创建 文件工具、远程传输工具、数据库工具,并将其中之一交给业务处理程序。
如果这几种工具的创建过程很简单,就可以用简单工厂,在一个类里面搞定。如果比较复杂,就用工厂方法,每种工具有各自的工厂类来创建,当然,这个时候,工厂也是抽象的,也就是说,不同产品的工厂有相同的创建接口,并且都返回相同的产品——即上面所说的抽象的产品。
多产品
如果产品比较复杂,不是单个产品,而是需要多件产品的套件,则使用抽象工厂。看看类图,工厂方法生产一种抽象产品,抽象工厂生产多种抽象产品。比如界面主题,不同的主题可以设置不同的颜色搭配、字体、日期时间格式、文本样式、边框样式、动态背景等等,这样一系列设置项,可以用抽象工厂来创建一套设置工具,分别完成各项目的设置。
生产和使用(与其它模式的关系)
作为创建型模式,工厂的应用很广泛。原则上讲,生产和使用是一个产品生命中的两个阶段,要使用一个产品,必须先生产出来。同时,我们也不会生产没用的产品,浪费资源,所以生产出来的产品都会被使用。
生产
程序中多数时候直接new一个对象,然后使用该实例。
但如果用到某个使用了Interface的设计模式,很可能就要搭配使用工厂模式了。因为Interface必然有实现者(被某个class implement,在类图中通常命名为ConcreteXYZ,代码比如class ConcreteXYZ implements InterfaceXYZ
)。而且不止一个实现者(如果只有一个ConcreteXYZ,就不必要用Interface来提取抽象接口了)。而多个ConcreteXYZ每个都需要被创建出来,创建多个不同类型的ConcreteXYZ,通常就需要采用某种工厂模式。使用
工厂虽然创建了一组ConcreteXYZ,但使用者(在类图中通常命名为Client,但有时没有画出来)通常不直接使用这些ConcreteXYZ,而仅使用它们共同的Interface接口中的方法,也就是说,Client不关心也不知道是哪个具体的ConcreteXYZ在执行任务。代码通常是interfaceXYZ.doSomething()
,而不是concreteXYZ.doSomething()
。配置
工厂创建的是一组ConcreteXYZ,Client却要使用interfaceXYZ,但这个interfaceXYZ在被使用时其实是一个ConcreteXYZ,那么Client怎样知道何时使用哪个ConcreteXYZ呢?
所以程序设计中还需要一段配置逻辑,在不同的情况下将适当的ConcreteXYZ交给Client。如果配置逻辑是固定的,可以直接在代码中实现,比如一串if else,在不同情况下return不同的ConcreteXYZ。如果需要在运行时设定,可以通过界面由用户选择。如果还要动态加载,可以通过配置文件+反射机制,比如框架创建和使用上层业务对象。
配置将生产和使用关联到一起。
小结
具有相同接口但不同实现的一组产品,可以用工厂模式来创建,具体采用哪种,要看创建对象的过程是简单还是复杂。工厂模式关注的是产品生命周期中创建阶段,不过程序设计时我们同时要考虑产品的使用和配置。
3. 适配器(Adapter)
适配器模式,顾名思义,就是适配器的意思。比如出国旅行,不同国家的电源插座是不一样的,我们带一个转换插座,将各种插座统一转换成我们常用的三孔或两孔插座,这个转换插座就是适配器。
类图(来自 图说设计模式)
对象适配器
使用场景
1. 不方便修改已有的接口
考虑上面转换插座的例子,因为各国插座的接口已经固定好了,不可能要求别人按照我们的标准更换插座,所以只能自己带转换插座去转接。在程序的角度,比如我们要采用外部第三方的组件,就可能需要在中间加一个适配器。
假如新设计一个程序,需要适配器的可能性较小。就比如假设各国还没有制定插座标准,那么就制定一个统一规范是最简单的。
2. 通常需要适配多个不同的接口,需要搭配工厂模式
假如有一天,你有一家“鸡拱门”公司收购了麦当劳和肯德基,你当然希望为客户提供“巨无霸+全家桶”套餐,但是整合两家店的订餐系统太麻烦了,如果在客户和两个订餐系统之间加一层适配器(具体有2个适配器,一个对接麦当劳,一个对接肯德基)就比较简单。如果是巨无霸,适配器将订单发给麦当劳,如果是全家桶,适配器将订单发给肯德基。
既然是2个适配器,其实,这就需要一个适配器工厂,也就是说,系统需要知道哪个订单交给哪个适配器去处理。
假如只收购了麦当劳呢?就一套系统,直接用就可以了哦。
4. 外观/门面/助理(Facade)
外观模式适用于将复杂的过程总结为一个简单的接口,就像老板助理。老板说,下周我要举办一个晚宴。助理就要安排场所和菜单、确认客人名单和座位、找财务要预算、安排宴会流程等等。老板就是提一个要求,不关心怎样操作,只要最后宴会顺利进行,助理搞定所有具体细节。
对照下面的类图来看,SystemA、B、C就相当于场所、财务等等,各种具体的细节事物。助理的角色,就是门面Facade,助理是SystemA、B、C的门面,她/他屏蔽了A、B、C的细节,只把结果(门面上展示的产品)呈现给老板。老板是Client,只交代助理要完成某任务,不管其它。
外观模式
使用场景
老板频繁开宴会,就需要一个专职的宴会助理。在这个意义上,称之为助理模式更贴切一点。
如果只是一次性的操作,比如公司倒闭的时候老板请散伙饭,反正也就一次,助理也懒了,老板只好自己操持各种细节,也是可以的。小结
外观模式的主要作用就是屏蔽细节。
5. 代理(Proxy)
买卖房屋通常都通过房产中介来交易,中介就是一种代理。在程序设计中,代理也是用来隔离调用者和被调用者,不过代理模式通常有更具体一些的应用场景。
类图(来自 图说设计模式)
[图片上传失败...(image-d3eb11-1535017736982)]
使用场景
空间代理
client不需要了解所操作的对象的实际位置。比如远程(Remote)代理:为一个位于不同的地址空间的对象提供一个本地 的代理对象,这个不同的地址空间可以是在同一台主机中,也可是在 另一台主机中,远程代理又叫做大使(Ambassador)。时间代理
比如代理先提供一个缩略图,必要时再加载大图。虚拟(Virtual)代理:如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建。
Copy-on-Write代理:它是虚拟代理的一种,把复制(克隆)操作延迟 到只有在客户端真正需要时才执行。一般来说,对象的深克隆是一个 开销较大的操作,Copy-on-Write代理可以让这个操作延迟,只有对象被用到的时候才被克隆。缓冲(Cache)代理
为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果。保护(Protect or Access)代理
控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限。
防火墙(Firewall)代理:保护目标不让恶意用户接近。强化代理
为被代理对象增加一些额外的功能,比如:
同步化(Synchronization)代理:使几个用户能够同时使用一个对象而没有冲突。
智能引用(Smart Reference)代理:当一个对象被引用时,提供一些额外的操作,如将此对象被调用的次数记录下来等。
对比适配器、外观模式、代理
适配器、外观、代理,可以说都是在client与调用对象之间增加了一层中间层,它们的主要区别在于:适配器侧重于统一不同制式的接口(插座转换器)
外观侧重于整合复杂的细节(老板助理)
代理则用于一些特定的情况(上面的举例)
6. 组合(Composite)
组合模式强调整体和部分具有一些相同的功能。这里有两个要点,一个是具有一些共性,另一个是具有 整体-部分 关系。比如目录下可以放文件,不过都可以进行复制、粘贴。
类图(来自 维基百科)
组合模式
使用场景
常见的组合模式是具有树状结构的数据,比如文件系统中 目录-子目录-文件;公司架构中 公司-部门-子部门。在Java程序中,GUI组件也是一种 组合模式,看下面的类图(来自设计模式):
GUI组件
我们考察一下这个例子为什么适合用组合模式。
共性
可以看到,Checkbox、Button、TextComponent等控件都是一个Component,而且Container容器也是一个Component,我们看看Java文档中关于 Component 提供的方法:这些方法,包括菜单、监听器、边框、绘图等,无论对控件(Button等)还是容器(Container)都是同样有效的,所以它们全都继承自Component。
add(PopupMenu popup)
addMouseListener(MouseListener l)
getBounds()
repaint()
等等
整体-部分
再看 Container 的文档,它提供的方法:
容器(Container)需要提供对组件(Component)的管理。
Container 里面可以包含 Component,所以具有 整体(Container) - 部分(Component) 关系。注意,Container自身也是一个Component,所以 Container里面也可以包含其它Container,就像目录下面可以有子目录一样。
典型的组合模式会有 整体-部分 关系,一个容器包含多个Component,所以容器通常会提供 add/remove/get 等功能。
add(Component comp)
add(Component comp, int index)
等等
7. 装饰(Decorator)
装饰模式用于动态地给一个对象添加一些额外的功能。
比如做一个生日蛋糕,基础部分就是一个蛋糕外面抹奶油。但是可以提供各种额外的装饰,比如写上生日快乐,加蜡烛,添加水果,还可以做多层蛋糕,等等。可以选择采用其中一种或多种装饰依次叠加,得到各式各样的蛋糕。而且以后还可以增加新的装饰种类。当然,不管怎样,原则是最后都要生成一个蛋糕。
类图(在 装饰模式 的基础上修改)
装饰模式
链式装饰
使用装饰模式的时候,经常需要一种链式装饰:做一个基础产品->添加装饰a->添加装饰b......,用代码写出来通常是这样:
(蛋糕)基础蛋糕 = new 巧克力蛋糕 (蛋糕)草莓蛋糕 = new 草莓蛋糕(基础蛋糕) (蛋糕)蜡烛草莓蛋糕 = new 蜡烛草莓蛋糕(草莓蛋糕)
每一次装饰都需要在上一个蛋糕之上进行,即需要将上一个对象实例作为参数传递给当前装饰对象。如果装饰层次太多,也会增加系统复杂度。
当然,好处是可以随意选择不同的装饰,还可以采用不同的顺序。
装饰、组合、代理
对比一下上面组合模式的类图,是不是和装饰模式有相似的感觉。
Decorator继承自Component,同时也包含一个Component变量,这也是一种组合关系,所以类图上看它们之间的关系,与组合模式中Composite与Component的关系是同样的。而且装饰模式中ConcreteComponent也类似于组合模式中的Leaf。
但是装饰和组合是针对不同的应用场景,我们看组合模式中Composite强调的是对Composite的add/remove/get,装饰模式中强调的是对Composite增加功能addBehavior。组合模式通常容器中包含多个Component,装饰模式中通常装饰者只使用一个被装饰者。
说到增加功能,上面讲代理的时候,有一种代理的应用场景也是给对象增加一些额外功能的,比如增加引用计数。那么这里装饰模式与代理模式有什么不同呢?最大的区别在于装饰模式更强调动态增加。也就是说,在基础功能之上,今天可能需要加个A功能,明天需要加个B功能,或者运行时根据情况决定添加哪些功能,这时就适合用装饰模式。
8. 享元(Flyweight)
当系统中存在大量细粒度对象时,如果这些对象具有部分相同的信息,则这些相同的部分内容可以共享,不必每个对象都持有该信息的副本,从而减少内存消耗。
我们可以将享元理解为一定数量的公共对象池。当需要某个享元对象时,并不是每次直接new一个新对象出来,而是获得公共对象池中自己所需对象的引用。
比如Java字符串String的设计就是一种享元模式,程序中所有String其实都放在一个String对象池中,如果两个String的内容相同,其实是引用同一个对象。
String a = "hello";String b = "hello"; System.out.println(a==b); // true
类图(来自《Design Patterns: Elements of Reusable Object-Oriented Software》)
享元模式
要维护公共对象池,通常可以采用一个单实例的享元工厂,它提供一个静态的工厂方法用于返回享元对象。如果所要求的享元对象已经存在,直接返回其引用,如果不存,则创建一个,并返回其引用。
案例
再看一个例子(参考设计模式之禅(第2版))。一个考试报名系统,每个考生报名时需要填写报名信息,包括 科目、考点、准考证邮寄地址等。当考生很多时,系统会创建大量的报考信息object,每个考生一个。但实际上只有4个科目,30个考点。所以科目和考点信息(对象实例)是可以共享的,无论有1千考生还是10万考生,都只需要4个科目对象,30个考点对象。
9. 桥接/双层抽象(Bridge)
桥接模式是软件设计模式中最难以理解的模式之一,它的目的是
Decouple an abstraction from its implementation so that the two can vary independently.
将一个抽象与它的实现解耦,使它们可以各自独立的变化。
这句话里面最让人费解的是抽象和实现,到底是指什么?先看下类图
类图(来自维基百科)
桥接模式
把Client加进去看一下(来自《Design Patterns: Elements of Reusable Object-Oriented Software》):
桥接模式
双层抽象
从类图可以看出,Client只使用Abstraction,Abstraction保存Implementor并使用它的方法。其实这就是一个双层抽象,GOF将第一层抽象称为Abstraction(抽象),第二层抽象称为Implementor(实现)。Abstraction持有并使用Implementor,称之为Bridge/桥接。引用一下《Design Patterns: Elements of Reusable Object-Oriented Software》中关于Implementor的说明:
Implementor
defines the interface for implementation classes. This interface doesn't have to correspond exactly to Abstraction's interface; in fact the two interfaces can be quite different. Typically the Implementor interface provides only primitive operations, and Abstraction defines higher-level operations based on these primitives.Implementor是实现类的公共接口,这些接口不需要与Abstraction的接口相同,实际上,这两者的接口经常有很大区别。典型情况下,Implementor接口是比较原始的操作,而Abstraction接口则是比较上层的操作,并且是基于Implementor的原始操作来实现。
Abstraction具有扩展性
另外需要注意的一点是,Abstraction是可以通过继承来扩展其功能的。在类图上体现的就是Abstraction提供 function() 功能,继承它的 RefinedAbstraction 还提供 refinedFunction() 功能。
案例
现在我们看一下书中的例子,来进一步帮助理解 Abstraction(抽象)和 Implementor(实现),以及什么场景下会需要这种桥接结构。(配图及代码来自《Design Patterns: Elements of Reusable Object-Oriented Software》)。
我们需要设计一个可应用于多种操作系统平台的通用GUI工具包,考虑其中Window类的设计。主要面临的问题是,
Window有多种形式,比如常规的window,外面是标题边框,里面显示具体的视图内容;也有图标型的window,它不显示视图内容,而是画出一个图标;等等。
不同操作系统在界面上画出GUI元素的实现方式是不一样的,比如 X Window 操作系统 和 IBM的 PM操作系统( Presentation Manager),等等。
如果用通常的继承结构,可能需要有 XNormalWindow, XIconWindow, PMNormalWindow, PMIconWindow,总共需要 [Window类型数 * 支持的OS数] 个class,当Window类型和支持的OS较多时,程序设计和维护都非常困难。
使用桥接模式,我们可以定义一个高层的Window接口(Abstraction)和一个底层的绘图接口(Implementor)。如下图所示:
跨平台Window类设计
不同类型的具体的Window基于Window(Abstraction)扩展,不同OS的支持基于WindowImp(Implementor)提供实现。两者可以独立的扩展,互不依赖。
下面摘录书中的部分代码,该案例详细说明和更多代码请参考原书。
Window会提供比较高层的功能
class Window { public: Window(View* contents); // requests handled by window virtual void DrawContents(); virtual void Open(); virtual void Close(); virtual void Iconify(); virtual void Deiconify(); // requests forwarded to implementation virtual void SetOrigin(const Point& at); virtual void SetExtent(const Point& extent); virtual void Raise(); virtual void Lower(); virtual void DrawLine(const Point&, const Point&); virtual void DrawRect(const Point&, const Point&); virtual void DrawPolygon(const Point[], int n); virtual void DrawText(const char*, const Point&); protected: WindowImp* GetWindowImp(); View* GetView(); private: WindowImp* _imp; View* _contents; // the window's contents };
WindowImp需要提供与OS相关的更细节的绘图功能
class WindowImp { public: virtual void ImpTop() = 0; virtual void ImpBottom() = 0; virtual void ImpSetExtent(const Point&) = 0; virtual void ImpSetOrigin(const Point&) = 0; virtual void DeviceRect(Coord, Coord, Coord, Coord) = 0; virtual void DeviceText(const char*, Coord, Coord) = 0; virtual void DeviceBitmap(const char*, Coord, Coord) = 0; // lots more functions for drawing on windows... protected: WindowImp(); };
不同类型的Window有自己独特的实现
class IconWindow : public Window { public: // ... virtual void DrawContents(); private: const char* _bitmapName; }; void IconWindow::DrawContents() { WindowImp* imp = GetWindowImp(); if (imp != 0) { imp->DeviceBitmap(_bitmapName, 0.0, 0.0); } }
结合抽象工厂
Window需要一个WindowImp的实例,有时是XWindowImp,有时是PMWindowImp,要看在哪个平台上运行。动态创建实例的场景适合用抽象工厂模式。由一个单实例工厂负责根据所在的操作系统平台来创建和返回相应的WindowImp实例。
WindowImp* Window::GetWindowImp () { if (_imp == 0) { _imp = WindowSystemFactory::Instance()->MakeWindowImp(); } return _imp; }
参考
强烈建议阅读原文《Design Patterns: Elements of Reusable Object-Oriented Software》/《设计模式:可复用面向对象软件的基础》
10. 观察者(Observer)
当一个对象状态发生改变时,需要了解其状态变化的对象都会得到通知。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。此种模式经常用于实时事件处理系统,比如Java中的GUI控件发布的事件,鼠标/键盘事件等。
另外比如 生产-消费 模型,多个生产者生产产品,多个消费者消费产品,在多线程设计中可以采用 wait-notify 方式来实现。产品都放在一个队列中,当生产者向队列中增加产品 或 消费者从队列中取出产品时,都会通知(notifyAll)其它正在等候(wait)的生产者和消费者,队列中的产品状态已经改变。被唤醒的的生产者和消费者会竞争同步锁,获得锁以后进行自己的生产或消费活动。
类图(来自设计模式)
[图片上传失败...(image-6e9abe-1535017736982)]
订阅和发布可以是多对多的关系,当然也可以一对多、多对一、一对一。
状态变化后的处理,如果比较简单快捷,可以同步完成。即状态变化时,状态对象函数直接调用观察者的函数,观察者方法返回后,状态对象的函数才会返回。
如果观察者的处理较为复杂,可以采用异步调用。即观察者在自己的线程中对状态变化的情况进行处理,当然,可以需要增加线程同步和协作的机制。比如上面讲的 生产-消费 模型。
观察模式可以形成观察链,比如a观察b,b观察c,c观察d,当d变化时通知c,从进而通知b,b再通知a。观察链增加了系统结构的复杂度,最好不要设计太多环节。
11. 命令(Command)
命令模式将命令本身抽象出来,即类图中的 <<interface>>Command ,从而我们可以对命令本身进行调度或操作。
类图(来自 维基百科)
命令模式
程序在绝大多数情况下,调用者直接调用执行者的某个方法,传入所需的参数,并获得执行结果。但如果遇到需要将命令这个东西本身抽象出来的情况,就可以采用命令模式。也就是说需要使用Command.execute() 这种间接的方式来调度命令的执行。
结合类图来看的话,通常 Client 直接调用 receiver.action()
。
使用命令模式后,Client 先设置好某个命令由哪个Receiver来执行,并将该命令放置到命令队列中,或者直接关联到某个 Invoker上面,但并不直接执行,而是由Invoker 来调度命令的执行。
案例
你开了一家小吃店,开始只有一个厨师三张桌子,客户下单的时候,你直接往后厨喊一嗓子“红烧肉盖饭”,厨师就直接做出来。程序中绝大多数调用就是这么干的。
因为做得好吃服务也很到位,你的生意越做越大,一不小心变成10个大厨50个菜品的网红餐厅。这时就要为下单的客人打印一张所点的菜品清单,多位客人的点菜清单(命令)依次发给后厨,大厨们选择清单中的菜品进行制作。适用场景
线程池
从程序设计的角度来看,这是一个10个线程50种任务的线程池系统。这里很自然需要采用命令模式,下单时服务员(Client)不能直接调用某个厨师(Reveiver)来完成某个菜品,必须将菜品命令(Command)添加到任务队列,由调度系统(Invoker)在根据一定的规则选择厨师(线程)来制作某个菜品(Command)。
进一步考虑,有时客户会修改点单,增加或删除菜品。这时只需要在菜品任务队列中添加或删除相应的菜品就可以了。
所以,用命令模式整个系统架构更加清晰,而且服务员和厨师都是异步执行,效率高。撤销和重做(undo/redo)
有些程序需要提供撤销和重做(undo/redo)功能,比如我们常用的WORD,或者PS。这跟上面修改菜品任务队列是类似的(不过undo/redo动作只能在栈顶操作,不能随意调整命令的次序),所以undo/redo也是一种适合采用命令模式的场景。GUI命令响应
界面元素,比如按钮、菜单等,当用户点击时会触发一个命令动作。我们可以预先将GUI元素(按钮、菜单等)与对应的命令绑定,一旦用户触发动作,绑定的命令就会执行。重点代码如下(来自 A Java Action, ActionListener, and AbstractAction example,稍做修改):
/** * Our "Copy" action. */ public class CopyAction extends AbstractAction { public CopyAction(String name, ImageIcon icon, String shortDescription, Integer mnemonic) { super(name, icon); putValue(SHORT_DESCRIPTION, shortDescription); putValue(MNEMONIC_KEY, mnemonic); } public void actionPerformed(ActionEvent e) { // 假设 docMgr 是一个文档内容管理器, docMgr.instance().doCopy(); } } // 创建一个命令(copyAction) copyAction = new CopyAction("Copy", copyIcon, "Copy stuff to the clipboard", new Integer(KeyEvent.VK_COPY)); // 命令绑定到菜单 JMenuItem copyMenuItem = new JMenuItem(copyAction); // 同一个命令绑定到按钮 JButton copyButton = new JButton(copyAction);
在这个例子中,命令模式的作用不算太重要,其主要用途是,可以用一个命令对象(copyAction)绑定多个界面元素(一个菜单和一个按钮)。
如果不用命令对象,直接在 ActionListener 中调用目标方法也是可以的。虽然需要分别设置ActionListener,但省略了CopyAction类,其实也很简洁。示例代码如下:
// 按钮事件监听器 copyButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { docMgr.instance().doCopy(); } }); // 菜单事件监听器 copyMenuItem.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent arg0) { docMgr.instance().doCopy(); } });
解疑
Client和Receiver
在基本场景下(程序绝大多数情况下调用方法时),只有Client和Receiver,Client直接调用Receiver的方法并获得返回结果。就像你直接喊厨房做个红烧肉一样。Invoker
在命令模式的场景下,由于某些原因Client不方便直接调用Receiver,所以通过Invoker来调度命令的执行,所以,Invoker是命令的调度者,比如餐厅的例子。
也有些时候,命令一触发就立即执行,但Invoker会附加一些针对命令本身的操作。这时,Invoker对命令进行额外的操作。比如redo/undo情况下,Invoker需要维护命令队列,以及当前有效命令在队列中的哪个位置,并根据情况要求命令执行redo/undo。
不管是调度命令,还是进行额外的操作,Invoker既不知道命令的发起者,也不知道命令的执行者,也不关心命令执行的结果,只是单纯的调度或维护命令本身。
Client/Receiver和Invoker
同样,Client和Receiver也不知道命令是如何调度的,或者做了什么其它的事情。Client只关心自己的命令肯定会被执行,并返回结果。也就是说服务员(Client)向厨房(Receiver)下的单肯定都会被完成,虽然不清楚何时、由哪个厨师完成。
看类图上面,Client/Receiver 与 Invoker 之间都没有任何联系。Client/Receiver和Invoker 解耦。
Client与Receiver解耦了吗?
注意看类图 Client 是明确需要使用 Receiver的,这两者并没有解耦。
客户点餐后,服务员会下单给厨房,客户结账时,服务员会把客户引到收银台,业务不熟的服务员是不行的。
有时候好像不需要Receiver?
很多时候我们可以直接在 Action 里面执行一系列操作,并不需要专门定义一个 Receiver类,好像 Receiver 被省略掉了。这里,我们应该理解为没有显式的定义一个Receiver,但依然完成了Receiver的功能。即使只是在Action里面System.out.println(),这个System就是Receiver。
小结
当且仅当我们需要对 命令本身 进行 调度(比如线程池)或 操作(比如redo/undo)时,可以采用命令模式。参考
维基百科 Command pattern
Why should I use the command design pattern while I can easily call required methods?
Why do we need a “receiver” class in the Command design pattern
12. 策略(Strategy)
类图(来自图说设计模式)
策略模式
某品牌在不同情况下有各种促销活动方案,比如中秋活动、换季折扣、积分抵扣、优惠券、满减、套装等等,而且还可能不断增加新的活动类型。
public double calcPrice(int 活动类型) { if(活动类型 == 中秋活动){ 计算中秋价格; } else if (活动类型 == 换季折扣){ 计算换季折扣价格; } ...... }
这样一大堆 if else 会非常繁琐。那么我们采用 策略模式来看看。
Interface 策略 { public double caldPrice(); }class 中秋策略 extends 策略{ public double caldPrice() { 计算中秋价格; } }class 换季折扣策略 extends 策略{ public double caldPrice() { 计算换季折扣价格; } }class 价格管理 { private 当前策略; public void 设置当前策略(策略 oneStrategy) { 当前策略 = oneStrategy; } // 主流程非常简单明了 public double calcPrice() { return 当前策略.caldPrice(); } }
策略模式简化了主流程,隔离了不同策略的实现。
不过呢,其实我们也并没有完全避开那些 if else,必然在程序中的某个地方(比如对策略的配置管理),需要根据用户的设置来指定 当前采用哪一个具体的策略。
动态策略
如果希望动态加载策略,甚至支持第三方开发的策略,那么策略模式就很有必要了。由于策略被定义为一个抽象的接口,调用者不需要知道具体的策略是哪一种,如果我们采用反射机制,可以完全动态的加载一个第三方策略,使用该策略来计算价格。策略与工厂
如果对比一下工厂模式,可以发现策略与工厂的类图长得很像。策略模式是一个抽象的策略接口,然后有几个具体的策略类。工厂模式有一个抽象的产品接口,然后有几个具体的产品类。它们的差异是什么?如果紧扣模式本身的概念来说,工厂是创建模式,策略是行为模式。但进一步想,创建之后不需要行动吗?多种不同的行为难道不是要先创建出来吗?策略模式中的 各种策略,其实也是被工厂生产出来的产品。工厂模式和策略模式,分别强调了一个完整过程中的两个阶段,生产阶段(工厂)和行为阶段(策略),当我们采用其中一个模式时,另一个模式也必然会(显式或隐式)的被使用。
语义
策略模式架构很简单,但还是被单独命名为一个模式,更多的是指出一类应用场景,即与策略、算法等相关的应用。
总结
设计模式的本质,是希望找到系统中共性的部分和差异的部分,将共性的部分提炼为接口(interface),差异的部分定义成不同的具体的类(class)来实现(implement)那个共同的接口。对于使用者(Client)来说,就可以调用抽象的接口(的实例),而不用关注具体是哪个类的实例在完成工作。
不管哪种设计模式,看到类图的时候最主要的关注点就是Interface部分,它代表了系统中可以被抽象出来的共性。
作者:X猪
链接:https://www.jianshu.com/p/52d983ba2e73