对象的有序行为(Behavioral Patterns)

设计模式之行为模式

本文是设计模式系列的最后一篇。主要讲解设计模式中最后一种模式类型——行为模式。本文抽选了几个主要的设计模式,忽略了少部分次要的,明显意图的模式。

在设计模式中,所谓的行为模式指的是对象之间的交互行为。而从程序的组织结构来看,基本的交互行为仅有一种,即一个对象调用另一个对象的方法,或者说一个对象请求另一个对象的方法,或者说一个对象向另一个对象发起了请求。总得来说,这三种说法大致上是等价的。

    class A{
    
        private B b = new B();
        
        public void methodA(){
            b.methodB();
        }
    }

在上面的例子中,我们说A对象在methodA方法中调用了b的methodB方法,或者说请求了methodB方法,或者说向b的methodB方法发起了请求。

此外,在行为模式中,除了最基本的交互行为之外,实际上同样还涉及类结构模式的组织,只不过这些结构组织在行为模式中处于次要地位。

一、基本的行为模式

有一些基本行为模式,无论是从组织结构还是行为模式上看,它们都异常简单。但从意图上看,却难以区分。

1. 策略模式(Strategy)

策略模式定义了一族算法(algorithms)。客户端持有一个算法接口,并有权选择使用哪一个具体的算法子类。

这个模式非常简单。

    interface Strategy{
        void caculate();
    }
    
    class StrategyA implements Strategy{
        public void caculate(){
            // ...
        }
    }
    
    class StrategyB implements Strategy{
        public void caculate(){
            // ...
        }
    }
    
    class Context{
        private Strategy strategy;
        
        public Context(Strategy strategy){
            this.strategy = strategy;
        }
        
        public doSomething(){
            //....
            strategy.caculate();
            //....
        }
    }

我们看到,上下文对象持有了一个Strategy接口的对象,它允许向其中内注入一个具体的strategy子类。从组织结构的角度上来看,它与之前文章《从设计模式看面向对象的程序结构组织》中有提到的桥接模式(Strategy)非常相象(以及与本文接下来将要提到的状态模式)。在桥接模式中,我们主动提供一个编程SPI,让服务商提供他们的实现,然后我们的应用程序框架基于这个实现来提供更多的功能。

特别是,设计模式的作者认为,策略模式与其它模式的区别在于,策略模式用来抽象算法(algorithms),而其它模式各有自己适应的目标。然而,算法这个词的外延十分夸张,尤其是在编程领域,也许凡是你能想到的东西都可以归为算法的范畴。因此,在这里我们不得不考虑在设计模式里,算法仅仅指那些常见通用的、与可计算理论和复杂度理论相关的所谓算法,而不是说通用的算法,否则策略模式将囊括其它诸多模式。

2. 状态模式(State)

    interface State{
        void caculate();
    }
    
    class StateA implements State{
        public void caculate(){
            // ...
        }
    }
    
    class StateB implements State{
        public void caculate(){
            // ...
        }
    }
    
    class Context{
        private State state;
        
        public Context(State state){
            this.state = state;
        }
        
        private changeState(){
            // ...
        }
        
        public doSomething(){
            //....
            state.caculate();
            //....
            changeState();
            state.caculate();
        }
    }

上面的代码从策略模式的说明代码拷贝而来,仅修改了名字,增加了一个改变状态的方法,在doSomething()中添加了一行改变状态的方法。同相,一个上下文对象持有了一个状态对象,这个状态对象代表了上下文对象的状态,它有A和B两个实现状态。

状态模式封装了对象对于状态的依赖。当状态改变时,附着在状态上相应的行为也会改变。也就是说,当状态从A变到B时,调用B状态的计算,从B变到A时,调用A状态的计算。

注意,我们在这里很难说服自己这所谓附着在状态上的行为不能是算法。当它是算法是,很显然它同样是策略模式。然而,另一个不同点在于,由于可能存在需要显式改变状态的场景,状态模式允许上下文了解它的状态子类分别是什么,对于纯粹的策略模式来说则无太所谓。

3. 命令模式(Command)

命令模式即所谓的回调模式,或者说Action模式。它用一个命令对象表示一个行为。这种表示在java编程语言中是必须的,因为java由于语法的限制,不能直接传递函数。而在javascript等脚本语言中,函数本身就是对象,可以直接传递,因而命令模式在这种语言中会被极大地简化。

    // 回调接口
    interface Command{
        void callback()
    }
    
    class Button{
        private Command command;
        
        public onClick(){
            command.callback();
        }
    }

Button类持有了一个Command接口,当点击发生的时候,它调用callback回调。

同样,你也很难说服自己callback函数中不能实现一个算法。并且,当它实现一个算法时,它与策略模式几乎毫无二致。但对于命令模式,我们唯一区分它跟策略模式的方式就是,把它看作一个普通的请求抽象。

4. 模板方法(Template)

