Java设计模式之行为型模式

行为型模式

策略模式【strategy】(接口主要)

【学习难度:★☆☆☆☆,使用频率:★★★★☆】

定义:策略模式定义了一系列算法,并将每个算法封装起来,使他们可以相互替换,且算法的变化不会影响到使用算法的客户。需要设计一个接口,为一系列实现类提供统一的方法,多个实现类实现该接口,设计一个抽象类(可有可无,属于辅助类),提供辅助函数。

策略模式

本质:接口提供具体需要实现的方法或者功能,抽象类提供辅助通用的方法(可有可无)。接口作为向外提供的策略或者算法功能,起主要作用。

当然,上图也可以转化为下图:

策略模式

经验总结:将通用不变(每个子类都需要)的共性方法放到抽象类中,具体特殊(不是每个子类都需要)的异性方法放到接口中。即:不变继承,变接口。

使用案例:网络请求缓存策略。


模板模式【Template Method】(抽象类主要)

【学习难度:★★☆☆☆,使用频率:★★★☆☆】

定义:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。TemplateMethod使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

模板模式

在抽象模板类里面,对于子类模板共用的方法定义为实方法,作为模板的主干(主流程方法);对于每个子类模板需要自己实现的不同方法则定义为抽象方法。

模板方法和策略模式的区别:

(1)模式方法注重流程。模式方法更加侧重于业务流程相对复杂且稳定,而其中的某些步骤(局部流程算法)变化相对剧烈的场景。

(2)策略模式注重行为变化。策略模式则是偏重于算法本身(整个算法)就变化相对剧烈的情形。

模板方法模式注重灵活性,策略模式注重适应性。

总结:

模板方法和策略模式都是解决算法多样性对代码结构冲击的问题。模板方法使用于业务场景相对复杂且稳定的情况,策略模式使用于算法相对多样灵活的场景。

当业务或算法相对简单时,策略模式和模板方法几乎等效,但是推荐使用策略模式(策略模式适应性强)。

使用案例:各种Base类。Android中Activity的生命周期等.


观察者模式【Observer】

【学习难度:★★★☆☆,使用频率:★★★★★】

定义:一个目标物件管理所有相依于它的观察者物件,并且在它本身的状态改变时主动发出通知。这通常透过呼叫各观察者所提供的方法来实现。此种模式通常被用来实现事件处理系统。

观察者模式

ISubject是被观察者/被订阅者/事件产生者/作者,IObserver是观察者/订阅者/事件消费者/读者。

ISubject持有一个IObserver的集合,当ISubject自身发生变化的时候,会遍历这个IObserver集合,将变化一一通知每个IObserver。

使用案例:RxBus、消息推送队列、Adapter的notifyDataSetChanged()、RxJava、Android控件格式各样的监听器。


迭代器模式【Iterator】

【学习难度:★★★☆☆,使用频率:★★★★★】

定义:提供一种方法顺序访问一个聚合对象中的各种元素,而又不暴露该对象的内部表示。

迭代器模式

迭代器模式应用的场景及意义:

(1)访问一个聚合对象的内容而无需暴露它的内部表示。

(2)支持对聚合对象的多种遍历。

(3)为遍历不同的聚合结构提供一个统一的接口。

使用案例:Collection,List、Set、Map等集合的迭代器

一般来说,迭代器模式是与集合共生共死的,关系非常密切,所以大多数语言在实现容器的时候都给提供了迭代器,并且这些语言提供的容器和迭代器在绝大多数情况下就可以满足我们的需要,所以迭代器模式我们使用的场景还是比较少的。


责任链模式【Chain of Responsibility】

【学习难度:★★★☆☆,使用频率:★★☆☆☆】

定义:在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。

请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。

责任链模式

Handler:作为处理者的动作,声明统一的事件处理方法;

AbstractHandler:作为抽象处理者,提供一个责任链后续连接的方法。

ConcreteHandler:作为具体处理者,处理它所负责的请求,可以访问后继者,如果可以处理请求则处理,否则将该请求转给他的后继者。

责任链的特点

责任链类似工厂里生产加工产品的流水线。对于每一个处理环节,只需要尽到自己的职责和知道其下一个环节是什么。整个请求并不知道具体的处理人是谁,也不知道请求何时处理结束。

举一个最简单的例子:

我们在提交报销单申请的时候,都会有一级、二级、三级审核人,一级审核人通过了需要交给二级审核人处理,二级审核人通过了需要交给三级审核人处理,以此类推,直到最后一级审核人通过了或者中间哪一级审核不通过了,整个申请流程才结束。这样申请的流程就形成了一条责任链。

职责链模式的优点:

