架构师日记-深入理解软件设计模式 | 京东云技术团队

京东云开发者
• 阅读 447

作者:京东零售 刘慧卿

一 设计模式与编程语言

1.1 什么是设计模式

设计模式(Design pattern) :由软件开发人员在软件开发中面临常见问题的解决方案,是经过长时间的试验积累总结出来的,它使设计更加灵活和优雅,复用性更好。从实用的角度来看,它代表了某一类问题的最佳实践。

设计模式到底解决了开发过程中的哪些难题呢,它又是如何来解决的呢?

其核心是:复用和解耦。使不稳定依赖于稳定、具体依赖于抽象,以此增强软件设计适应变化的能力。

1.2 什么是编程范式

要探讨设计模式和编程语言的关系,还得从编程范式谈起。编程范式一词最早来自 Robert Floyd 在 1979 年图灵奖的颁奖演说,是程序员看待程序的观点,代表了程序设计者认为程序应该如何被构建和执行的看法,与软件建模方式和架构风格有紧密关系。

当前主流的编程范式有三种:

1.结构化编程(structured programming)

2.面向对象编程(object-oriented programming)

3.函数式编程(functional programming)

这几种编程范式之间的关系如下:

1.起初是非结构化编程,指令(goto指令)可以随便跳转,数据可以随便引用。后来有了结构化编程,人们把 goto 语句去掉了,约束了指令的方向性,过程之间是单向的,但数据却是可以全局访问的;

2.后来面向对象编程的时候,人们干脆将数据与其紧密耦合的方法放在一个逻辑边界内,约束了数据的作用域,靠关系来查找;

3.到函数式编程的时候,人们约束了数据的可变性,通过一系列函数的组合来描述数据,从源到目标映射规则的编排,中间它是无状态的;

编程范式是抽象的,编程语言是具体的。编程范式是编程语言背后的思想,要通过编程语言来体现。C 语言的主流编程范式是结构化编程,而 Java 语言的主流编程范式是面向对象编程,后来 Java8 开始支持 Lambda 表达式,将函数式编程范式的内容融合进来,同时新诞生的语言一开始就支持多范式,比如 Scala,Go 和 Rust 等。

从结构化编程到面向对象编程,再到函数式编程,抽象程度越来越高(离图灵机模型越来越远),与领域问题的距离越来越近。直观的来讲,就是解决现实问题的效率提升了,灵活性和执行效率随之有所下降。

设计模式无论用什么语言实现都是可以的,然而由于语言的各自差异化特点,不是每种语言都完美或统一实现各种设计模式。比如Java里面有策略模式,那是因为Java8之前不支持方法传递,不能把一个方法当作参数传给别人,所以有了策略模式。而JavaScript等语言可以直接传函数,就根本没必要造一个策略模式出来。

1.3 什么是多态特性

面向对象编程语言有三大特性:封装、继承和多态。

1.封装即信息隐藏或数据保护,“数据结构"通过暴露有限的访问接口,授权外部仅能通过"数据结构"提供的方法(函数)来访问其内部的数据;

2.继承的好处是可以实现代码复用,但不应过度使用,如果继承的层次过深就会导致代码可读性和可维护性变差。 因此建议少用继承而多用组合模式;

3.多态可以分为变量的多态,方法的多态,类的多态。通常强调的是类的多态,多态的实现是指子类可以替换父类,在实际代码运行过程中调用子类的方法实现;

多态可以说是面向对象中最重要的一个特性,是解决项目中紧偶合的问题,提高代码的可扩展性和可复用性的核心,是很多设计模式、设计原则、编程技巧的代码实现基础。

多态比较直观的理解就是去完成某个动作,当不同的对象去完成时会产生出不同的状态,其作用范围可以是方法的参数和方法的返回类型。

多态这种特性也需要编程语言提供特殊的语法机制来实现,Java 中多态可以通过"子类继承父类+子类重写父类方法+父类引用指向子类对象"的方式实现,还可以通过"接口语法"的方式实现。C++中则使用virtual(虚函数)关键字来实现。 像一些动态语言如 Python 也可以通过 duck-typing 的语法实现,另外 Go 语言中的"隐藏式接口"也算是 duck-typing。

Python 语言,实现多态示例如下:

class MyFile:
    def write(self):
        print('I write a message into file.')

class MyDB:
    def write(self):
        print('I write data into db. ')

def doIt(writer):
    writer.write()

def demo():
    myFile= MyFile()
    myDB = MyDB()
    doIt(myFile)
    doIt(myDB )