也许你会很奇怪模板方法为什么要放在全文的这样一个位置。但在我看来,如果说状态模式和命令模式非常像的话,那么模板方法本质上就是一个与策略模式没有任何区别的模式,尽管它从表面上看上去似乎比前两者与策略模式相差得更远。

我们来看一个简单的分治算法框架, 以及基于这个框架的快排和归并排序。


    abstract class DivideAndConquer{
        private List list;
        
        public DivideAndConquer(List list){
            this.list = list;
        }
        
        protected List getData(){
            return list;
        }
        
        // int 指明分开的界限索引
        abstract protected DivideAndConquer subProblem(List sublist);
        
        // int 指明分开的界限索引
        abstract protected int doDivide();
        
        // 两个list合并的结果
        abstract protected List doConquer(List list1, List list2);
        
        public List caculate(){
            // 计算分界限索引
            int index = doDivide();
            
            // 按索引分别获得左右列表
            List leftList = list.sublist(0, index);
            List rightList = list.sublist(index + 1, list.length())
            
            DivideAndConquer leftSubProblem = subProblem(leftList);
            DivideAndConquer rightSubProblem = subProblem(rightList);
            
            List leftResult = leftSubProblem.caculate();
            List rightResult = rightSubProblem.caculate();
            
            return doConquer(leftResult, rightResult);
        }
    }
    
    class QuikSort{
        
        public QuikSort(List list){
            super(list);
        }
        
        protected DivideAndConquer subProblem(List sublist){
            return new QuikSort<>(sublist);
        }
        
        protected int doDivide(){
            // 将列表分开,并证返回索引的元素处于正确位置
            // 且位置小于这个索引的元素小于它,大于这个索引的元素位置大于它
        }
        
        protected List doConquer(List list1, List list2){
            // 简单地合并两个列表
        }
    }
    
    class MergeSort{
        
        public MergeSort(List list){
            super(list);
        }
        
        protected DivideAndConquer subProblem(List sublist){
            return new MergeSort<>(sublist);
        }
        
        protected int doDivide(){
            // 取最中间元素
        }
        
        protected List doConquer(List list1, List list2){
            // list1和list2中是排好序的元素
            // 将list1和list2中元素排到一个列表中
        }
    }

可以看到分治算法模板类定义了三个模板方法,分别是subProblemdoDividedoConquer。我们继承这个抽象类,然后在子类中实现不同的子算法,即可获得不同的具体算法,如上,我们通过一个分治抽象类继承出了两个不同的排序算法。理论上讲,只要某个具体算法使用的是分治理论,就可以通过这个类继承出来。

注意,在这里subProblem模板方法实际上还是一个工厂方法。

然而,通过对比策略方法我们发现,它不过是就是把策略接口定义在自己身上,然后再把自己定义成抽象类而已。实现一个策略接口变成了继承抽象的父类,所不同的仅仅是应用了面向对象编程的不同机制而已。

5. 访问者模式(Visitor)

Visitor 用来实现一个类的定制策略。


    class Bean{
        private Set properties;
        private Set methods;
        
        BeanVisitor visitor;
        
        public void doVisit(){
            visitor.visitProperties(properties);
            visitor.visitMethods(methods);
        }
    }
    
    interface BeanVisitor{
        void visitProperties(Set properties);
        void visitMethods(Set methods);
    }

我们可以看到,Bean类型使用BeanVisitor来定制它的属性,这些由BeanBeanVisitor协商出来的可访问的属性称为"可定制的属性"。从结构上看,BeanVisitorBean的结构是非熟悉的,也正因为如此,BeanVisitor有能力定制Bean的属性。

Bean可以在方法上依赖一个类,也可以将一个BeanVisitor作为它的持有依赖,这都无关紧要。但对比之下却很关键,因为策略模式的策略接口也是如此——策略接口当然也可以作为方法参数传递。但习惯上,我们却经常把BeanVisitor作为接口参数传递而Strategy作为持有对象。为了保持说明上的一致,我们这里把BeanVisitor声明为Bean属性。但读者应该了解,这只是依赖的不同方式,并不会导致设计模式本质发生变化。

对于BeanVisitor来说,它的策略发生了一些改变。一是从数量上来看,BeanVisitor通常会有多个接口方法(Strategy也可以有多个策略接口方法,但不是常见情况);二是从目标上看,这些策略方法通常与一个目标对象相关,其策略类型通常就只是针对目标对象的可定制属性。

二、多依赖管理

所谓多依赖指得是一个对象直接或间接持有了许多个对象。我们将看到,多依赖管理解决的是如何对这些依赖进行访问,以及将一个操作请求分发到各个依赖的对象中去。

当然,我们首先回忆一下,前文对于单个对象的依赖是如何控制访问以及分发访问请求的。显然,没有任何的控制。前面所描述的模式中,大抵可以这么总结,一个类持有一个特殊用途的接口,无论它是私有的还是公有的,在这个类中我们都可以直接访问它,并且调用相关的请求。