1 ) 降低耦合度 :该模式使得一个对象无需知道是其他哪一个对象处理其请求。对象仅需知道该请求会被“正确”地处理。接收者和发送者都没有对方的明确的信息,且链中的对象不需知道链的结构。

  1. 职责链可简化对象的相互连接 。它们仅需保持一个指向其后继者的引用,而不需保持它所有的候选接受者的引用。

  2. 增强了给对象指派职责(Responsibility)的灵活性 :当在对象中分派职责时,职责链给你更多的灵活性。你可以通过在运行时刻对该链进行动态的增加或修改来增加或改变处理一个请求的那些职责。你可以将这种机制与静态的特例化处理对象的继承机制结合起来使用。

4)增加新的请求处理类很方便。

职责链模式的缺点:

  1. 不能保证请求一定被接收。既然一个请求没有明确的接收者,那么就不能保证它一定会被处理(该请求可能一直到链的末端都得不到处理)。一个请求也可能因该链中的某一环没有被正确配置而得不到处理。

  2. 由于请求是在责任链上传递,当责任链较长的时候,由于请求无法立即精确的找到处理者,处理效率会降低。

Ps:责任链模式常常会和组合模式一起使用。每一个子对象或者组合对象都可以分别承担不同的职责,从而形成一条责任链。

使用案例:View的事件分发机制。


命令模式【Command】

【学习难度:★★★☆☆,使用频率:★★★★☆】

定义:在面向对象程式设计的范畴中,命令模式(Command Pattern)是一种设计模式,它尝试以物件来代表实际行动。

一般来说,行为请求者与行为实现者通常是一种紧耦合的关系。行为请求者通过直接持有行为实现者,调用行为实现者的实现方法来实现请求。但是一旦行为实现者发生变化,行为请求者也不可避免地需要进行相应的修改。

命令模式就是通过在行为请求者与行为实现者中间引入中介者(命令),将行为请求者的行为动作抽象为对象(命令),行为请求者不直接操作行为实现者,而是通过操作命令间接操作行为实现者,从而实现二者之间的松耦合。

命令模式

在图中,Invoker是调用者(司令员),Receiver是被调用者(士兵),CommandA是命令,实现了Command接口,持有接收对象Receiver。

Invoker并不是直接持有被调用者Receiver,而是通过持有中间者命令,调用命令的执行方法去间接执行。这样即使被调用者发生了变化,我们只需变更命令的实现即可,调用者无需进行修改,这样就解除了调用者和被调用者之前的耦合关系。

其实这很好理解,我们可以将Invoker比喻为机关领导,Command比喻为行政命令或文件,Receiver比喻为下属机构或者基层公务员。领导只关心命令的执行,并不关心具体是谁去执行这条命令。

使用案例:MVP模式


7、备忘录模式【Memento】【学习难度:★★☆☆☆,使用频率:★★☆☆☆】

定义:在不破坏封闭的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。这样以后就可将该对象恢复到原先保存的状态。

备忘录模式

Originator:原发器。可以使用备忘录来保存某个时刻原发器自身的状态,也可以使用备忘录来恢复内部状态。

Memento:备忘录。主要用来存储原发器对象的内部状态,但是具体需要存储哪些数据是由原发器对象来决定的。另外备忘录应该只能由原发器对象来访问它内部的数据,原发器外部的对象不应该能访问到备忘录对象的内部数据。

Storage:备忘录管理者,或者称为备忘录存储者。主要负责保存备忘录对象,但是不能对备忘录对象的内容进行操作或检查。

备忘录模式的优点:

(1)更好的封装性。通过使用备忘录对象,来封装原发器对象的内部状态,虽然这个对象是保存在原发器对象的外部,但是由于备忘录对象的窄接口并不提供任何方法,这样有效的保证了对原发器对象内部状态的封装,不把原发器对象的内部实现细节暴露给外部。

(2)简化了原发器。备忘录对象负责保存原发器的状态,被保存到原发器对象之外,让Storage来管理他们请求的状态,使得原发器状态的保存和存储管理分离,从而使原发器对象得到简化。

备忘录模式的缺点:

可能会导致高开销。备忘录模式基本的功能,就是对备忘录对象的存储和恢复,它的基本实现方式就是缓存备忘录对象。这样一来,如果需要缓存的数据量很大,或者是特别频繁的创建备忘录对象,开销是很大的。

与备忘录模式可组合使用的模式:

(1)命令模式

命令模式实现中,在实现命令的撤销和重做的时候,可以使用备忘录模式,在命令操作的时候记录下操作前后的状态,然后在命令撤销和重做的时候,直接使用相应的备忘录对象来恢复状态就可以了。
在这种撤销的执行顺序和重做执行顺序可控的情况下,备忘录对象还可以采用增量式记录的方式,可以减少缓存的数据量。