二 设计模式与架构模式

2.1 了解架构模式

对给定上下文的软件架构中常见问题的一种通用的可复用的解决方案,可以为设计大型软件系统的各个方面提供相应的指导。它不仅显示了软件需求和软件结构之间的对应关系,而且指定了整个软件系统的组织和拓扑结构,提供了一些设计决策的基本原理,常见的架构设计模式如下:

架构模式 模式描述 适用场景
分层模式 (Layered pattern) 用于可分解为子任务的结构化程序,每个子任务都位于特定的抽象层级,每一层都为上一层提供服务。 桌面应用程序; 电子商务web应用程序; 移动App;
客户端-服务器模式 (Client-server pattern) 服务器将向多个客户端提供服务。客户端从服务器请求服务,服务器向这些客户端提供相关服务。 电子邮件、文档共享和银行等在线应用程序; 基于IPC的应用程序;
主从模式 (Master-slave pattern) 主节点将工作分配给相同的从节点,并根据从节点返回的结果计算最终结果。 数据库主从复制; 进程内多线程调度;
管道-过滤器模式 (Pipe-filter pattern) 用于构造生成和处理数据流的系统。每个处理步骤都包含一个过滤器组件。要处理的数据通过管道传递。这些管道可用于缓冲或同步目的。 编译器;
代理模式 (Broker pattern) 通过解耦组件来构造分布式系统。 消息中间件;; 网络传输中的代理软件
点对点模式 (Peer-to-peer pattern) 每个组件都称为对等节点。对等节点既可以作为客户机(从其他对等节点请求服务),也可以作为服务器(向其他对等节点提供服务)。 文件共享网络; 多媒体协议;
事件-总线模式 (Event-bus pattern) 订阅发布模式,事件源将消息发布到事件总线上的特定通道,监听者订阅特定的通道。 通知服务; 注册中心;
模型-视图-控制器模式(Model-view-controller pattern) MVC模式,解耦组件并允许有效的代码重用 web应用程序架构; GUI 应用程序;
黑板模式 (Blackboard pattern) 对于没有确定解决方案策略的问题非常有用,所有的组件都可以到达黑板。组件可以生成添加到黑板上的新数据对象。组件在黑板上查找特定类型的数据,并通过与现有的知识源进行模式匹配找到这些数据。 语音识别; 车辆识别及追踪;
解释器模式 (Interpreter pattern) 用于设计一个解释专用语言编写的程序组件。 数据库查询语言,如SQL 用于描述通信协议的语言;

2.2 了解设计模式

在1995年,有四位编程界的前辈合著了一本书,书名叫做《Design Patterns: Elements of Reusable Object-Oriented Software》,翻译过来就是《设计模式:可复用面向对象软件的基础》,书里面总共收录了23种设计模式。这本书是软件研发领域重要的里程碑,合著此书的四位作者,被业内称为GoF(Gang of Four),因此这本书也被人称为GoF设计模式。

设计模式按照目的来分类有:创建、结构、行为三种,按照作用范围来分类有:类模式和对象模式两种。

1.创建型模式:用于创建对象,就是将对象的创建与使用分离。从而降低系统的耦合度,使用者不需要关注对象的创建细节,对象的创建由相关的工厂来完成。

2.结构型模式:描述如何将类,对象,接口之间按某种布局组成更大的结构。

3.行为型模式:用于描述程序在运行时复杂的流程控制,即描述多个类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,它涉及算法与对象间职责的分配。

23种设计模式如下:

类型 模式名称 模式描述
创建型 单例模式(Singleton) 某个类只能生成一个实例,该类提供了一个全局访问点供外部获取该实例,其拓展是有限多例模式。
工厂方法模式(Factory Method) 定义一个用于创建产品的接口,由子类决定生产什么产品。
抽象工厂模式(AbstractFactory) 提供一个创建产品族的接口,其每个子类可以生产一系列相关的产品。
建造者模式(Builder) 将一个复杂对象分解成多个相对简单的部分,然后根据不同需要分别创建它们,最后构建成该复杂对象。
原型模式(Prototype) 将一个对象作为原型,通过对其进行复制而克隆出多个和原型类似的新实例。
结构型 适配器模式(Adapter) 将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。
桥接模式(Bridge) 将抽象与实现分离,使它们可以独立变化。它是用组合关系代替继承关系来实现,从而降低了抽象和实现这两个可变维度的耦合度。
组合模式(Composite) 将对象组合成树状层次结构,使用户对单个对象和组合对象具有一致的访问性。
装饰模式(Decorator) 动态的给对象增加一些职责,即增加其额外的功能。
外观模式(Facade) 为多个复杂的子系统提供一个一致的接口,使这些子系统更加容易被访问。
亨元模式(Flyweight) 运用共享技术来有效地支持大量细粒度对象的复用。
代理模式(Proxy) 为某对象提供一种代理以控制对该对象的访问。即客户端通过代理间接地访问该对象,从而限制、增强或修改该对象的一些特性。
行为型 模板方法模式(TemplateMethod) 定义一个操作中的算法骨架,而将算法的一些步骤延迟到子类中,使得子类可以不改变该算法结构的情况下重定义该算法的某些特定步骤。
策略模式(Strategy) 定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的改变不会影响使用算法的客户。
命令模式(Command) 将一个请求封装为一个对象,使发出请求的责任和执行请求的责任分割开。
职责链模式(Chain of Responsibility) 把请求从链中的一个对象传到下一个对象,直到请求被响应为止。通过这种方式去除对象之间的耦合。
状态模式(State) 允许一个对象在其内部状态发生改变时改变其行为能力。
观察者模式(Observer) 多个对象间存在一对多关系,当一个对象发生改变时,把这种改变通知给其他多个对象,从而影响其他对象的行为。
中介者模式(Mediator) 定义一个中介对象来简化原有对象之间的交互关系,降低系统中对象间的耦合度,使原有对象之间不必相互了解。
迭代器模式(Iterator) 提供一种方法来顺序访问聚合对象中的一系列数据,而不暴露聚合对象的内部表示。
访问者模式(Visitor) 在不改变集合元素的前提下,为一个集合中的每个元素提供多种访问方式,即每个元素有多个访问者对象访问。
备忘录模式(Memento) 在不破坏封装性的前提下,获取并保存一个对象的内部状态,以便以后恢复它。
解释器模式(Interpreter) 提供如何定义语言的文法,以及对语言句子的解释方法,即解释器。

2.3 小结

•架构模式更像是宏观战略层面的设计,设计模式则更像是战略目标拆解出来的具体任务的实现方案;

•软件架构是软件的一种搭建形式,往往规定了软件的模块组成,通信接口(含通信数据结构),组件模型,集成框架等,往往规定了具体的细节;

•设计模式是一种软件的实现方法,是一种抽象的方法论,是为了更好的实现软件而归纳出来的有效方法;

•实现一种软件架构,不同组成部分可能用到不同的设计模式,某个部分也可能可以采用不同的设计模式来实现;

三 应用实践指南

3.1 适用场景

不使用设计模式也能实现业务诉求,系统也能够正常运行,为什么要使用设计模式呢?

是的,相当一部分场景是不需要进行设计模式的引入的,比如:业务逻辑简单,业务演进方向不明朗,或者就是一个不需要经常迭代的功能点。但当我们遇到了复杂问题设计的时候,就需要借助前人的经验了,而设计模式就是前人为我们沉淀总结的各种常见问题的解决方案。

那么多种设计模式,难道我需要全部系统的学习实现一遍,都要闭着眼睛就能写出来吗?其实不用,这就跟排序算法一样,我们只需要记住每种算法的适用范围和场景就可以了,在有需要的时候,再去深入研究就可以了。以下总结了各种设计模式对应的适用场景:

模式名称 适用场景
单例模式(Singleton) 无状态类使用单例模式可以节省内存资源
工厂方法模式(Factory Method) 在不知道具体实现细节的情况下创建对象的场景
抽象工厂模式(AbstractFactory) 客户端与对象创建解耦,需要创建多个不同类型的对象的场景
建造者模式(Builder) 生成复杂对象的场景
原型模式(Prototype) 快速创建大量同类对象的场景
适配器模式(Adapter) 让两个不兼容的类一起工作的场景
桥接模式(Bridge) 将一个类的抽象部分和实现部分独立改变的场景
组合模式(Composite) 表示树形结构的场景
装饰模式(Decorator) 动态地为对象添加新职责的场景
外观模式(Facade) 为一个复杂的子系统提供一个简单的接口的场景
亨元模式(Flyweight) 在多个地方共享大量细粒度对象的场景
代理模式(Proxy) 在访问某个对象时增加额外控制的场景
模板方法模(TemplateMethod) 在不改变算法结构的情况下重定义算法中的某些步骤的场景
策略模式(Strategy) 在不同情况下使用不同算法的场景
命令模式(Command) 支持命令的撤销和恢复、延迟调用或日志操作的场景
职责链模式(Chain of Responsibility) 在不明确指定接收者的情况下,向多个对象中提交一个请求的场景
状态模式(State) 根据对象的状态来改变它的行为的场景。
观察者模式(Observer) 在对象之间松散耦合的场景
中介者模式(Mediator) 在多个对象之间松散耦合的场景
迭代器模式(Iterator) 为容器对象提供多种遍历方式的场景
访问者模式(Visitor) 在不改变各元素的类的前提下定义对这些元素的新操作的场景
备忘录模式(Memento) 历史回放或者回滚等场景
解释器模式(Interpreter) 定义一个语言并为该语言实现一个解释器的场景

