设计模式| 行为型模式 (上)

前言

行为型模式共十一种:策略模式、模板方法模式、观察者模式、迭代器模式、解释器模式、责任链模式、命令模式、备忘录模式、状态模式、访问者模式、中介者模式。 分两篇文章总结,本篇主要涉及到的设计模式是:

策略模式、模板方法模式、观察者模式、迭代器模式、解释器模式、责任链模式。

其他同系列的文章还有: 面向对象编程中的六大原则 设计模式| 创建型模式 设计模式| 结构型模式 设计模式| 行为型模式 (上) 设计模式| 行为型模式 (下) 欢迎阅读,评论!!!

1、策略模式

算法的封装与切换-策略模式

生活中,对一个问题解决,有多种方案可以选择。比如出行旅游去某地,要选择交通工具的问题上。

- 如果没有时间但是不在乎钱,可以选择坐飞机。
- 如果没有钱,可以选择坐大巴或者火车。
- 如果再穷一点,可以选择骑自行车。

组织架构好这些灵活多样的算法(选择),而且可以随意互相替换。这种解决方案就是策略模式。

策略模式(Strategy Pattern):定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。

 在策略模式结构图中包含如下几个角色:
  ● Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。
    在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。

  ● Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。
    环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。

  ● ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,
    具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。

1.策略模式的优缺点

策略模式的主要优点有:

策略类之间可以自由切换,由于策略类实现自同一个抽象,所以他们之间可以自由切换。

易于扩展,增加一个新的策略对策略模式来说非常容易,基本上可以在不改变原有代码的基础上进行扩展。

避免使用多重条件,如果不使用策略模式,对于所有的算法,必须使用条件语句进行连接,通过条件判断来决定使用哪一种算法,
在上一篇文章中我们已经提到,使用多重条件判断是非常不容易维护的。

2.策略模式的缺点主要有两个:

 维护各个策略类会给开发带来额外开销,可能大家在这方面都有经验:一般来说,策略类的数量超过5个,就比较令人头疼了。

必须对客户端(调用者)暴露所有的策略类,因为使用哪种策略是由客户端来决定的,
因此,客户端应该知道有什么策略,并且了解各种策略之间的区别,否则,后果很严重。
例如,有一个排序算法的策略模式,提供了快速排序、冒泡排序、选择排序这三种算法,客户端在使用这些算法之前,
是不是先要明白这三种算法的适用情况?再比如,客户端要使用一个容器,有链表实现的,也有数组实现的,
客户端是不是也要明白链表和数组有什么区别?就这一点来说是有悖于迪米特法则的。

适用场景

做面向对象设计的,对策略模式一定很熟悉,因为它实质上就是面向对象中的继承和多态,在看完策略模式的通用代码后,
我想,即使之前从来没有听说过策略模式,在开发过程中也一定使用过它吧?至少在在以下两种情况下,大家可以考虑使用策略模式,

  A.几个类的主要逻辑相同,只在部分逻辑的算法和行为上稍有区别的情况。
  B.有几种相似的行为,或者说算法,客户端需要动态地决定使用哪一种,那么可以使用策略模式,将这些算法封装起来供客户端调用。
   
策略模式是一种简单常用的模式,我们在进行开发的时候,会经常有意无意地使用它,一般来说,
策略模式不会单独使用,跟模版方法模式、工厂模式等混合使用的情况比较多。

2、模板方法模式

在现实生活中,很多事情都包含几个实现步骤,例如请客吃饭,无论吃什么,一般都包含点单、吃东西、买单等几个步骤,
通常情况下这几个步骤的次序是:点单 --> 吃东西 --> 买单。在这三个步骤中,点单和买单大同小异,
最大的区别在于第二步——吃什么?吃面条和吃满汉全席可大不相同,

在软件开发中,有时也会遇到类似的情况,某个方法的实现需要多个步骤(类似“请客”),其中有些步骤是固定的(类似“点单”和“买单”)
而有些步骤并不固定,存在可变性(类似“吃东西”)。为了提高代码的复用性和系统的灵活性,
可以使用一种称之为模板方法模式的设计模式来对这类情况进行设计,
在模板方法模式中,将实现功能的每一个步骤所对应的方法称为基本方法(例如“点单”、“吃东西”和“买单”),
而调用这些基本方法同时定义基本方法的执行次序的方法称为模板方法(例如“请客”)。

在模板方法模式中,可以将相同的代码放在父类中,例如将模板方法“请客”以及基本方法“点单”和“买单”的实现放在父类中,而对于基本方法“吃东西”,在父类中只做一个声明,将其具体实现放在不同的子类中,