(2)原型模式

在原发器对象创建备忘录对象的时候,如果原发器对象中全部或者大部分的状态都需要保存,一个简洁的方式就是直接克隆一个原发器对象。也就是说,这个时候备忘录对象里面存放的是一个原发器对象的实例。

使用案例:撤销操作、游戏存档、棋类悔棋、Canvas的save()和restore()、Activity的onSaveInstanceState和onRestoreInstanceState 。(适合功能比较复杂的,但需要维护或记录属性历史的功能。)


状态模式【State】

【学习难度:★★★☆☆,使用频率:★★★☆☆】

定义:允许一个对象在其内部状态改变时改变它的行为。对象看起来似乎修改了它的类。

核心思想是:当对象的状态改变时,同时改变其行为。

主要解决的问题:当控制一个对象状态转换的条件表达式过于复杂时的情况。把状态的判断逻辑转移到表示不同的一系列类当中,可以把复杂的逻辑判断简单化。

状态模式

上下文环境(Context):它定义了客户程序需要的接口并维护一个具体状态角色的实例,将与状态相关的操作委托给当前的Concrete State对象来处理,并提供状态切换的方法。

抽象状态(AbstractState):定义一个接口以封装使用上下文环境的一个特定状态相关的行为。

具体状态(Concrete State):实现抽象状态定义的接口以及状态切换的规则。

状态模式的特点:

状态模式描述了对象状态的变化以及对象如何在每一种状态下表现出不同的行为。

状态模式的关键是引入了一个抽象类来专门表示对象的状态,这个类我们叫做抽象状态类AbstractState,而对象的每一种具体状态类都继承了该类,并在不同具体状态类中实现了不同状态的行为,包括各种状态之间的转换。

状态模式的优点:

(1)封装了转换规则。

(2)将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。

(3)允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。

状态模式的缺点:

(1)状态模式的使用必然会增加系统类和对象的个数。

(2)状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。

(3)状态模式对“开闭原则”的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态;而且修改某个状态类的行为也需修改对应类的源代码。

状态模式和策略模式的区别:

(1)状态模式突出的是“状态的变化”,由状态的变化去改变行为,并且状态的变化是发生在系统内部的且有转换规则的,外部无法自由地选择状态。

(2)策略模式中多个策略之间是独立的,无关联的,外部可以自由选择执行的策略。

使用案例:登录状态控制、WiFi状态、Android中的StateMachine机制。


访问者模式【Visitor】

【学习难度:★★★★☆,使用频率:★☆☆☆☆】

定义:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。

简单来说,就是可以在原有类不修改的条件下,轻松增加算法操作。

访问者模式就是一种分离对象数据结构与行为的方法,通过这种分离,可达到为一个被访问者动态添加新的操作而无需做其它的修改的效果。

访问者模式的主要缺点是增加新的数据结构很困难,因此它适用于数据结构相对稳定的系统,能够将数据结构和算法解耦。

访问者模式

Visitor:接口或者抽象类。它定义了对每一个元素的访问行为,它的参数就是可以访问的元素。

ConcreteVisitor:具体访问者。对每一个元素类访问的具体行为(算法)。

Element:元素接口或者抽象类。定义了一个accept方法,每一个元素都要可以被访问者访问。

ConcreteElement:具体元素类。它提供了接受访问的具体方法实现。

ObjectStructure:对象数据结构。内部管理元素集合,并且可以迭代这些元素供访问者访问。

Visitor提供算法,Element提供算法数据,ObjectStructure管理Element。

访问者模式的优点:
1.各角色职责分离,符合单一职责的原则 。
2.具有优秀的扩展性 和灵活性。
3.使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化 。

访问者模式的缺点:
1.具体元素对访问者公布细节,违反了“最少知道”的原则
2.具体元素(数据结构)变更时导致修改成本变大 。

访问者模式的使用场景:
1.对象结构比较稳定,但经常需要在此对象结构上定义新的操作.
2.需要对一个对象结构中的对象进行很多不同的并且不相关的操作,它可以在不改变这个数据结构的前提下定义作用于这些元素的新的操作(即访问者)。

访问者模式可以和组合模式组合在一起使用。组合模式用于组织元素,访问者模式用于元素访问处理。

使用案例:药房抓药开药、OA系统员工数据统一以及绩效考评。


中介者模式【Mediator】

【学习难度:★★★☆☆,使用频率:★★☆☆☆】

定义:定义一个中介对象来封装系列对象之间的交互。中介者使各个对象不需要显示地相互引用,从而使其耦合性松散,而且可以独立地改变他们之间的交互。

中介者模式