3.2 场景案例

为了让读者对设计模式有个更加直观立体的感知,接下来以实际案例为大家展现一下设计模式在实际场景的应用。案例包含了创建型,结构型,行为型各种模式类型里常用的设计模式,比如:

•用工厂模式隔离业务实现;

•用策略模式消解业务流程分支;

•用模板方法模式提取业务分支公共流程;

•用建造者模式简化入参对象的构建难度;

•用代理模式横向扩展通用能力(日志,异常处理);

•用职责链模式对请求进行敏感词,防刷校验;

•用命令模式让指令拥有了记忆;

中国有个古谚语:“一个和尚挑水吃,两个和尚抬水吃,三个和尚等水吃。” 我们就通过程序来模拟出家人的寺庙生活。

工厂模式

首先,这三个人是如何成为和尚的呢?

一号和尚(贫困潦倒型),出生在一个大山里头,父母怕他孤单,给他生了5个弟弟,在他9岁那年,恰巧家里闹了饥荒,为了吃上饭,进了寺庙,出了家;

二号和尚(走投无路型),出生在一个湖泊旁边,因为生性耿直,18岁那年,走在街头,路见不平,三拳打死街上恶霸,为了赎罪,受了戒,坠入空门;

三号和尚(天选之子型),从小敏而好学,性情温厚,对佛学产生浓厚兴趣,13岁那年,为了继承和光大佛法,断了尘缘,皈依佛门。

N号和尚,......

每一个和尚的来历都不尽相同,但在当下喝不上水,这件事情上,都显得不重要。重要的是,只要凑足三个和尚,就会没水喝。那么寺庙如招收和尚?这里就可以用到工厂模式的思想。

    // 贫困潦倒产生的和尚过程:1.大山里;2.闹饥荒;3.要吃饭;
    一号和尚 = HeShangFactoty.getOneHeshang("贫困潦倒型");
    // 走投无路产生的和尚过程:1.生性耿直;2.打死恶霸;3.要赎罪;
    二号和尚 = HeShangFactoty.getOneHeshang("走投无路型");
    // 天选之子产生的和尚过程:1.敏而好学;2.佛学感兴趣;3.要广大佛法;
    三号和尚 = HeShangFactoty.getOneHeshang("天选之子型");

以上示例想体现的是工厂模式能将复杂的对象创建和使用进行了分离设计。下面就以和尚吃水这件事情,用程序的方式详细展现工厂模式的实现思路。按照和尚的人数,分别有挑,抬,等三种实现方式。以下为基础代码实现:

public interface Waterable {
    Water getWater();
}

public class TiaoShui implements Waterable{
    public Water getWater(){
        System.out.println("先到山下去!");
        return "水是挑上来的!";
    }
} 

public class TaiShui implements Waterable{
    public Water getWater(){
        System.out.println("先到山下去!");
        return "水是抬上来的!";
    }
} 

public class DengShui implements Waterable{
    public Water getWater(){
        System.out.println("就坐在原地!");
        return "水是等不来的!";
    }
} 

具体使用

public class Factory {
    /**
     * 按照和尚数量生成取水对象
     *
     * @param heShangNum 和尚数量
     * @return
     */
    public static Waterable getWaterable(Integer heShangNum) {
        switch (heShangNum) {
            case 1:
                return new TiaoShui();
            case 2:
                return new TaiShui();
            case 3:
                return new DengShui();
            default:
                throw new RuntimeException("庙小,装不下那么多和尚!");
        }
    }
}

策略模式

按照不同的条件(人数),分别有几种获取水的方法:挑,抬,等。可以通过策略模式来实现,前面的实现方式其实就是策略模式和工厂模式的结合。我们再看一下策略模式的具体使用方式如下:


    /**
     * 通过入参和尚人数,就可以动态改变Waterable.getWater()的取水模式
     * @param heShangNum
     * @return
     */
    public void getWater(Integer heShangNum) {
        Waterable waterable = Factory.getWaterable(heShangNum);
        Water water = waterable.getWater();// 取水
    }

