聊聊实际工作中设计模式的使用

news2024/10/5 13:55:01

          一直想在CSDN上写一篇关于软件设计模式的文章,草稿打了好久,但很长时间都没有想好该如何写,主要有几点考虑:

          1、市面上同类的介绍实在太多了。正所谓第一个能够把美女比喻成鲜花的人是天才,第二个还这么说的是庸才,第N个只能算作是废材。 我曾经也在个人网站上写过7,8篇类似的,可是如果只是简单的知识堆砌,却不能让大家读起来就知道如何应用的话,又有什么意义呢?

          2、  在大学时期我买了一本设计模式相关的书《大话设计模式》,但我没有读完,这本书卖得很火,可我不喜欢为了白话而白话,里面举的例子很生硬,我喜欢言简意赅,或者像《明朝那些事儿》这种庄谐并重的书籍,抑或像《Effective Java》这种不说多余废话的书。

       所以我自己都不喜欢的东西,又怎么能写给别人看呢?本篇文章可能不会像我们网上看到的关于每个设计模式的定义和讲解,我只是说说我在工作中是如何使用的。

      首先还是要说下设计模式是干什么用的。不管怎样,你想能够熟练打出一套令世人惊叹的拳法,首先要做的是练好打拳的基本功,掌握基础的设计模式是必要的。

       设计模式是前辈们通过在实际工作中总结出来的经验,用于设计可复用的面向对象的软件。我很喜欢的一句话就是,不是所有的事情都要从头开始,我们必须学会站在巨人的肩膀上。那设计模式就是我们设计软件的一把利剑,可以帮我们快速出鞘,解决曾经已经出现过的问题,帮助我们能提高软件系统的复用性、扩展性,甚至于可维护性。

      先列出一张我之前画的一个脑图,把大部分比较常用的设计模式几乎都汇总了一遍。不过本文不会详细介绍每个设计模式,会在系列文章中详细描述。

        如果想要用好设计模式,首先要记住的是软件设计模式的作用是为了解决实际的问题,并不是为了设计而设计,为了使用而使用;此外,设计模式的应用讲究的是因地制宜,量体裁衣,不能够生搬硬套,有些场景下,不用设计模式可能要比用更好。

        我开发过很多的软件系统,其中有几个我主导开发的是我个人比较满意的,包括现在处于开发中的一个具备可视化服务编排的联机交易引擎,以及我之前在小米开发的清结算系统以及发票系统。本文也主要结合联机交易引擎以及清结算系统聊聊我如何使用设计模式的。

        第一个案例:小米清结算系统

        作为电商平台,我们拿到用户支付的钱之后,要通过计费、清分和结算等工作把每笔钱分给不同的商户。比如用户购买商品支付100元,经过清结算后,商家A收到30,商家B收到60,小米收到10块。其大致流程是这样:

        

        从上述示意图中可以看到,整个清结算流程涉及到的环节还是比较多的,那该如何去设计整个系统来保证清结算过程能够稳定运行?如何实现不同节点准确无误地流转呢?如果有新的步骤加进来,该如何扩展呢?

        我第一个原则是不可做臃肿的胖子,需要将整个流程按照业务环节进行拆分,每个功能只负责单一的职责。 整个过程由多个承担不同职责的处理器串联起来,形成一个完整的职责链。这就是责任链模式

        计费Processor->清分Processor->请求分账Processor->接收分账Processror->结算Processor。。。。。。

        至此,我们将整个业务拆分成了不同的职责,分而治之。对于每一笔支付单,我们对外暴露的只有一个process处理接口,一整个链式处理都在内部提前定义好。责任链模式在很多场景都有应用到,像sentinel的slot责任链也是典型的责任链模式。

        那第二个问题,我该如何实现这些处理器的流转呢?

        实现流转的方式有很多,可以让每个处理器负责去触发下一个,也可以让一个调度器负责调度。当然,我并没有采用这两种方案。主要我是考虑到我们的整个流程并不是同步的,异步实现更为妥善。每个处理器在执行完成之后,都会发布一个事件,通知说某笔支付单(支付单号是全局唯一的)我已经处理完了啊,然后接着去处理其他支付单了。监听者当发现有个支付单发布了事件,就会进行相应的处理。

        想必您也知道了,这就是观察者模式,实现发布订阅的方式有Spring的发布订阅以及Redis的Pub/Sub。通过观察者模式,使得不同的处理器完全解耦,这是一个事件驱动的传播机制。通过观察者模式,更有利于后续新处理器的扩展。这里补充一点,有的人会把它叫做发布订阅模式,并说明观察者模式和发布订阅模式的差异,最早GoF中并没有提过发布订阅,这个算是对观察者模式的一种升级,我们可以把这两者合二为一,不用一定要强调其区别的,可以说发布订阅把重点放在了解耦两个字上。

        接着往下聊。刚才提到事件监听者接到消息会接着处理,那这么多的处理器,到底应该由哪个Processor处理?

       上面提到的所有Processor都是以存储在数据库的支付单数据为基础的,Processor会依赖支付单状态机,并变更支付单的状态。那么当事件监听者接受到消息时,会根据当前支付单的状态来决定我们该用哪一个Processor去处理。这个就是典型的简单工厂模式。可以看下示例代码:

        switch (status) {
            case 1:
                return clearProcessor;

            case 2:
                return requestDivideProcessor;

            case 3:
                return acceptDivideSuccessProcessor;
            
            ........

            default:
                throw new RunTimeException("抛出异常");
        }

    现在有另外一个问题,这么多处理器,他们是否是完全不同的处理逻辑?是否有通用的部分?此外,是否需要遵顼同样的规范?

      每个处理器尽管负责不同的职责,但他们都需要取支付单,都需要变更支付单状态,业务处理完后都需要发布事件。也就是大致的处理逻辑是一致的,只是有一部分核心的业务不一样。因此,所有的处理器应该可以有一套共同的基础框架,基础模板,他们完全可以继承同一个抽象类,并实现具体的抽象方法,抽象类负责这个模板的定义。模板就是搭建好的房子,处理器只需要按照既定结构实现即可。 这个地方使用的就是模板模式。下面是一个示例伪代码:

public abstract class PayProcessor {
    //处理器需要实现的方法
    public abstract void handle();

    public void process(PayOrder payOrder){
           //读支付单
           getPayOrder();
          //处理逻辑
          handle();
         //发布事件
         publishEvent();
    }
}

        现在又有一个问题,如果处理器发生异常该怎么办?在实际工作中,无论我们系统设计得如何完美,都不可能保证100%不出现事故的。这就像造飞机,我们不可能造一架永远不出事故的飞机,而是要保证只要出现事故,我们能够及时、快速地解决故障,从而保障飞机的稳定运行。开发软件也是如此,我们必须有能够及时处理异常的能力。      

        那对于这个问题,我的实现原则是异常处理的逻辑不可影响正常功能的开发,因此我通过切面的方式实现的,即实现一个AOP动态代理,当代理检测到发生异常时,会自动发起重试,如果重试一定次数还是失败,就会将失败信息写入到失败任务表,等待异步去发起重试。是的,这是标准的代理模式。通过代理模式我很容易实现了对处理器的处理方法进行了拦截和控制,实现了我的异常处理机制。当然,我觉得这里把其叫做装饰器模式也是OK的,两者的实现方式基本上相同的,只不过人们喜欢在定义上逻辑区分出来,如说代理模式强调的是对目标对象的控制,说装饰器模式强调的是动态添加额外的功能,就如Python的装饰器一样,通过闭包函数实现对目标对象的功能增强。那在spring上,我觉得可以根据具体需要实现的业务来判断使用两种哪一种,可,这真的不重要。

好了,上面就是我在清结算系统使用的设计模式,总结下就是:

        为了拆分业务流程,提高维护性和扩展性,使用了责任链模式;

        为了提高复用性,使用了模板模式;

        为了解耦和异步流转,使用了观察者模式;

        为了对监听者提供统一的处理器调用,使用了简单工厂模式;

        为了不影响原功能的前提下实现异常处理,使用了代理模式(或者叫装饰器模式)。

