读《大话设计模式》 - 行为型设计模式

187 阅读16分钟

行为型设计模式

行为型模式是对在不同的对象之间划分责任和算法的抽象化。行为型模式不仅仅关注类和对象的结构,而且重点关注它们之间的相互作用。通过行为型模式,可以更加清晰地划分类与对象的职责,并研究系统在运行时实例对象之间的交互。

1. 有哪些

  1. 职责链模式
  2. 命令模式
  3. 解释器模式
  4. 迭代器模式
  5. 中介者模式
  6. 备忘录模式
  7. 观察者模式
  8. 状态模式
  9. 策略模式
  10. 模板方法模式
  11. 访问者模式

2. 各设计模式图解

2.1 职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这个对象连成一条链,并沿着这条链条传递该请求,直到有一个对象处理他为止

27.职责链模式.png

优点

  • 将请求发送者和接收者解耦,发送者无需知道具体的接收者。
  • 请求处理对象仅需维持一个指向其后继者的引用,而不需要维持它对所有的候选处理者的引用,可简化对象的相互连接。

缺点

  • 处理链长的时候性能受到影响,因此需要控制链中最大节点数量,可以在Handler设置一个最大节点数量来控制
  • 递归调用,调式麻烦

应用场景

  • 有多个对象可以处理某请求的时候

2.2 命令模式

将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化。队请求排队或记录请求日志,以及支持可撤销的操作。

19.命令模式.png

源码应用

  • JdbcTemplate中的StatementCallback就是一个抽象的命令接口doInStatement方法为执行命令的方法其子类就是具体的命令

优点

  • 将请求对象和具体执行的对象解耦,两者具有较好的独立性
  • 可以较容易的设计一个命令队列
  • 可以较容易地将命令记入日志,可以容易地实现对请求的撤销和恢复
  • 允许接收请求的一方决定是否要否决请求

缺点

  • 可能导致系统又很多的命令类,因为针对每一个对请求接收者的调用操作都需要设计一个具体的命令类,因此在某些系统中可能需要提供大量的命令类
  • 增加系统复杂度

2.3 解释器模式

是指给定一个语言(表达式),定义它的文法的一种表示,并定义一个解释器,使用该解释器来解释语言中的句子(表达式)

25.解释器模式.png

源码应用

  • SpringExpression

Spring解释器模式.png

优点

  • 易于改变和扩展文法。由于在解释器模式中使用类来表示语言的文法规则,因此可以通过继承等机制来改变或扩展文法
  • 实现文法较为容易。在抽象语法树中每一个表达式节点类的实现方式都是相似的,这些类的代码编写都不会特别复杂,还可以通过一些工具自动生成节点类代码。

缺点

  • 复杂文法难以维护,么一个规则都需要一个类,包含大量文法规则的话,类的个数会很多
  • 执行效率低,因为解释器模式中使用了大量的循环和递归调用,因此解释较为复杂的句子的时候速度很慢,而且调试麻烦

应用场景

  • 需要用语言描述的地方都可以使用他,但是我想我这辈子是不会使用他的!

2.4 迭代器模式

提供一种方法顺序访问一个聚集对象内部的元素,而又不暴露这个对象的内部表,其别名为游标(Cursor)

21.迭代器模式.png

源码应用

  • JDK的集合类都使用了迭代器模式,例如List,Map

22.JDK迭代器源码.png

优点

  • 它支持以不同的方式遍历一个聚合对象,在同一个聚合对象上可以定义多种遍历方式
  • 提供了统一的方法遍历对象,客户端不用在考虑聚合的类型,使用一种方法就可以遍历对象了
  • 迭代器简化了聚集类,分离了职责,符合单一职责原则

缺点

  • 每个聚合对象都需要一个迭代器,会生成多个迭代器不好管理类

应用场景

  • 展示或遍历一组相同对象的时候,适合使用迭代器模式

2.5 中介者模式

用一个中介对象(中介者)来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。中介者模式又称为调停者模式,它是一种对象行为型模式。

23.中介者模式.png