1.输入参数1:挑水模式的实现(对应Tiaoshui实现类);

2.输入参数2:抬水模式的实现(对应Taishui实现类);

3.输入参数3:等不到水模式的实现(对应Dengshui实现类);

通过和尚人数,就可以动态获得对应的取水实现,即所谓的通过策略实现业务,对于使用方来说(主流程),无需关注取水的具体实现(解耦:业务流程稳定性的设计体现),新增取水方式时,只需要新增一个类实现就可以了,存量的实现和主流程都不会受到影响。

模板方法

我们细化取水过程,取水过程一般需要三步:

1.拿起工具(扁担或者木棍);

2.到寺庙南面的小河边(步行);

3.装满水带回寺庙(挑水,抬水,等水);

我们可以将取水流程步骤进行模板化。

public interface Waterable {
    Water getWater();
}

public abstract class AbstractWaterable implements Waterable {
    @Override
    public Water getWater() {
        takeTool();
        toRiver();
        return moveWater();
    }
    /**
     * 拿起工具
     */
    protected abstract String takeTool();

    /**
     * 到河边去
     */
    protected String toRiver() {
        System.out.println("走过去!");
        return "步行";
    }

    /**
     * 将水带回来
     *
     * @return
     */
    protected abstract Water moveWater();
}

个性化场景实现

public class TiaoShui extends AbstractWaterable {

    @Override
    protected String takeTool() {
        return "扁担";
    }

    @Override
    protected Water moveWater() {
        return "挑水";
    }
}

public class Taishui extends AbstractWaterable{
    @Override
    protected String takeTool() {
        return "木棍";
    }

    @Override
    protected Water moveWater() {
        return "抬水";
    }
}

public class DengShui extends AbstractWaterable{
    @Override
    protected String takeTool() {
        return "意念";
    }

    @Override
    protected String toRiver() {
        return "一动不动";
    }

    @Override
    protected Water moveWater() {
        return "无水";
    }
}

具体使用

    /**
     * 和尚取水:实现一个和尚挑水喝,两个和尚抬水喝,三个和尚等水喝
     */
    public void fetchWater(){
        // 
        for (int heShangNum = 1; heShangNum < 4; heShangNum++) {
            Waterable waterable = Factory.getWaterable(heShangNum);
            Water water = waterable.getWater();
        }
    }

模板方法讲的是流程标准定义和能力复用。示例中,定义了取水的三个阶段,选择工具,出行方式,搬运方式。单看出行方式中,【挑水】和【抬水】复用了模板方法里的通用实现,【等水】则个性化的重写了出行方式。

建造者模式

我们取水需要一些工具,按照取水方式(挑,抬,等)可以分为扁担+木桶,木棍+木桶,意念(什么也不需要)等装备的组合方式。如何定义getWater(ToolBox toolBox)的入参ToolBox,使其能够按照对应取水方式匹配正确的装备组合呢?这里就可以使用建造者模式。

public class ToolBox {
    private final String bianDan;
    private final String muTong;
    private final String muGun;

    private ToolBox(TiaoBuilder builder){
        this.bianDan=builder.bianDan;
        this.muTong=builder.muTong;
        this.muGun = null;
    }
    private ToolBox(TaiBuilder builder){
        this.bianDan = null;
        this.muTong = null;
        this.muGun=builder.muGun;
    }
    private ToolBox(DengBuilder builder){
        this.bianDan = null;
        this.muTong = null;
        this.muGun=null;
    }
    public static class TiaoBuilder{
        private String bianDan;
        private String muTong;

        public TiaoBuilder setBianDan(String bianDan) {
            this.bianDan = bianDan;
            return this;
        }
        public TiaoBuilder setMuTong(String muTong) {
            this.muTong = muTong;
            return this;
        }
        public ToolBox build(){
            return new ToolBox(this);
        }
    }

    public static class TaiBuilder{
        private String muGun;
        private String muTong;

        public TaiBuilder setMuGun(String muGun) {
            this.muGun = muGun;
            return this;
        }
        public TaiBuilder setMuTong(String muTong) {
            this.muTong = muTong;
            return this;
        }
        public ToolBox build(){
            return new ToolBox(this);
        }
    }

    public static class DengBuilder{
        public ToolBox build(){
            return new ToolBox(this);
        }
    }

    //省略getter方法
}

具体使用