第二个案例:联机交易服务引擎

        先自吹自擂一番,这个是我目前为止最满意的一个系统,倾注了我很多的心血。相比于市面上的同类开源软件,我们联机交易引擎算得上有过之而无不及。

        为了保密性,本文不会详细描述我们的业务流程,只大概罗列下我们的主要技术能力(俗称吹牛逼):我们支持可视化的服务编排、支持非常丰富的编排规则、支持分布式事务管理、支持自研的基于ETCD实现的分布式锁。有了它,会大幅提高开发者的系统开发效率,更能够提升分布式系统的性能、可用性、扩展性。

        我的目标和愿景是在我们单位内部稳定使用一段时间之后,能够把这个软件开源出去,因为我们开发的是具备一定通用性的软件,所以我希望能够让更多的人知道和使用,也希望借此能够打响我们单位的知名度。

        这个案例不会像第一个案例那样介绍,只说一下使用了哪些设计模式。

        从我们的名字可以看出,我们软件是以引擎为驱动的,编排和执行引擎是双核,也是对外暴露的门面。从外观上看,用户看到的也只有这两个,类似于CPU,里面复杂的指令读取、执行等复杂逻辑是隐藏的。引擎不负责具体实现功能,但会对内部的所有实现子模块进行一系列的组合,白话就是引擎不生产功能,他们只是功能的搬运工。比如编排引擎组合了规则加载、规则验证、规则解析、规则存储、规则查询等等一系列的模块。看到这儿,您应该知道了,我们用了门面模式。        

          我们做分布式事务管理是要记录事务流水的,这是分布式事务的基础。而记录事务流水的底层组件是可扩展的,即可以根据业务需求选择存的不同存储策略,如PG,Redis,内存,hdfs等等。这个地方我们使用了策略模式。引擎会根据规则在运行时动态去选择某一种存储策略,使用这种模式就告别了繁琐的if判断,引擎本身并不知道具体策略怎么实现,只是知道是干什么的,只需要根据规则来决定我的具体行为即可。

          和第一个案例一样,我们支持的规则很多,不同规则会有不同的规则构造器、解析器以及处理器,那这就涉及到模板模式、简单工厂模式的使用,这里就不再赘述了。

        

 总结          

        其他的设计模式就不再过多介绍,像单例、建造者模式(如lombok的builder)、适配器、迭代器模式(yield)都是比较常见的模式。

        我希望读完这篇介绍之后你们也能够通过实际工作去真正掌握,纸上得来终觉浅嘛。如果实在不知道该如何动手去写,我推荐一个web框架,那是我见过的最好的web框架,即php界的laravel。我读过很多web框架的源码,像python的Django和Flask,java的Spring,golang的gin。我敢说,laravel可谓无出其右,设计模式的应用真的恰到好处,多一分则笨重,少一分则欠妥。

        最后还是想说,设计模式需要活学活用,避免本本主义。

        如果想了解设计模式的,可以看GoF出版的设计模式一书,这是设计模式的起源,或者可以看我网站写过的几篇,不过我这个不全的。

    附录:

  springboot中使用的设计模式

        单例模式、代理模式、观察者模式、装饰器模式、适配器模式、工厂模式、策略模式、模板模式、责任链模式

        单例模式:这个没啥好说得,IoC容器中的Bean都是单例的。

        工厂模式:通过BeanFactory获取bean。

        代理模式:springboot中提供了动态代理,AOP是springboot重要的概念,提供了切面编程能力,在AOP增强的地方会创建代理(卧槽,咋看着像装饰器模式,但其最重要的区别它可能和目标类本身业务无关)。具体可看之前写的文章: JAVA代理及Dubbo与Springboot的应用 。

        装饰器模式:springboot中带有Wrapper和Decorator的类。

        模板模式:抽象类中使用了模板模式。

        观察者模式:通过事件Event的发布和listen,实现了观察者模式。

        策略模式:在Bean实例化过程中需要使用InstantiationStrategy,springboot提供了simple和cglib两个策略的实现。此外,在Resource资源房访问中也实现了策略模式。此外,spring的资源访问Resource也实现了策略模式.

        适配器模式:HandlerAdapter就是典型的适配器模式,对于不同的controller使用不同的handler处理。

        责任链模式:Spring中的filter就是一种责任链模式,构成了一个chain,在真正到达目的请求之前,会经过一系列的doFilter。

        mybatis使用了建造者模式(SqlSessionFactoryBuilder),工厂模式(SqlSessionFactory)、代理模式(Connection对象),模板模式、装饰器模式等。

laravel中使用的设计模式

        单例模式、简单工厂模式、工厂模式、观察者模式、门面模式、模板模式;

        单例模式:应该是最常见的了。

        最重要的几个单个例:

$app->singleton(
    Illuminate\Contracts\Http\Kernel::class,
    App\Http\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Console\Kernel::class,
    App\Console\Kernel::class
);

$app->singleton(
    Illuminate\Contracts\Debug\ExceptionHandler::class,
    App\Exceptions\Handler::class
);

        观察者模式:比较著名的就是laravel种的event,listener。当发生时间时,listener会自动感知,并执行后续一系列操作。观察者模式的主要作用就是解耦,减少生产方和消费方的耦合,其实就是类似于消息的发布-订阅。

        门面模式:这个在laravel种真的是被大量的使用。常用的有Auth,DB,Queue,View,Redis,

Mail,Route等等很多很多。

        简单工厂模式:在创建DB的底层连接器时用到了。底层额数据库可以是Mysql,SQLite,PostgreSQL等等。

 switch ($config['driver']) {
            case 'mysql':
                return new MySqlConnector;
            case 'pgsql':
                return new PostgresConnector;
            case 'sqlite':
                return new SQLiteConnector;
            case 'sqlsrv':
                return new SqlServerConnector;
        }



各个connector都extends Connector implements ConnectorInterface

        工厂模式:在Queue中,不同的driver对应不同的Connector,不同的Connector连接到不同的队列。如RedisConnector对应connect RedisQueue。其中driver是根据我们配置文件获得。在启动时我们的门面会整合各种connectors,在实际使用中会根据配置选择某一个。

        模板模式:在各种abstract类中,都可以看到模板方式的使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1617768.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Kotlin语法入门-类与对象(6)

Kotlin语法入门-类与对象(6) 文章目录 Kotlin语法入门-类与对象(6)六、类与对象1、声明和调用2、get和set3、init函数初始化4、constructor构造函数4.1、主构造函数4.2、二级构造函数4.3、多个构造函数4.4、省略主构造函数并写了次构造函数 5、类的继承与重写5.1、继承5.2、继承…

【Tello无人机】无人机编队操作面板实现

为了方便进行无人机的编队演示,以及在各种场景下实现队形的灵活切换,开发了一套专门的上位机控制平台。本文将重点介绍应用于tello无人机的编队操作面板及其核心功能。 操作面板页面 下图展示了操作面板,其中包含5种编队动作和3个可选位置设…

2024深圳杯(东北三省)数学建模选题建议及各题思路来啦!

大家好呀,2024深圳杯数学建模(东北三省数学建模联赛)开始了,来说一下初步的选题建议吧: 首先定下主基调, 本次深圳杯(东北三省)建议选A。难度上D>B>C&#…

开源模型应用落地-chatglm3-6b-集成langchain(十)

一、前言 langchain框架调用本地模型,使得用户可以直接提出问题或发送指令,而无需担心具体的步骤或流程。通过LangChain和chatglm3-6b模型的整合,可以更好地处理对话,提供更智能、更准确的响应,从而提高对话系统的性能…

Linux中进程和计划任务管理(2)

一.进程命令 1.lsof lsof 命令,“list opened files”的缩写,直译过来,就是列举系统中已经被打开的文件。通过 lsof 命令,我们就可以根据文件找到对应的进程信息,也可以根据进程信息找到进程打开的文件。 格式&…

【详细实现】v1.0 随机点名应用

1.引言 前面对这个应用的功能做了详细的分析(长什么样、能干什么),以先这样对一个项目最开始的分析成为需求分析,需求分析之后就是设计阶段。 那么一般的项目,在设计阶段都需要干什么呢?在需求分析阶段确定…

Linux系统中安装MySQL

1、在电脑中安装虚拟机 2、df -h查看光盘是否挂载,没挂载用mount -o ro /dev/sr0 /media命令挂载 3、进入etc/yum.repos.d目录查看仓是否配置,若配置进行下一一步,未配置则进行配置 配置软件仓库 [rootlocalhost yum.repos.d]# vim rhle.r…

423 世界读书日 和京东零售技术人一起读好书

我们正处于一个复杂、变化的世界,想要更好地理解、适应它,读书可能是最方便的方式之一。 4 月 23 日世界读书日,我们整理了 10 位零售技术人的书籍推荐给大家,欢迎大家一起来共读好书。愿大家在忙碌工作之余,都能够持…