优点

  • 多个类相互耦合会形成网状结构,使用中介者模式将网状结构分离为星型结构,进行解耦
  • 可以减少子类生成,中介者将原本分布于多个对象间的行为集中在一起,改变这些行为只需生成新的中介者子类即可,这使各个同事类可被重用,无须对同事类进行扩展

缺点

  • 中介者承担了较多的责任,一旦中介者出现问题就芭比Q了

应用场景

  • 一般应用于一组对象以定义良好但是复杂的方式进行通讯的场合
  • 定制一个分布在多个类中的行为,而又不想生成太多的子类的场合(中介者将责任全拿去了,只要生成一个中介者子类就好了)

2.6 备忘录模式

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

24.备忘录模式.png

优点

  • 它提供了一种状态恢复的实现机制,使得用户可以方便地回到一个特定的历史步骤,当新的状态无效或者存在问题时,可以使用暂时存储起来的备忘录将状态复原。
  • 备忘录实现了对信息的封装,一个备忘录对象是一种原发器对象状态的表示,不会被其他代码所改动。备忘录保存了原发器的状态,采用列表、堆栈等集合来存储备忘录对象可以实现多次撤销操作。

缺点

  • 如果类的成员变量过多,势必会占用较大的资源,而且每一次保存都会消耗一定的内存

应用场景

  • 存档恢复,数据库事务管理
  • 在之前的一个系统,需要对接OA,费控等外部系统。如果其中一个环节出现了问题,已经做过的处理就要回滚。这个时候备忘录可以很好的处理

2.7 观察者模式

观察者模式(Observer Pattern)定义了对象之间的一对多依赖,让多个观察者对象同时监听一个主体对象,当主体对象发生变化时,它的所有依赖者(观察者)都会收到通知并更新,属于行为型模式。观察者模式有时也叫做发布订阅模式。观察者模式主要用 于在关联行为之间建立一套触发机制的场景。

14.观察者模式.png

源码应用

  • JDK提供了一套观察者的模型 Observable是被观察者的顶层抽象,Observer是观察者的顶层接口;

JDK观察者模式.png

  • SpringApplicationListener也实现了观察者模式。publish事件最后调用onApplicationEvent方法。可以说所有实现了EventListener的都实现了观察者模式

优点

  • 观察者和被观察者之间建立了一个抽象的耦合,观察目标只需要维持一个抽象观察者的集合,无需了解其具体观察者;
  • 观察者模式支持广播通信,简化了一对多系统的设计难度;
  • 观察者模式满足“开闭原则”的要求,增加新的具体观察者无须修改原有系统代码,在具体观察者与观察目标之间不存在关联关系的情况下,增加新的观察目标也很方便。

缺点

  • 如果一个观察目标对象有很多直接和间接观察者,将所有的观察者都通知到会花费很多时间;
  • 如果观察者和观察目标之间有循环依赖,可能导致系统崩溃;
  • 观察者只能知道观察者变化了,但是不知道观察者是如何变化的;

委托事件模型

由于真实开发中,观察者可能已经实现并不想再去实现某个接口从而实现观察者模式。于是出现了委托事件模型

委托就是一种引用方法的类型。一旦为委托分配了方法,委托将与该方法具有完全相同的行为

13.委托事件模型.png

2.8 状态模式

许一个对象在其内部状态改变时改变它的行为,对象看起来似乎修改了它的类。主要解决的是当控制一个对象状态转换的条件表达式过于复杂时的情况,把状态的判断逻辑转移到表示不同状态的一系列类当中,可以把复杂的判断逻辑简化

26.状态模式.png

优点

  • 代码有很强的阅读性。将每个状态的有关行为全部封装在了具体状态类,注入不同的状态获取不同的行为
  • 方便维护。将容易产生为题的if-else优化掉了,允许将状态转换逻辑和状态对象合成一体。

缺点

  • 会产生大量的类,每个状态都需要一个类来维护。
  • 会ocp原则支持不是很友好,新增状态的时候需要修改那些负责转换状态的源码,否则无法转换到新增的状态

应用场景

  • 当一个对象或者事件有很多种状态,状态之间会互相转换,对不同的状态要求有不同的行为的时候,状态模式是极好的选择

2.9 策略模式

定义了算法家族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化不会影响到使用算法的客户

9.策略模式.png