ToolBox oneHeShangToolBox = new ToolBox.TiaoBuilder().setMuTong("小号木桶").setBianDan("小号扁担").build();
ToolBox twoHeShangToolBox = new ToolBox.TaiBuilder().setMuTong("大号木桶").setMuGun("长号木棍").build();
ToolBox threeHeShangToolBox = new ToolBox.DengBuilder().build();

建造者模式属于创建型设计模式,它可以将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

代理模式

为了鼓励多劳多得,庙里取水开始采用积分机制,每取回来一桶水就要敲一下木鱼,打一次卡。这里就可以采用代理模式。代理分为静态代理和动态代理,为了简单起见,这里就用静态代理来举例。

    public class WaterableProxy implements Waterable{
    /**
     * 被代理的原始对象
     */
    private Waterable waterable;

    public WaterableProxy(Waterable waterable) {
        this.waterable = waterable;
    }

    @Override
    public Water getWater() {
        Water water = waterable.getWater();
        // 增强的新功能,不管是挑水,抬水,等水,只有带回来水,就可以
        if(water != "无水"){
            System.out.println("我敲一下木鱼,打一次卡!");
        }
        return water;
    }
}

具体使用

    public void doProxy(){
        Waterable waterable = new Taishui();
        WaterableProxy proxyWaterable = new WaterableProxy(waterable);
        proxyWaterable.getWater();
    }

代理模式就是代理对象具备真实对象的功能,并代替真实对象完成相应操作,并能够在操作执行的前后,对操作进行增强处理。(通过代理访问真实对象)

责任链模式

为了升级取水工具,将小木桶升级大金桶,寺庙决定对外提供烧香拜佛,诵经礼佛等增值服务。为了安全起见,寺庙引进了安检机制,流程是这样的:

•禁止携带宠物;

•衣着穿戴整齐;

•其它业障,最终解释权归寺庙;

public interface SecureFilter {
    void filter(Map context);
}

public class PetSecure implements SecureFilter{
    @Override
    public void filter(Map context) {
        if(context.containsKey("宠物")){
            throw new RuntimeException("请出去:禁止携带宠物进入!");
        }
    }
}

public class WearSecure implements SecureFilter{
    @Override
    public void filter(Map context) {
        if(context.containsKey("光膀子")){
            throw new RuntimeException("请出去:有伤风化者!");
        }
    }
}

public class OtherSecure implements SecureFilter{
    @Override
    public void filter(Map context) {
        if(context.containsKey("大声喧哗")){
            throw new RuntimeException("请出去:佛门乃清净之地!");
        }
    }
}

具体使用

/**
 * 安检责任链实现
 */
class SecureChain implements SecureFilter{
    // 注入PetSecure,WearSecure,OtherSecure等过滤器
    private List<SecureFilter> secureFilterList;
    /**
     * 进入寺庙,进行安检逻辑
     * @param context
     */
    @Override
    public void filter(Map context) {
        // 进行安检流程
        for (SecureFilter secureFilter : secureFilterList) {
            secureFilter.filter(context);
        }
        System.out.println("佛祖保佑,安检通过!");
    }
}

流程示意图

架构师日记-深入理解软件设计模式 | 京东云技术团队

责任链模式一般和过滤器模式组合一起使用,即创建一个链条,经过这个链条处理的所有对象和数据分别进行依次加工,每个环节负责处理不同的业务,环节间彼此独立解耦,同时可以复用。这种设计的巧妙之处在于可以链式调用,不同的过滤方式可以灵活的排序和组合。既可以使用单个过滤器进行处理,也可以直接添加一条责任链。

命令模式

寺庙里的和尚除了打水工作之外,还有很多工作要做,所有的工作安排都是按照主持的指令来执行的,比如某日清晨的工作安排如下:

1.一号和尚做早餐;

2.二号和尚扫庭院;

3.三号和尚敲古钟;

结构定义

public class Command implements Serializable {
    // 做早餐,打扫,敲钟等指令标识
    private OrderTypeEnum order;
    // 正向执行OR逆向回滚
    private Integer direction;
    // 省略get和set方法
}

// 指令动作执行器,每种指令对应一个实现
public interface OrderHandler {
 /**
     * 执行逻辑
     *
     * @param callContext
     * @return
     */
    PipeResult execute(CallContext callContext);
    /**
     * 支持的命令类型:做早餐,打扫,敲钟等命令标识
     *
     * @return
     */
    OrderTypeEnum getOrderType();

}

// 指令类型管理器
public interface PipelineCmd {

    /**
     * 指令行定义
     *
     * @return
     */
    Command getCommand();