1. 迭代器模式(Iterator)

相比之下,我更喜欢把这个模式称为访问器模式。

多个依赖在对象中一般是存储为一个集合类,然而集合类与集合类之间可以大不相同,对它们的访问方式也各有不同。例如最基本的分类,可以把一个集合类分为CollectionMap,即单个的集合和对的集合,它们的访问方式显然具有很大差异。然而,对于Collection来说,QueueStack这种拥有特殊访问控制方法的类显然也居有一些顺序上的差异。

这个时候,就可以把这些不同的访问过程抽象到Iterator中。客户端只要持有一个Iterator对象,并按照Iterator对象上的协议访问(通常是nexthasNext方法)就可以避免知道底层是什么具体的集合类。

因为Iterator实际上做的事情就是抽象出各个不同类型集合的访问过程,因此把它称作访问器实在是再合适不过了。用迭代器来称呼的原因更有可能是,Iterator在使用中通常用在迭代的场合。由于通常交流中,我们还是比较习惯称为迭代器,因此本文并不打算更换这个称呼。

迭代器模式常用例子可以在JDK的集合框架中找到,这也许是少有的几个在编程语言中无歧义实现的设计模式之一。

2. 接受分发请求的对象

分发模式,包括接下来将要提到的责任链模式和观察者模式,都有一些共同的特点。第一,它们都包含一组依赖对象,第二,它们都需要向这组依赖对象分发请求。其次,在设计上,它们也有一些共同的要素,即抽象出一个接受请求的对象,而不用管具体是谁在处理这个请求。

    interface RequestHanlder{
        void handle(Request request);    
    }

在上面的代码中,我们给出了一个RequestHanlder接口。

当一个请求发生时,我们将看到,RequestHanlder将接受这个请求,并把请求分发到它管理的对象中。对于客户端对象来说,RequestHanlder其实就是一个集合类,它包含了一系列的处理对象,客户端不关心请求怎么处理,这由RequestHanlder的实现来决定。

3. 责任链模式(Chain of Responsibility)

责任链模式将RequestHanlder进一步实现为一个请求处理链。

    abstract class ResponsibilityChainRequestHanlder implements RequestHanlder{
    
        private ResponsibilityChainRequestHanlder successor;
        
        public ResponsibilityChainRequestHanlder(ResponsibilityChainRequestHanlder successor){
            this.successor = successor;
        }
    }

在实现具体的handle处理的时候,如果某一个RequestHanlder不接受当前的请求,则需要将它传递给下一个(successor)处理器。

注意到,在这种实现方式里,需要有一个管理责任链本身的逻辑。例如,如何将责任链组装起来。在更通用的实现里,我们提供一个ResponsibilityChain类型。

    class ResponsibilityChain{
    
        private ResponsibilityChainRequestHanlder requestHandlers;
        
        public ResponsibilityChain(ResponsibilityChainRequestHanlder requestHandlers){
            this.requestHandlers = requestHandlers;
        }
        
        public void addRequestHanlder(ResponsibilityChainRequestHanlder requestHandler){
            //....
        }
        
        public ResponsibilityChainRequestHanlder next(){
            //...
        }
    }

可以看到,ResponsibilityChain提供了添加RequestHanlder和获取下一个RequestHanlder的方法。然而要记住的是,这些对于责任链模式来说仅仅是辅助性的。

4. 观察者模式(Observer)

观察者模式在责任链模式的基础上放宽了许多限制。我们甚至可以用上面的迭代器模式来描述。


    interface Observer{
        void doUpdate(Subject subject);
    }
    
    class Subject implements RequestHanlder{
    
        private String titile;
        
        Iterator listeners;
    
        public void handle(){
            while(listeners.hasNext()){
                Observer observer = listeners.next();
                observer.doUpdate(this);
            }
        }
        
        public void setTitle(String title){
            this.titile = title;
            handle();
        }
    }

当主题对象的title改变时,就会通知所有已经注册的Observer对象。

我们使用迭代器的目的在于表达Observer如何在主题对象中存储,以什么样的顺序获得与观察者模式是无关的。事实上,观察者模式对主题对象是否作为业务对象,listeners是否作为Subject的持有对象都不作规定。

与责任链同样作为分发请求的模式,观察者模式通常会把请求平均分发到每一个对象,或者在主题对象的层次上进行访问控制。但在责任链中,请求是否传到下一个处理者是由当前的处理者来决定的。与此同时我们会发现Observer接口实际上是一个命令模式的回调接口。

结语

全文完。

了解更多关于结构性设计模式的内容:

  1. 深入理解创建类设计模式(Creational Patterns)
  2. 从设计模式看面向对象的程序结构组织(Structural Patterns)

你可能感兴趣的:(对象的有序行为(Behavioral Patterns))