Kubectl常见排查pod问题命令

一.查看命名空间pod及其日志 #查看命名空间pod kubectl get pods -n <命名空间名称> #该命令不加-n命名空间名称&#xff0c;默认是查看default命名空间的pod#查看对应pod的日志kubectl logs -f <pod-name> -n <namespace>#同样的如果查看的是default命名空…

在 vue3 中使用高德地图

前言&#xff1a;定位地图位置所需要的经纬度&#xff0c;可以通过 拾取坐标 获取。 一&#xff1a;快速上手 1. 安装依赖 npm install amap/amap-jsapi-loader # or pnpm add amap/amap-jsapi-loader # or yarn add amap/amap-jsapi-loader 2. 创建组件 src/components/Ma…

有关栈的练习

栈练习1 给定一个栈&#xff08;初始为空&#xff0c;元素类型为整数&#xff0c;且小于等于 109&#xff09;&#xff0c;只有两个操作&#xff1a;入栈和出栈。先给出这些操作&#xff0c;请输出最终栈的栈顶元素。 操作解释&#xff1a; 1 表示将一个数据元素入栈&#xff…

Mysql 存在多条数据,按时间取最新的那一组数据

1、数据如下&#xff0c;获取每个用户最近的一次登录数据 思路1&#xff1a;order by group by 先根据UserIdLogInTime排序&#xff0c;再利用Group分组&#xff0c;即可得到每个User_Id的最新数据。 1 SELECT * FROM login_db l ORDER BY l.user_id, l.login_time DESC; 排…

一款辅助应用助力盲人公交出行畅行无阻

在这个日新月异的时代&#xff0c;科技进步正以前所未有的速度改变着人们的生活方式&#xff0c;尤其是在提升特殊群体生活质量方面展现出巨大潜力。今日&#xff0c;我们将目光聚焦于盲人公交出行&#xff0c;探讨一款名叫蝙蝠避障的创新辅助应用如何以其实时避障与拍照识别功…

vben admin Table 实现表格列宽自由拖拽

更改BasicTable.vue文件 Table添加 resize-column“resizeColumn” 添加并 return resizeColumn const resizeColumn (w, col) > { setCacheColumnsByField(col.dataIndex, { width: w }); }; 在column中添加 resizable: true,

做一个答题pk小程序多少钱

在探讨“做一个答题pk小程序多少钱”这一问题时&#xff0c;我们首先需要明确的是&#xff0c;小程序的价格并非固定不变&#xff0c;而是受到多种因素的影响。这些因素包括但不限于小程序的复杂度、功能需求、开发周期、技术难度以及开发团队的规模和经验等。因此&#xff0c;…

单机单实例部署RocketMQ及测试

部署Apache RocketMQ需要准备NameServer、Broker、Proxy三个组件。它有几种部署模式&#xff1a; Local模式:Broker 和 Proxy 是同进程部署Cluster模式:Broker 和 Proxy 分别部署 为了部署简便&#xff0c;我们采用local模式。 RocketMQ部署 Dockerfile FROM ubuntu:22.04 …

Linux KASAN使用与实现原理

一、KASAN工具使用 KASAN工具&#xff1a;Kernel Address SANitizer(KASAN)是一种动态内存安全错误检测工具&#xff0c;主要功能是检查内存越界访问和使用已释放内存的问题。 1.1 KASAN宏控开关 KASAN有三种模式&#xff1a;1.通用KASAN&#xff1b;2.基于软件标签的KASAN&…

模版初阶【C++】

✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨ &#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1…

嵌入式Python基础1-2

嵌入式Python基础1-2 条件语句 if elif else 随机数random eval while循环 for循环 水仙花数 循环else list 列表常用方法 增删改查 加排序 append remove pop index() 升序sort(&#xff09;降序 sort(reverseTrue) 反转 reverse&#xff08;&#xff09;…

[Collection与数据结构] PriorityQueue与堆

1. 优先级队列 1.1 概念 前面介绍过队列&#xff0c;队列是一种先进先出(FIFO)的数据结构&#xff0c;但有些情况下&#xff0c;操作的数据可能带有优先级&#xff0c;一般出队列时&#xff0c;可能需要优先级高的元素先出队列&#xff0c;该中场景下&#xff0c;使用队列显然…