    /**
     * 执行逻辑
     *
     * @param pipeContext
     * @return
     */
    PipeResult execute(PipeContext pipeContext);

    /**
     * 如果可以撤消指令,则此方法应返回true ,否则返回false
     *
     * @return
     */
    default boolean isReversible() {
        return true;
    }
}

 // 指令执行器管理器
 public interface CmdHandler {
    /**
     * 业务执行
     *
     * @param callContext
     * @return
     */
    PipeResult execute(CallContext callContext);

    /**
     * 业务回滚(只回滚当前指令)
     *
     * @param callContext
     * @return
     */
    PipeResult rollback(CallContext callContext);

    /**
     * 全部回滚
     *
     * @param pipeContext
     * @return
     */
    PipeResult rollbackAll(PipeContext pipeContext);
}

命令实现

public class ZhuChiCmd implements PipelineCmd {
    private Command command;
    private transient OrderHandler orderHandler;

    public StepCmd(Command command, OrderHandler orderHandler) {
        this.command = command;
        this.orderHandler= orderHandler;
    }

    @Override
    public PipeResult execute(PipeContext pipeContext) {
        return orderHandler.execute(new CallContext(command, pipeContext));
    }
    // 省略get和set方法
}


public class Breakfast implements OrderHandler {
 /**
     * 执行逻辑
     *
     * @param callContext
     * @return
     */
    PipeResult execute(CallContext callContext){
        System.out.println("做早餐啦!");
    }
    /**
     * 支持的指令类型:做早餐,打扫,敲钟等指令标识
     *
     * @return
     */
    OrderTypeEnum getOrderType(){
        return OrderTypeEnum.BREAKFAST;
    }

}

public class Clean implements OrderHandler {
 /**
     * 执行逻辑
     *
     * @param callContext
     * @return
     */
    PipeResult execute(CallContext callContext){
        System.out.println("打扫庭院啦!");
    }
    /**
     * 支持的指令类型:做早餐,打扫,敲钟等命令标识
     *
     * @return
     */
    OrderTypeEnum getOrderType(){
        return OrderTypeEnum.CLEAN;
    }

}

public class Ring implements OrderHandler {
 /**
     * 执行逻辑
     *
     * @param callContext
     * @return
     */
    PipeResult execute(CallContext callContext){
        System.out.println("敲钟啦!");
    }
    /**
     * 支持的命令类型:做早餐,打扫,敲钟等指令标识
     *
     * @return
     */
    OrderTypeEnum getOrderType(){
        return OrderTypeEnum.Ring;
    }

}

public class CmdFactory {
    private List<OrderHandler> orderHandlerList;

    /**
     * 获取指定指令条件的指令对象
     *
     * @param command
     * @return
     */
     public PipelineCmd getPipelineCmd(Command command) {
        for (OrderHandler orderHandler : orderHandlerList) {
            OrderTypeEnum orderTypeEnum = orderHandler.getOrderType();
            if (orderTypeEnum.equals(command.getOrder())) {
                return new ZhuChiCmd(command, orderHandler);
            }
        }
        throw new RuntimeException("对不起主持:没有多余的和尚来执行新命令了!");
    }
     /**
     * 获取给定指令的回滚操作指令对象
     *
     * @param command
     * @return
     */
    public PipelineCmd getRollbackPipelineCmd(Command command) {
        Command rollbackCommand = getRollbackCommand(command);
        return getPipelineCmd(rollbackCommand);
    }
}

具体使用

public class CmdHandlerImpl implements CmdHandler {
    private CmdFactory cmdFactory;

    @Override
    public PipeResult execute(CallContext callContext) {
        PipelineCmd pipelineCmd = cmdFactory.getPipelineCmd(callContext.getCommand());
        PipeResult pipeResult = pipelineCmd.execute(callContext.getPipeContext());
        return pipeResult;
    }

    @Override
    public PipeResult rollback(CallContext callContext) {
        Command rollbackCommand = cmdFactory.getRollbackCommand(callContext.getCommand());
        if (rollbackCommand == null) {
            return new PipeResult("不需要回滚");
        }
        PipelineCmd pipelineCmd = cmdFactory.getPipelineCmd(rollbackCommand);
        if (!pipelineCmd.isReversible()) {
            return new PipeResult("不支持回滚");
        }
        PipeResult pipeResult = pipelineCmd.execute(callContext.getPipeContext());
        return pipeResult;
    }