源码应用

  • Comparator下有很多的策略类,使用的时候也可以借助函数式接口自己去实现自己的策略
  • 函数式接口都可以认为是策略模式的体现,我们使用的时候自己去实现我们所需要的策略
  • SpringMvc的Handle

优点

  • 符合开闭原则,可以让用户在不修改原有系统的基础上选择算法或者行为,也可以灵活的增加算法或行为;
  • 避免了多重选择语句,增加代码可读性;
  • 可以封装几乎任何类型的变化,不只是算法而已。只要在不同的时间或者场景用到不同的业务规则,就可以考虑策略模式处理这种变化的可能性;

缺点

  • 系统会有很多的策略类,任何细小的变化都导致系统增加一个策略类,增加维护难度;
  • 客户端必须知道所有的策略类,并自行决定使用哪个策略类;只适用于客户端知道所有算法和行为的情况;
  • 无法在客户端使用多个策略类

应用场景

  • 不同的时间或者场景要使用到不同的业务规则,就可以考虑适用策略模式了

2.10 模板方法模式

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

10.模板方法模式 (1).png

源码应用

  • SpringConfigurableApplicationContextrefresh应用到了模板方法模式

  • 集合中大量用到模板方法,AbstractListAbstractCollection等都是很多集合类的父类,所以会把一些公共部分放在抽象父类中,一些可扩展的方法让子类实现;

    public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    ​
        protected AbstractList() {
        }
    ​
        // add 就是一个模板
        public boolean add(E e) {
            // 调用另一个add [子类实现特定的步骤]
            add(size(), e);
            return true;
        }
         . . .
    ​
        public void add(int index, E element) {
           // 通过抛出异常的行为,达到不强制子类实现,但是使用时必须实现的效果(就很nice)。
            throw new UnsupportedOperationException();
        }
        . . .
    }
    
  • Mybatis中的BaseExecutor也大量用到模板方法

    public abstract class BaseExecutor implements Executor {
    ​
    ​
        . . .
       // update实现算法骨架
      @Override
      public int update(MappedStatement ms, Object parameter) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
        if (closed) {
          throw new ExecutorException("Executor was closed.");
        }
        clearLocalCache();
        // 子类完善算法细节
        return doUpdate(ms, parameter);
      }
      protected abstract int doUpdate(MappedStatement ms, Object parameter) throws SQLException;
    ​
      protected abstract List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException;
    ​
      protected abstract <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
          throws SQLException;
    ​
      protected abstract <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql)
          throws SQLException;
    } 
    
  • Spring中各种Template结尾的类 。例如JdbcTemplate,就巧妙的时候了回调函数实现完成了模板

        // 例如 execute方法定义算法的骨架,具体的获取结果集的方法(算法)由外部实现传递回调函数进来实现(巧妙)
        private <T> T execute(PreparedStatementCreator psc, PreparedStatementCallback<T> action, boolean closeResources)
                throws DataAccessException {
    ​
            Assert.notNull(psc, "PreparedStatementCreator must not be null");
            Assert.notNull(action, "Callback object must not be null");
            if (logger.isDebugEnabled()) {
                String sql = getSql(psc);
                logger.debug("Executing prepared SQL statement" + (sql != null ? " [" + sql + "]" : ""));
            }
    ​
            Connection con = DataSourceUtils.getConnection(obtainDataSource());
            PreparedStatement ps = null;
            try {
                ps = psc.createPreparedStatement(con);
                applyStatementSettings(ps);
                // 结果集的获取使用回调函数完成
                T result = action.doInPreparedStatement(ps);
                handleWarnings(ps);
                return result;
            }
            catch (SQLException ex) {
                // Release Connection early, to avoid potential connection pool deadlock
                // in the case when the exception translator hasn't been initialized yet.
                if (psc instanceof ParameterDisposer) {
                    ((ParameterDisposer) psc).cleanupParameters();
                }
                String sql = getSql(psc);
                psc = null;
                JdbcUtils.closeStatement(ps);
                ps = null;
                DataSourceUtils.releaseConnection(con, getDataSource());
                con = null;
                throw translateException("PreparedStatementCallback", sql, ex);
            }
            finally {
                if (closeResources) {
                    if (psc instanceof ParameterDisposer) {
                        ((ParameterDisposer) psc).cleanupParameters();
                    }
                    JdbcUtils.closeStatement(ps);
                    DataSourceUtils.releaseConnection(con, getDataSource());
                }
            }
        }
    