Mediator:中介者接口。在里面定义了各个同事之间相互交互所需要的方法,可以是公共的方法,也可以是小范围的交互方法。

ConcreteMediator:具体的中介者实现对象。它需要了解并为维护每个同事对象,并负责具体的协调各个同事对象的交互关系。

Colleague:同事类的定义,通常实现成为抽象类。主要负责约束同事对象的类型,并实现一些具体同事类之间的公共功能,比如,每个具体同事类都应该知道中介者对象,也就是每个同事对象都会持有中介者对象的引用,这个功能可定义在这个类中。

ConcreteColleague:具体的同事类。实现自己的业务,需要与其他同事对象交互时,就通知中介对象,中介对象会负责后续的交互。

中介者模式将一个网状的系统结构变成一个以中介者对象为中心的星形结构,在这个星型结构中,使用中介者对象与其他对象的一对多关系来取代原有对象之间的多对多关系。

中介者模式的优点:

(1)中介者模式简化了对象之间的交互,它用中介者和同事的一对多交互代替了原来同事之间的多对多交互,一对多关系更容易理解、维护和扩展,将原本难以理解的网状结构转换成相对简单的星型结构。

(2) 中介者模式可将各同事对象解耦。中介者有利于各同事之间的松耦合,我们可以独立的改变和复用每一个同事和中介者,增加新的中介者和新的同事类都比较方便,更好地符合“开闭原则”。

(3)可以减少子类生成,增加同事类的复用性。中介者将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成新的中介者子类即可,这使各个同事类可被重用,无须对同事类进行扩展。

中介者模式的缺点:

(1)在具体中介者类中包含了大量同事之间的交互细节,可能会导致具体中介者类非常复杂,使得系统难以维护。

(2)由于“中介”承担了较多的责任,所以一旦这个中介对象出现了问题,那么整个系统就会受到重大的影响。

中介者模式的适用场景:

(1)系统中对象之间存在复杂的引用关系,系统结构混乱且难以理解。

(2) 一个对象由于引用了其他很多对象并且直接和这些对象通信,导致难以复用该对象。

(3)想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。可以通过引入中介者类来实现,在中介者中定义对象交互的公共行为,如果需要改变行为则可以增加新的具体中介者类。

中介者模式和外观模式的区别:

(1)中介者模式是行为型模式,外观模式是结构型模式。

(2)中介者模式是将多对多的相互作用转化为一对多的相互作用(也就是各个系统可以互相作用,注意这里强调的是各个系统之间的作用)。

而外观模式模式则是提供一个高层次的接口,使得子系统更易于使用(注意这里强调的是为了让外部更容易使用这些子系统,所以统一放在一个对象中)。

(3)中介者模式中所有的请求都是由中介者协调同事类和中介者本身共同作用完成业务,而外观模式则将处理都委托给子系统完成。

(4)中介者模式是用一个中介对象来封装一系列同事对象的交互行为,而外观模式则是为外部提供一个统一的接口方便调用。


解释器模式【Interpreter】

【学习难度:★★★★★,使用频率:★☆☆☆☆】

定义:给定一个语言,定义它的文法的一种表示,并定义一个解释器,这个解释器使用该表示来解释语言中的句子。这里的“语言”是指使用规定格式和语法的代码。

解释器模式1
解释器模式2

(1)抽象表达式(Expression)角色:声明一个所有的具体表达式角色都需要实现的抽象接口。这个接口主要是一个interpret()方法,称做解释操作。

(2)终结符表达式(Terminal Expression)角色:实现了抽象表达式角色所要求的接口,主要是一个interpret()方法;文法中的每一个终结符都有一个具体终结表达式与之相对应。比如有一个简单的公式R=R1+R2,在里面R1和R2就是终结符,对应的解析R1和R2的解释器就是终结符表达式。

(3)非终结符表达式(Nonterminal Expression)角色:文法中的每一条规则都需要一个具体的非终结符表达式,非终结符表达式一般是文法中的运算符或者其他关键字,比如公式R=R1+R2中,“+"就是非终结符,解析“+”的解释器就是一个非终结符表达式。

(4)环境(Context)角色:这个角色的任务一般是用来存放文法中各个终结符所对应的具体值,比如R=R1+R2,我们给R1赋值100,给R2赋值200。这些信息需要存放到环境角色中,很多情况下我们使用Map来充当环境角色就足够了。

使用案例:自定义简单的语言


项目源码

Github项目源码地址:https://github.com/xuexiangjys/architect-java/tree/master/src/designpattern/behavior, 觉得有帮助的话记得给个star!~~

微信公众号

更多资讯内容,欢迎扫描关注我的个人微信公众号!

微信公众号

推荐阅读更多精彩内容