在一个子类中提供“吃面条”的实现,而另一个子类提供“吃满汉全席”的实现。
通过使用模板方法模式,一方面提高了代码的复用性,另一方面还可以利用面向对象的多态性,在运行时选择一种具体子类,
实现完整的“请客”方法,提高系统的灵活性和可扩展性。

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

  大部分刚步入职场的毕业生应该都有类似这样的经历。一个复杂的任务,
  由公司中的牛人们将主要的逻辑写好,然后把那些看上去比较简单的方法写成抽象的,交给其他的同事去开发。
  这种分工方式在编程人员水平层次比较明显的公司中经常用到。
  比如一个项目组,有架构师,高级工程师,初级工程师,则一般由架构师使用大量的接口、抽象类将整个系统的逻辑串起来,
  实现的编码则根据难度的不同分别交给高级工程师和初级工程师来完成。这就是典型的用到了模版方法模式。

模版方法模式的结构

 模版方法模式由一个抽象类和一个(或一组)实现类通过继承结构组成,抽象类中的方法分为三种:

 1. 抽象方法:父类中只声明但不加以实现,而是定义好规范,然后由它的子类去实现。
 2.  模版方法:由抽象类声明并加以实现。一般来说,模版方法调用抽象方法来完成主要的逻辑功能,
            并且模版方法大多会定义为final类型,指明主要的逻辑功能在子类中不能被重写。
 3. 钩子方法:由抽象类声明并加以实现。但是子类可以去扩展,子类可以通过扩展钩子方法来影响模版方法的逻辑。

 抽象类的任务是搭建逻辑的框架,通常由经验丰富的人员编写,因为抽象类的好坏直接决定了程序是否稳定性。
 实现类用来实现细节。抽象类中的模版方法正是通过实现类扩展的方法来完成业务逻辑。只要实现类中的扩展方法通过了单元测试,
 在模版方法正确的前提下,整体功能一般不会出现大的错误。

模版方法的优点及适用场景

 容易扩展。一般来说,抽象类中的模版方法是不易反生改变的部分,而抽象方法是容易反生变化的部分,
 因此通过增加实现类一般可以很容易实现功能的扩展,符合开闭原则。

 便于维护。对于模版方法模式来说,正是由于他们的主要逻辑相同,才使用了模版方法,假如不使用模版方法,
 任由这些相同的代码散乱的分布在不同的类中,维护起来是非常不方便的。

 比较灵活。因为有钩子方法,因此,子类的实现也可以影响父类中主逻辑的运行。
 但是,在灵活的同时,由于子类影响到了父类,违反了里氏替换原则,也会给程序带来风险。这就对抽象类的设计有了更高的要求。

 在多个子类拥有相同的方法,并且这些方法逻辑相同时,可以考虑使用模版方法模式。
 在程序的主框架相同,细节不同的场合下,也比较适合使用这种模式。

3、观察者模式

对象间的联动—观察者模式

观察者模式是使用频率最高的设计模式之一,它用于建立一种对象与对象之间的依赖关系,
一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。
在观察者模式中,发生改变的对象称为观察目标,而被通知的对象称为观察者, 
一个观察目标可以对应多个观察者,而且这些观察者之间可以没有任何相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。

   观察者模式定义如下:

观察者模式(Observer Pattern):定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,
其相关依赖对象皆得到通知并被自动更新。
观察者模式的别名包括发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、
源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。

观察者模式的结构

在最基础的观察者模式中,包括以下四个角色:

被观察者:从类图中可以看到,类中有一个用来存放观察者对象的Vector容器(之所以使用Vector而不使用List,
        是因为多线程操作时,Vector在是安全的,而List则是不安全的),
        这个Vector容器是被观察者类的核心,另外还有三个方法:attach方法是向这个容器中添加观察者对象;
        detach方法是从容器中移除观察者对象;notify方法是依次调用观察者对象的对应方法。这个角色可以是接口,
        也可以是抽象类或者具体的类,因为很多情况下会与其他的模式混用,所以使用抽象类的情况比较多。

具体的被观察者:使用这个角色是为了便于扩展,可以在此角色中定义具体的业务逻辑。
观察者:观察者角色一般是一个接口,它只有一个update方法,在被观察者状态发生变化时,这个方法就会被触发调用。
具体的观察者:观察者接口的具体实现,在这个角色中,将定义被观察者对象状态发生变化时所要处理的逻辑。