优点

  • 将不同的代码不同的子类中,通过对子类的扩展增加新的行为,提高代码的扩展性;
  • 把不变的行为写在父类上,去除子类的重复代码,提供了一个很好的代码复用平台,符合开闭原则。
  • 在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时并不会改变算法中步骤的执行次序

缺点

  • 需要为每一个基本方法的不同实现提供一个子类,如果父类可变的基本方法太多,将会导致类的个数的增加,增加系统复杂性(可以使用函数式接口传递回调函数进模板方法的形式避免);
  • 继承自身的缺点,如果父类添加新的抽象方法,所有子类都要改一遍(可以通过父类给一个方法抛出异常的形式不强制子类实现【这样是违背里式替换法则的】)

应用场景

  • 一次性实现一个算法的不变的部分,并将可变的行为留给子类来实现(当可变的不可变的行为在方法的子类实现中混合在一起的时候,不变的行为就会在子类中重复,这时我们就通过模板方法模式将这些行为搬移到单一的地方,帮助子类摆脱重复的不变行为的纠缠)

  • 之前做过一个项目需要对接SAP系统,对接的步骤都一致。只是获取的函数名以及设置的参数会因为单据的不一致有所区别这使用模板方法就十分合适

    public abstract class AbstractSapProcessor<T> {
    ​
        @Autowired
        private SapSystemConfig sapSystemConfig;
    ​
        /**
         * 推送SAP
         *[模板的定义]
         * @param t 推送SAP的参数
         * @return
         */
        public final RebateStatusVo pushSAP(T t) {
            try {
                // 1:获取SAP连接
                JCoDestination destination = getConnection();
                // 2: 获取函数并设置参数设置
                JCoFunction function = getJCoFunctionAndSetUpParam(destination);
                // 3: 执行函数
                function.execute(destination);
                // 4.解析Sap响应
                return getSapResult(function);
            } catch (JCoException e) {
                . . .
            }
        }
    ​
        /**
         * 获取函数并设置参数
         * @param destination
         * @return
         */
        protected abstract JCoFunction getJCoFunctionAndSetUpParam(JCoDestination destination);
    ​
        /**
         * 获取SAP响应
         *
         * @param function
         * @return
         */
        private RebateStatusVo getSapResult(JCoFunction function) {
          . . .
        }
        /**
         * 获取连接
         *
         * @return
         */
        private JCoDestination getConnection() {
            JCoDestination destination = null;
            try {
                destination = ConnectPooled.connectWithPooled(sapSystemConfig);
            } catch (JCoException e) {
                log.error("======>推送SAP,获取连接失败", e);
                throw new RebateOrderException(RebateOrderErrMsgEnum.SAP_CONNECT_ERROR);
            }
            return destination;
        }
    

思考

在使用模板方法的时候感觉和策略十分的像,都是子类实现不同的算法,客户端根据选择不同的子类使用,所以有点迷惘。

进一步思考,模板方法模式强调的是固定的流程,子类实现可变的部分,而且也可以由客户端去实现这一不同的部分。但是策略模式不一样,客户端只能做选择不能改变算法。模板方法强调的是共同点策略模式强调的是不同点。

2.11 访问者模式

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

20.访问者模式.png

优点

  • 符合单一职责原则,让程序具有优秀的扩展性,增加新的访问操作很方便符合ocp原则
  • 将有关元素的的访问行为集中到一个访问对象中,而不是分散在一个个元素类中。类的职责更加清晰,有利于对元素的复用,相同的元素对象可以供多个不同的访问者访问

缺点

  • 增加新的元素类很困难 ,需要修改所有访问者。违背ocp原则
  • 破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问
  • 访问者依赖的事具体的元素而不是抽象的元素,违背了依赖倒置原则

应用场景

  • 系统有比较稳定的系统结构,又有经常变化的系统功能,那么访问者模式比较合适

这里有一篇写的很好的学习并理解23设计模式可以学习