前言
因为设计模式种类多,且重理解重回忆,所以本文尽量言简意赅,便于时时温习。
设计模式(Design Pattern)是前辈们对代码开发经验的总结,是解决特定问题的一系列套路。它不是语法规定,而是一套用来提高代码可复用性、可维护性、可读性、稳健性以及安全性的解决方案。
1995年,GoF(Gang of Four,四人组/四人帮)合作出版了《设计模式:可复用面向对象软件的基础》一书,共收录了23种设计模式,从此树立了软件设计模式领域的里程碑,人称「GoF设计模式」。
这 23 种设计模式的本质是面向对象设计原则的实际运用,是对类的封装性、继承性和多态性,以及类的关联关系和组合关系的充分理解。
当然,软件设计模式只是一个引导,在实际的软件开发中,必须根据具体的需求来选择:
对于简单的程序,可能写一个简单的算法要比引入某种设计模式更加容易;
但是对于大型项目开发或者框架设计,用设计模式来组织代码显然更好。
我们要清楚,设计模式并不是Java的专利,它同样适用于C++、C#、JavaScript等其它面向对象的编程语言。
设计原则
开闭原则
开闭原则的含义是:当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。
里氏替换原则
里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。
依赖倒置原则
依赖倒置原则的原始定义为:高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。其核心思想是:要面向接口编程,不要面向实现编程。
依赖倒置原则是实现开闭原则的重要途径之一,它降低了客户与实现模块之间的耦合。
单一职责原则
单一职责原则又称单一功能原则,由罗伯特·C.马丁(Robert C. Martin)于《敏捷软件开发:原则、模式和实践》一书中提出的。这里的职责是指类变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。
该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:
- 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
- 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。
迪米特法则
迪米特法则的定义是:只与你的直接朋友交谈,不跟“陌生人”说话。其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。
迪米特法则中的“朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。
模式分类
根据目的来分类
根据模式是用来完成什么工作来划分,这种方式可分为创建型模式、结构型模式和行为型模式3种。
创建型模式:用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。
GoF中提供了单例、原型、工厂方法、抽象工厂、建造者等5种创建型模式。
结构型模式:用于描述如何将类或对象按某种布局组成更大的结构。
GoF中提供了代理、适配器、桥接、装饰、外观、享元、组合等7种结构型模式。
行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。
GoF中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等11种行为型模式。
根据作用范围来分类
根据模式是主要用于类上还是主要用于对象上来分,这种方式可分为类模式和对象模式两种。
类模式:用于处理类与子类之间的关系,这些关系通过继承来建立,是静态的,在编译时刻便确定下来了。
GoF中的工厂方法、(类)适配器、模板方法、解释器属于该模式。
对象模式:用于处理对象之间的关系,这些关系可以通过组合或聚合来实现,在运行时刻是可以变化的,更具动态性。
GoF中除了以上4种,其他的都是对象模式。
创建型模式 | 结构型模式 | 行为型模式 | |
---|---|---|---|
类模式 | 工厂方法 | (类)适配器 | 模板方法、解释器 |
对象模式 | 单例、原型、抽象工厂、建造者 | 代理、(对象)适配器、桥接、装饰、外观、享元、组合 | 策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录 |
1 策略模式
【介绍】:
策略模式(Strategy Pattern)该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理。
策略模式允许我们在实现某一个功能时,如果存在多种算法或者策略,我们可以根据环境或者条件的不同选择不同的算法或者策略来完成该功能,如数据排序策略有冒泡排序、选择排序、插入排序、二叉树排序等,我们可以根据不同的场景使用不同的算法。
如果使用多重条件转移语句实现(即硬编码,if-else),不但使条件语句变得很复杂,而且增加、删除或更换算法要修改原代码,不易维护,违背开闭原则。如果采用策略模式就能很好解决该问题。
【比喻】:
在现实生活中常常遇到实现某种目标存在多种策略可供选择的情况,例如,出行旅游可以乘坐飞机、乘坐火车、骑自行车或自己开私家车等,超市促销可以釆用打折、送商品、送积分等方法。
【优点】:
- 多重条件语句不易维护,而使用策略模式可以避免使用多重条件语句,如 if…else 语句、switch…case 语句。
- 策略模式提供了一系列的可供重用的算法族,恰当使用继承可以把算法族的公共代码转移到父类里面,从而避免重复的代码。
- 策略模式可以提供相同行为的不同实现,客户可以根据不同时间或空间要求选择不同的。
- 策略模式提供了对开闭原则的完美支持,可以在不修改原代码的情况下,灵活增加新算法。
- 策略模式把算法的使用放到环境类中,而算法的实现移到具体策略类中,实现了二者的分离。
【缺点】:
- 客户端必须理解所有策略算法的区别,以便适时选择恰当的算法类。
- 策略模式造成很多的策略类,增加维护难度。
【应用】:
Spring在具体实例化Bean的过程中,创建对象时先通过ConstructorResolver找到对应的实例化方法和参数,再通过实例化策略InstantiationStrategy进行实例化,它有两种具体策略类,分别为SimpleInstantiationStrategy和CglibSubclassingInstantiationStrategy,前者对构造方法无MethodOverrides的对象使用反射来构造对象,而构造方法有MethodOverrides的对象则交给CglibSubclassingInstantiationStrategy来创建。
【案例】:
1 | //抽象策略类 |
1 | 具体策略A的策略方法被访问! |
2 观察者模式
【介绍】:
观察者模式(Observer Pattern)指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式。
它的关键实现是在抽象类里有一个列表存放观察者们。一旦有变动发生,则依次调用这些观察者的相关方法。
【比喻】:
就是现实中的发布-订阅模型,或者说广播模型。
【优点】:
- 降低了目标与观察者之间的耦合关系,两者之间是抽象耦合关系。符合依赖倒置原则。
- 目标与观察者之间建立了一套触发机制。
【缺点】:
- 目标与观察者之间的依赖关系并没有完全解除,而且有可能出现循环引用。
- 当观察者对象很多时,通知的发布会花费很多时间,影响程序的效率。
【应用】:
Spring中的监听机制就使用到了观察者模式,其中:
- 观察者们需要实现
ApplicationListener<E extends ApplicationEvent>
接口,这是抽象观察者。 - 抽象目标(或者叫做抽象的消息发布者)是
ApplicationEventPublisherAware
接口 - Spring观察者模式发布事件的代码都在
ApplicationEventPublisher
类中,所以我们生成的具体目标(或者叫做具体的消息发布者)没必要自己编写代码,直接调用ApplicationEventPublisher
的publishEvent
方法即可。 - Spring中的事件要继承
ApplicationEvent
类,即观察者模式中的主题,可以看做一个普通的bean类,用于保存在事件监听器的业务逻辑中需要的一些字段;
发布事件之后,在Spring的ApplicationEventPublisher
的底层,SimpleApplicationEventMulticater
从容器中获取所有的监听器列表,遍历列表,对每个监听器分别执行invokeListener
方法,紧接着它会调用一个doInvokeListener
方法,该方法就会调用ApplicationListener
的onApplicationEvent
方法。
【案例】:
1 | //抽象目标,也是抽象的消息的发布者 |
1 | 具体目标发生改变... |
3 责任链模式
【介绍】:
责任链(Chain of Responsibility)模式,是为了避免请求发送者与多个请求接收者耦合在一起,于是将所有请求的接收者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
在这种模式中,通常每个接收者都包含对另一个接收者的引用。如果一个对象不能处理该请求,那么它会把相同的请求传给下一个接收者,依此类推。
在责任链模式中,客户只需要将请求发送到责任链上即可,无须关心请求的处理细节和请求的传递过程,请求会自动进行传递。所以责任链将请求的发送者和请求的处理者解耦了。
通常情况下,可以通过数据链表来实现职责链模式的数据结构。
【比喻】:
在现实生活中,一个事件需要经过多个对象处理是很常见的场景。例如,公司员工请假,可批假的领导有部门负责人、副总经理、总经理等,但每个领导能批准的天数不同,员工必须根据需要请假的天数去找不同的领导签名,也就是说员工必须记住每个领导的姓名、电话和地址等信息。
【优点】:
- 降低了对象之间的耦合度。该模式使得一个对象无须知道到底是哪一个对象处理其请求以及链的结构,发送者和接收者也无须拥有对方的明确信息。
- 增强了系统的可扩展性。可以根据需要增加新的请求处理类,满足开闭原则。
- 增强了给对象指派职责的灵活性。当工作流程发生变化,可以动态地改变链内的成员或者调动它们的次序,也可动态地新增或者删除责任。
- 责任链简化了对象之间的连接。每个对象只需保持一个指向其后继者的引用,不需保持其他所有处理者的引用,这避免了使用众多的 if 或者 if···else 语句。
- 责任分担。每个类只需要处理自己该处理的工作,不该处理的传递给下一个对象完成,明确各类的责任范围,符合类的单一职责原则。
【缺点】:
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的责任链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 责任链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
【应用】:
- Apache Tomcat对Encoding的处理
- Struts2的拦截器
- jsp servlet的Filter。
- Spring中的过滤器ApplicationFilterChain。
【案例】:
1 | //抽象处理者角色 |
4 模板模式
【介绍】:
模板模式(Template Pattern)定义了一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。它是一种类行为型模式。
【比喻】:
例如,去银行办理业务一般要经过以下4个流程:取号、排队、办理具体业务、对银行工作人员进行评分等。
其中取号、排队和对银行工作人员进行评分的业务对每个客户是一样的,可以在父类中实现,但是办理具体业务却因人而异,它可能是存款、取款或者转账等,可以延迟到子类中实现。
这样的例子在生活中还有很多,例如,一个人每天会起床、吃饭、做事、睡觉等,其中“做事”的内容每天可能不同。我们把这些规定了流程或格式的实例定义成模板,允许使用者根据自己的需求去更新它,例如,简历模板、论文模板等。
【优点】:
- 它封装了不变部分,扩展可变部分。它把认为是不变部分的算法封装到父类中实现,而把可变部分算法由子类继承实现,便于子类继续扩展。
- 它在父类中提取了公共的部分代码,便于代码复用。
- 部分方法是由子类实现的,因此子类可以通过扩展方式增加相应的功能,符合开闭原则。
【缺点】:
- 对每个不同的实现都需要定义一个子类,这会导致类的个数增加,系统更加庞大,设计也更加抽象,间接地增加了系统实现的复杂度。
- 父类中的抽象方法由子类实现,子类执行的结果会影响父类的结果,这导致一种反向的控制结构,它提高了代码阅读的难度。
- 由于继承关系自身的缺点,如果父类添加新的抽象方法,则所有子类都要改一遍。
【应用】:
- Java Servlet中,HttpServlet这个类就是一个抽象的模板类,它定义了doGet,doPost,doHead,doDelete等一系列的抽象方法,并在service方法中规定了前面这些方法的执行顺序和条件,形成了http访问的模板。我们定义的新的servlet子类,只需要继承HttpServlet,并实现doGet,doPost等方法即可。
- Mybatis中,BaseExecutor定义了数据库操作的基本模板:doUpdate()方法、doQuery()方法、doQueryCursor()方法、doFlushStatement()方法。继承BaseExecutor的子类只需要实现四个基本方法来完成数据库的相关操作即可。
- SpringBoot为用户封装了很多继承代码,都用到了模板方式,例如那一堆XXXtemplate。
【案例】:
1 | //抽象类 |
1 | 抽象类中的具体方法被调用... |
5 状态模式
【介绍】:
状态模式(State Pattern)对有状态的对象,把复杂的“判断逻辑”提取到不同的状态对象中,允许状态对象在其内部状态发生改变时改变其行为。
在软件开发过程中,应用程序中的部分对象可能会根据不同的情况做出不同的行为,我们把这种对象称为有状态的对象,而把影响对象行为的一个或多个动态变化的属性称为状态。
当有状态的对象与外部事件产生互动时,其内部状态就会发生改变,从而使其行为也发生改变。
对这种有状态的对象编程,传统的解决方案是:将这些所有可能发生的情况全都考虑到,然后使用if-else或switch-case语句来做状态判断,再进行不同情况的处理。但是显然这种做法对复杂的状态判断存在天然弊端,条件判断语句会过于臃肿,可读性差,且不具备扩展性,维护难度也大。
以上问题如果采用“状态模式”就能很好地得到解决。状态模式的解决思想是:当控制一个对象状态转换的条件表达式过于复杂时,把相关“判断逻辑”提取出来,用各个不同的类进行表示,系统处于哪种情况,直接使用相应的状态类对象进行处理,这样能把原来复杂的逻辑判断简单化,消除了 if-else、switch-case 等冗余语句,代码更有层次性,并且具备良好的扩展力。
【比喻】:
例如人都有高兴和伤心的不同状态,不同的状态有不同的行为,将不同的状态及其对应的行为封装成独立的状态对象,这样就可以根据情绪表现出不同的行为,同时不同的行为也会反馈自己切换成不同的状态。
【优点】:
- 结构清晰,状态模式将与特定状态相关的行为局部化到一个状态中,并且将不同状态的行为分割开来,满足“单一职责原则”。
- 将状态转换显示化,减少对象间的相互依赖。将不同的状态引入独立的对象中会使得状态转换变得更加明确,且减少对象间的相互依赖。
- 状态类职责明确,有利于程序的扩展。通过定义新的子类很容易地增加新的状态和转换。
【缺点】:
- 状态模式的使用必然会增加系统的类与对象的个数。
- 状态模式的结构与实现都较为复杂,如果使用不当会导致程序结构和代码的混乱。
- 状态模式对开闭原则的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源码,否则无法切换到新增状态,而且修改某个状态类的行为也需要修改对应类的源码。
【应用】:
Spring中的状态机stateMachine。
【案例】:
1 | //环境类 |
输出
1 | 当前状态是 A. |
状态模式和策略模式看起来很像,UML图都很像,但其实含义不一样。状态模式重点在各状态之间的切换从而做不同的事情,而策略模式更侧重于根据具体情况选择不同策略,并不涉及切换,策略之间是完全独立的。同时,在状态模式中,每个状态通过持有Context的引用,来实现状态转移;但是每个策略都不持有Context的引用,它们只是被Context使用。
6 迭代器模式
【介绍】:
迭代器(Iterator Pattern)模式提供一个对象来顺序访问集合对象中的一系列数据,它在客户访问类与集合类之间插入一个迭代器,这分离了集合对象与其遍历行为,对客户也隐藏了其内部细节而不暴露集合对象的内部表示。
例如Java中的Collection、List、Set、Map等都包含了迭代器。在日常开发中,我们几乎不会自己写迭代器。除非需要定制一个自己实现的数据结构对应的迭代器,否则,开源框架提供的API完全够用。
【比喻】:
比如:物流系统中的传送带,不管传送的是什么物品,都会被打包成一个个箱子,并且有一个统一的二维码。这样我们不需要关心箱子里是什么,在分发时只需要一个个检查发送的目的地即可。
比如,我们平时乘坐交通工具,上车的队列,都是统一刷卡或者刷脸进站,而不需要关心是男性还是女性、是残疾人还是正常人等信息。
【优点】:
- 访问一个集合对象的内容而无须暴露它的内部表示。
- 遍历任务交由迭代器完成,这简化了聚合类。
- 它支持以不同方式遍历一个集合,甚至可以自定义迭代器的子类以支持新的遍历。
- 增加新的集合类和迭代器类都很方便,无须修改原有代码。
- 封装性良好,为遍历不同的集合结构提供一个统一的接口。
【缺点】:
增加了类的个数,这在一定程度上增加了系统的复杂性。
【应用】:
Java中的Collection、List、Set、Map等都包含了迭代器。
【案例】:
1 | //抽象集合 |
1 | 聚合的内容有:中山大学 华南理工 韶关学院 |
7 命令模式
【介绍】:
命令(Command Pattern)模式将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。这样两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。
在命令对象内部持有处理该命令的接受者,这样每个命令和其接受者的关系就得到了绑定。
通过把命令封装为一个对象,命令发送者把命令对象发出后,就不去管是谁来接受处理这个命令,命令接受者接受到命令对象后进行处理,也不用管命令是谁发出的,所以命令模式实现了发送者与接受者之间的解耦,而具体把命令发送给谁还需要一个控制器。
【比喻】:
在现实生活中,命令模式的例子也很多。比如看电视时,我们只需要轻轻一按遥控器就能完成频道的切换,这就是命令模式,将换台请求和换台处理完全解耦了。电视机遥控器(命令发送者)通过按钮(具体命令)来遥控电视机(命令接收者)。而对于电视机遥控器来说,它只能操控电视,它操控的对象已经和遥控器绑定了,我们不管里面的逻辑。
对于用户来说,我们想看电视,就只管找电视遥控器,不关心电视遥控器是如何打开电视的,想开空调,就只管找空调遥控器,以此类推。
同样的,电视作为接受者,也不关心是谁打开了它,它只和遥控器绑定,如果哪天电视要升级改版,也和发送者没关系。
【优点】:
- 通过引入中间件(抽象接口)降低系统的耦合度。
- 扩展性良好,增加或删除命令非常方便。采用命令模式增加与删除命令不会影响其他类,且满足“开闭原则”。
- 可以实现宏命令。命令模式可以与组合模式结合,将多个命令装配成一个组合命令,即宏命令。
- 方便实现Undo和Redo操作。命令模式可以与后面介绍的备忘录模式结合,实现命令的撤销与恢复。
- 可以在现有命令的基础上,增加额外功能。比如日志记录,结合装饰器模式会更加灵活。
【缺点】:
- 可能产生大量具体的命令类。因为每一个具体操作都需要设计一个具体命令类,这会增加系统的复杂性。
- 命令模式的结果其实就是接收方的执行结果,但是为了以命令的形式进行架构、解耦请求与实现,引入了额外类型结构(引入了请求方与抽象命令接口),增加了理解上的困难。不过这也是设计模式的通病,抽象必然会额外增加类的数量,代码抽离肯定比代码聚合更加难理解。
【应用】:
Tomcat作为一个服务器本身会接受外部大量请求,当一个请求过来后tomcat根据域名去找对应的host,找到host后会根据应用名去找具体的context(应用),然后具体应用处理请求。
Tomcat中的Connector作为命令发出者,Connector接受到请求后把请求内容封装为request对象(命令对象),然后使用CoyoteAdapter作为分发器把请求具体发配到具体的host,host再根据request对象找到具体的context,至此找到了具体的应用,交给具体应用处理。
这就实现了:对于具体host来说他不关心这个请求是谁给的,对于Connector来说他也不必关心谁来处理,但是两者是通过request封装请求对象进行关联起来。
【案例】:
1 | //调用者 |
8 备忘录模式
【介绍】:
备忘录(Memento Pattern)模式在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便以后当需要时能将该对象恢复到原先保存的状态。该模式又叫快照模式。
其实很多应用软件都提供了这项功能,如Word、记事本、Photoshop、Eclipse等软件在编辑时按Ctrl+Z组合键时能撤销当前操作,使文档恢复到之前的状态;
备忘录模式能记录一个对象的内部状态,当用户后悔时能撤销当前操作,使数据恢复到它原先的状态。
【比喻】:
每个人都有犯错误的时候,都希望有种“后悔药”能弥补自己的过失,让自己重新开始,但现实是残酷的。在计算机应用中,客户同样会常常犯错误,能否提供“后悔药”给他们呢?当然是可以的,而且是有必要的。这个功能由备忘录模式来实现。
【优点】:
- 提供了一种可以恢复状态的机制。当用户需要时能够比较方便地将数据恢复到某个历史的状态。
- 实现了内部状态的封装。除了创建它的发起人之外,其他对象都不能够访问这些状态信息。
- 简化了发起人类。发起人不需要管理和保存其内部状态的各个备份,所有状态信息都保存在备忘录中,并由管理者进行管理,这符合单一职责原则。
【缺点】:
资源消耗大。如果要保存的内部状态信息过多或者特别频繁,将会占用比较大的内存资源。
【应用】:
spring-webflow中的StateManageableMessageContext类,就才用了备忘录模式,它接口中定义了createMessagesMemento()方法,其实现类DefaultMessageContext有其默认实现:
1 | private Map<Object, List<Message>> sourceMessages; |
【案例】:
备忘录模式使用三个类 Memento、Originator和CareTaker。
Memento用来存储要被恢复的对象的状态。Originator创建并在Memento对象中存储状态。Caretaker对象是Memento的管理者,负责管理存储多版本的Memento,以及从Memento中恢复对象的状态。
1 | public class Memento { |
1 | Current State: State #4 |
9 访问者模式
【介绍】:
访问者模式(Visitor Pattern)将作用于集合类中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式。
比较难理解?我们用商场的商品来比喻一下。
【比喻】:
比如说在商场购物时放在购物车中的商品,购物车就是集合类,商品是元素(可能是不同类型),那么我们知道,不同的访问者,对于商品的操作是不一样的。收银员对商品的操作是计价,而顾客对商品的操作是使用。
常规情况下我们会在商品类中定义settle()方法用来计价,定义use()方法用来使用,但假如我们现在新增了一类访问者呢?假如新增了一类质检员,对商品进行质检,难道我们还要将每个商品类都新增check()方法吗?后面如果再来一类访问者呢?
访问者模式就是为了解决这种痛点应运而生的。
【优点】:
- 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
- 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
- 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
- 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。
【缺点】:
- 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
- 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
- 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
【应用】:
Spring的BeanDefinitionVisitor类被设计用来访问BeanDefinition对象。PropertyPlaceholderConfigurer类会遍历得到的所有的BeanDefinition对象,依次调用visitor.visitBeanDefinition(bd)方法。不过目前Spring目前只有BeanDefinitionVisitor一个访问者类,但代码中已经保留了拓展性。
【案例】:
访问者模式包含以下主要角色。
- 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作visit(),该操作中的参数类型标识了被访问的具体元素。
- 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
- 抽象元素(Element)角色:声明一个包含接受操作accept()的接口,被接受的访问者对象作为accept()方法的参数。
- 具体元素(ConcreteElement)角色:实现抽象元素角色提供的accept()操作,其方法体通常都是visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
- 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由List、Set、Map等聚合类实现。
1 | //抽象访问者 |
1 | 具体访问者A访问,我是质检员,进行质检,元素A是罐头商品,打开罐头检查---->具体元素A的操作。我是罐头商品,打开罐头。 |
10 中介者模式
【介绍】:
中介者(Mediator Pattern)模式定义了一个中介对象来封装一系列对象之间的交互,使原有对象之间的耦合松散,且可以独立地改变它们之间的交互。
在现实生活中,常常会出现好多对象之间存在复杂的交互关系,这种交互关系常常是“网状结构”,它要求每个对象都必须知道它需要交互的对象。例如,每个人必须记住他(她)所有朋友的电话;而且,朋友中如果有人的电话修改了,他(她)必须让其他所有的朋友一起修改,这叫作“牵一发而动全身”,非常复杂。
如果把这种“网状结构”改为“星形结构”的话,将大大降低它们之间的“耦合性”,这时只要找一个“中介者”就可以了。如前面所说的“每个人必须记住所有朋友电话”的问题,只要在网上建立一个每个朋友都可以访问的“通信录”就解决了。
【比喻】:
例如,你想租房,可以找房产中介,房产中介那里有许多的房源信息。
例如,多个用户可以向聊天室(中介类)发送消息,聊天室向所有的用户显示消息。
【优点】:
- 类之间各司其职,符合迪米特法则。
- 降低了对象之间的耦合性,使得对象易于独立地被复用。
- 将对象间的一对多关联转变为一对一的关联,提高系统的灵活性,使得系统易于维护和扩展。
【缺点】:
中介者模式将原本多个对象直接的相互依赖变成了中介者和多个同事类的依赖关系。当同事类越多时,中介者就会越臃肿,变得复杂且难以维护。
【应用】:
在各种的MVC框架中,其中C(控制器)就是M(模型)和V(视图)的中介者。
【案例】:
中介者模式包含以下主要角色。
- 抽象中介者(Mediator)角色:它是中介者的接口,提供了同事对象注册与转发同事对象信息的抽象方法。
- 具体中介者(Concrete Mediator)角色:实现中介者接口,定义一个List来管理同事对象,协调各个同事角色之间的交互关系,因此它依赖于同事角色。
- 抽象同事类(Colleague)角色:定义同事类的接口,保存中介者对象,提供同事对象交互的抽象方法,实现所有相互影响的同事类的公共功能。
- 具体同事类(Concrete Colleague)角色:是抽象同事类的实现者,当需要与其他同事对象交互时,由中介者对象负责后续的交互。
1 | //抽象中介者 |
1 | 具体同事类1发出请求。 |
11 解释器模式
【介绍】:
解释器(Interpreter Pattern)模式给分析对象定义一个语言,并定义该语言的文法表示,再设计一个解析器来解释语言中的句子。也就是说,用编译语言的方式来分析应用中的实例。这种模式实现了文法表达式处理的接口,该接口解释一个特定的上下文。
这种模式实现了一个表达式接口,该接口解释一个特定的上下文。这种模式被用在SQL解析、符号处理引擎等。
在项目开发中,如果要对数据表达式进行分析与计算,无须再用解释器模式进行设计了,Java提供了以下强大的数学公式解析器:Expression4J、MESP(Math Expression String Parser)和Jep等,它们可以解释一些复杂的文法,功能强大,使用简单。
【比喻】:
【优点】:
- 扩展性好。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法。
- 容易实现。在语法树中的每个表达式节点类都是相似的,所以实现其文法较为容易。
【缺点】:
- 执行效率较低。解释器模式中通常使用大量的循环和递归调用,当要解释的句子较复杂时,其运行速度很慢,且代码的调试过程也比较麻烦。
- 会引起类膨胀。解释器模式中的每条规则至少需要定义一个类,当包含的文法规则很多时,类的个数将急剧增加,导致系统难以管理与维护。
- 可应用的场景比较少。在软件开发中,需要定义语言文法的应用实例非常少,所以这种模式很少被使用到。
【应用】:
用于SQL语句的解析。
【案例】:
假如“韶粵通”公交车读卡器可以判断乘客的身份,如果是“韶关”或者“广州”的“老人” “妇女”“儿童”就可以免费乘车,其他人员乘车一次扣2元。
然后,根据文法规则按以下步骤设计公交车卡的读卡器程序的类图。
- 定义一个抽象表达式(Expression)接口,它包含了解释方法interpret(String info)。
- 定义一个终结符表达式(Terminal Expression)类,它用集合(Set)类来保存满足条件的城市或人,并实现抽象表达式接口中的解释方法 interpret(Stringinfo),用来判断被分析的字符串是否是集合中的终结符。
- 定义一个非终结符表达式(AndExpressicm)类,它也是抽象表达式的子类,它包含满足条件的城市的终结符表达式对象和满足条件的人员的终结符表达式对象,并实现 interpret(String info) 方法,用来判断被分析的字符串是否是满足条件的城市中的满足条件的人员。
- 最后,定义一个环境(Context)类,它包含解释器需要的数据,完成对终结符表达式的初始化,并定义一个方法 freeRide(String info) 调用表达式对象的解释方法来对被分析的字符串进行解释。
1 | //抽象表达式类 |
1 | 您是韶关的老人,您本次乘车免费! |