    @Override
    public PipeResult rollbackAll(PipeContext pipeContext) {
        // 命令执行备忘录模式对象,这里不再展开
        Caretaker<Command> caretaker = pipeContext.getCaretaker();
        // 拿到上一步执行命令,依次循环回滚
       Command command = caretaker.pop();
        while (command != null) {
            PipelineCmd pipelineCmd = cmdFactory.getRollbackPipelineCmd(command);
            if (pipelineCmd != null) {
                pipelineCmd.execute(pipeContext);
            }
            command = caretaker.pop();
        }
        return new PipeResult();
    }

}

命令模式将一个请求封装为一个对象,使发出的请求的对象和执行请求的对象分割开。这两者之间通过命令对象进行沟通,这样方便将命令对象进行储存、传递、调用、增加与管理。命令模式可以与备忘录模式组合使用,方便实现Undo和Redo操作。

3.3 实践心得

设计原则

具体包含单一职责原则SRP、开闭原则OCP、里氏替换原则LSP、依赖倒置原则DIP、接口隔离原则ISP、最少知识原则LKP等很多种,其核心还是围绕着低耦合,高复用,高内聚,易扩展,易维护展开的。

模式与原则

1.设计原则是指导思想,设计模式是实现手段之一;

2.设计原则在实际开发中并不能做到完全遵守,往往是打破一些原则,遵守一些原则,来实现设计的合理性;(成本,性能)

3.设计模式往往是问题解决方案的骨架,有时候可以当做开发规范和任务拆分执行落地的技术手段;

4.一个设计模式,往往不仅仅采用一种设计原则,而是一些设计原则的整合;

5.设计模式不是一成不变的,可以根据问题场景,输出新的模式;

6.一个复杂场景问题,有时候需要多种设计模式的组合;

7.学设计模式,死记硬背是没用的,要从实践中习得;

8.避免设计过度,使简单的问题复杂化。一定要牢记简洁原则,设计模式是为了使设计简单,而不是更复杂;

四 总结

本文从设计模式与编程语言的关系,设计模式与架构模式的区别,设计原则和设计模式的关系等几个维度进行了分析和解答。关于设计模式应该如何学习和应用的问题,给出了学习意见和实践心得。当然,为了让设计模式更加的直观和立体,也花了大量篇幅在应用实践案例上面,主要是通过场景化的案例,以设计模式的方式给出解决方案,其中部分场景为了方便理解,将问题做了简化处理,但这不影响我们去理解设计模式要解决的问题类型。冰冻三尺非一日之寒,滴水石穿非一日之功,希望本文能够为你带来帮助。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
3年前
PPDB:今晚老齐直播
【今晚老齐直播】今晚(本周三晚)20:0021:00小白开始“用”飞桨(https://www.oschina.net/action/visit/ad?id1185)由PPDE(飞桨(https://www.oschina.net/action/visit/ad?id1185)开发者专家计划)成员老齐,为深度学习小白指点迷津。
Wesley13 Wesley13
3年前
FLV文件格式
1.        FLV文件对齐方式FLV文件以大端对齐方式存放多字节整型。如存放数字无符号16位的数字300(0x012C),那么在FLV文件中存放的顺序是:|0x01|0x2C|。如果是无符号32位数字300(0x0000012C),那么在FLV文件中的存放顺序是:|0x00|0x00|0x00|0x01|0x2C。2.  
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
PHP创建多级树型结构
<!lang:php<?php$areaarray(array('id'1,'pid'0,'name''中国'),array('id'5,'pid'0,'name''美国'),array('id'2,'pid'1,'name''吉林'),array('id'4,'pid'2,'n
Wesley13 Wesley13
3年前
Java日期时间API系列36
  十二时辰,古代劳动人民把一昼夜划分成十二个时段,每一个时段叫一个时辰。二十四小时和十二时辰对照表:时辰时间24时制子时深夜11:00凌晨01:0023:0001:00丑时上午01:00上午03:0001:0003:00寅时上午03:00上午0
Wesley13 Wesley13
3年前
00_设计模式之语言选择
设计模式之语言选择设计模式简介背景设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结。设计模式(Designpattern)代表了最佳的实践,通常被有经验的面向对象的软件开发人员所采用。设计模式是软件开发人员在软件开发过程中面临的
Stella981 Stella981
3年前
Jenkins 插件开发之旅:两天内从 idea 到发布(上篇)
本文首发于:Jenkins中文社区(https://www.oschina.net/action/GoToLink?urlhttp%3A%2F%2Fjenkinszh.cn)!huashan(https://oscimg.oschina.net/oscnet/f499d5b4f76f20cf0bce2a00af236d10265.jpg)
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_