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

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

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

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

5. 访问者模式(Visitor)

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


    class Bean{
        private Set<Property> properties;
        private Set<Method> methods;
        
        BeanVisitor visitor;
        
        public void doVisit(){
            visitor.visitProperties(properties);
            visitor.visitMethods(methods);
        }
    }
    
    interface BeanVisitor{
        void visitProperties(Set<Property> properties);
        void visitMethods(Set<Method> 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<Observer> 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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容