观察者模式的优点

 观察者与被观察者之间是属于轻度的关联关系,并且是抽象耦合的,这样,对于两者来说都比较容易进行扩展。

 观察者模式是一种常用的触发机制,它形成一条触发链,依次对各个观察者的方法进行处理。但同时,这也算是观察者模式一个缺点,
 由于是链式触发,当观察者比较多的时候,性能问题是比较令人担忧的。
 并且,在链式结构中,比较容易出现循环引用的错误,造成系统假死。

4、迭代器模式

遍历聚合对象中的元素-迭代器模式

迭代器模式定义如下:

迭代器模式(Iterator Pattern):提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。迭代器模式是一种对象行为型模式。

 迭代器模式是一种使用频率非常高的设计模式,
 通过引入迭代器可以将数据的遍历功能从聚合对象中分离出来,聚合对象只负责存储数据,而遍历数据由迭代器来完成。
 由于很多编程语言的类库都已经实现了迭代器模式,因此在实际开发中,
 我们只需要直接使用Java、C#、OC等语言已定义好的迭代器即可,迭代器已经成为我们操作聚合对象的基本工具之一。

iOS 迭代器:NSEnumerator

1、字典中的(有两个方法):
  - (NSEnumerator<KeyType> *)keyEnumerator;//获取所有key值
  - (NSEnumerator<ObjectType> *)objectEnumerator;//获取所有value值

2、数组中的(有两个方法)
  - (NSEnumerator<ObjectType> *)objectEnumerator;//正向遍历数组    ——>完全可用 for in 语法代替
  - (NSEnumerator<ObjectType> *)reverseObjectEnumerator;//反向遍历数组
  1. 主要优点 迭代器模式的主要优点如下: (1) 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式。 在迭代器模式中只需要用一个不同的迭代器来替换原有迭代器即可改变遍历算法, 我们也可以自己定义迭代器的子类以支持新的遍历方式。 (2) 迭代器简化了聚合类。由于引入了迭代器,在原有的聚合对象中不需要再自行提供数据遍历等方法,这样可以简化聚合类的设计。 (3) 在迭代器模式中,由于引入了抽象层,增加新的聚合类和迭代器类都很方便,无须修改原有代码,满足“开闭原则”的要求。
  2. 主要缺点 迭代器模式的主要缺点如下: (1)由于迭代器模式将存储数据和遍历数据的职责分离,增加新的聚合类需要对应增加新的迭代器类,类的个数成对增加, 这在一定程度上增加了系统的复杂性。 (2)抽象迭代器的设计难度较大,需要充分考虑到系统将来的扩展,例如JDK内置迭代器Iterator就无法实现逆向遍历, 如果需要实现逆向遍历,只能通过其子类ListIterator等来实现,而ListIterator迭代器无法用于操作Set类型的聚合对象。 在自定义迭代器时,创建一个考虑全面的抽象迭代器并不是件很容易的事情。
  3. 适用场景 在以下情况下可以考虑使用迭代器模式: (1) 访问一个聚合对象的内容而无须暴露它的内部表示。将聚合对象的访问与内部数据的存储分离, 使得访问聚合对象时无须了解其内部实现细节。 (2) 需要为一个聚合对象提供多种遍历方式。 (3) 为遍历不同的聚合结构提供一个统一的接口,在该接口的实现类中为不同的聚合结构提供不同的遍历方式, 而客户端可以一致性地操作该接口。

5、解释器模式

自定义语言的实现—解释器模式

解释器模式介绍

解释器模式:给定一个语言,定义它的文法一种表示。并定义一个解释器,这个解释器使用该表示来解释语言中的句子。

也就是说,如果你想自己开发一种语言来解释执行某些语言的特定语法,可以考虑使用解释器模式。

该模式对于我们开发人员来说,基本上都用不到。除非你想自己开发一种语言。

解释器模式真正开发起来很难,就相当于自己开发了一种语言给别人用。

解释器模式UML图:

应用场景

通常来说,当有一个语言需要解释执行,并且你可将该语言中的句子表示为一个抽象语法树叶,则可以使用解释器模式。

EL表达式的处理

正则表达式解释器

SQL语法的解释器

数学表达式解释器:Math 、   Expression String Parser 、 Expression4J

6、责任链模式

请求的链式处理—责任链模式

很多情况下,在一个软件系统中可以处理某个请求的对象不止一个,
例如SCM系统中的采购单审批,主任、副董事长、董事长和董事会都可以处理采购单,他们可以构成一条处理采购单的链式结构,
采购单沿着这条链进行传递,这条链就称为职责链。职责链可以是一条直线、一个环或者一个树形结构,最常见的职责链是直线型,
即沿着一条单向的链来传递请求。链上的每一个对象都是请求处理者,
职责链模式可以将请求的处理者组织成一条链,并让请求沿着链传递,
由链上的处理者对请求进行相应的处理,客户端无须关心请求的处理细节以及请求的传递,
只需将请求发送到链上即可,实现请求发送者和请求处理者解耦。

 职责链模式定义如下:

职责链模式(Chain of Responsibility Pattern):避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。

在职责链模式结构图中包含如下几个角色:

  ● Handler(抽象处理者):它定义了一个处理请求的接口,一般设计为抽象类,
                        由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。
                        因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象
                       (如结构图中的successor),作为其对下家的引用。通过该引用,处理者可以连成一条链。

  ● ConcreteHandler(具体处理者):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象
                               处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,
                               如果可以处理请求就处理它,否则将请求转发给后继者;
                               在具体处理者中可以访问链中下一个对象,以便请求的转发。

具体处理者是抽象处理者的子类,它具有两大作用:第一是处理请求,第二是转发请求。

责任链模式的优缺点

 责任链模式与if…else…相比,他的耦合性要低一些,因为它把条件判定都分散到了各个处理类中,
 并且这些处理类的优先处理顺序可以随意设定。责任链模式也有缺点,这与if…else…语句的缺点是一样的,
 那就是在找到正确的处理类之前,所有的判定条件都要被执行一遍,当责任链比较长时,性能问题比较严重。

责任链模式的适用场景

就像开始的例子那样,假如使用if…else…语句来组织一个责任链时感到力不从心,代码看上去很糟糕时,
就可以使用责任链模式来进行重构。

总结

 责任链模式其实就是一个灵活版的if…else…语句,它就是将这些判定条件的语句放到了各个处理类中,这样做的优点是比较灵活了,
 但同样也带来了风险,比如设置处理类前后关系时,一定要特别仔细,搞对处理类前后逻辑的条件判断关系,
 并且注意不要在链中出现循环引用的问题。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Python中文社区

Python优雅地dumps非标准类型

專 欄 ❈正小歪,Python 工程师,主要负责 Web 开发和日志数据处理。博客文章《真正的 Tornado 异步非阻塞》、《使用 JWT 让你的 REST...

2435
来自专栏喔家ArchiSelf

一个函数的自白

我是——编程世界的函数,不是数学中的幂,指,对和三角函数等等,但是和f(x)又有着千丝万缕的关系。

1405
来自专栏noteless

[零]java8 函数式编程入门官方文档中文版 java.util.stream 中文版 流处理的相关概念

https://docs.oracle.com/javase/8/docs/api/

3471
来自专栏C/C++基础

我所理解的C++反射机制

在实际的项目中,听到师兄说C++中用到了反射,出于好奇,就查阅相关资料,发现强大的C++本身并不支持反射,反而Java支持反射机制。当我得知这个事实时,一直唯C...

1373
来自专栏WindCoder

Java设计模式学习笔记—桥接模式

文章最后“Java设计模式笔记示例代码整合”为本系列代码整合,所有代码均为个人手打并运行测试,不定期更新。本节内容位于其Bridge包(package)中。

961
来自专栏Java架构师学习

十年Java”老兵“浅谈源码的七大设计模式

一个专业的程序员,总是把代码的清晰性,兼容性,可移植性放在很重要的位置。他们总是通过定义大量的宏,来增强代码的清晰度和可读性,而又不增加编译后的代码长度和代码...

37712
来自专栏Golang语言社区

论golang是世界上最好的语言

概述 golang is a better C and a simple C++ golang主要特性 1、语法简单 舍弃语法糖,严格控制关键字 C++语法糖之...

4119
来自专栏听雨堂

【4】通过简化的正则表达式处理字符串

阅读目录 常见字符串操作 使用正则表达式处理字符串 “前后限定”查找目标 自动处理转义字符 界定串的通用化 多个目标的匹配 进一步扩展 结论 在...

2446
来自专栏java学习

Java每日一练(2017/7/6)

最新通知 ●回复"每日一练"获取以前的题目! ●【新】Ajax知识点视频更新了!(回复【学习视频】获取下载链接) ●答案公布时间:为每期发布题目的第二天 ★【新...

3449
来自专栏黑泽君的专栏

Eclipse保存文件时出现字符编码错误

eclipse 由于开源所以支持了比较杂的编码方式,而这些一个工程导入时添加了不少的外来程序,由于不是同一工程一次编码带来了其中含有 GBK 或 UTF8 或 ...

3201

扫码关注云+社区

领取腾讯云代金券