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

server/2024/9/23 20:15:49/

          一直想在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.ppmy.cn/server/19276.html

相关文章

广州增城牛仔裤制衣厂房的降温

针对广州增城牛仔裤制衣厂房的降温问题,以下是一些建议的降温方案: 通风换气:改善厂房的通风状况是降温的首要步骤。可以安装大型工业风扇或排风扇,增加空气流通,减少热空气滞留。同时,确保厂房的门窗能够…

websocket爬虫

人群看板需求分析 先找到策略中心具体的数据。对应数据库中的数据 看看接口是否需要被逆向 点开消费者细分,可以找到人群包(人群名称) 点击查看透视 label字段分类: 在这里插入图片描述 预测年龄:tagTitle 苹果id&#x…

单片机小项目——直流电机+按键

利用普中单片机的代码实现在按下第k个独立按键时,直流电机运作k秒 #include "reg52.h"typedef unsigned int u16; //对系统默认数据类型进行重定义 typedef unsigned char u8; sbit DC_MotorP1^0; //定义独立按键控制脚 sbit KEY1P3^1; sbit KEY2P3^0; sb…

k8s部署alertmanager

修改alertmanager-pvc.yaml文件中的信息&#xff0c;然后应用YAML文件 cat > /opt/k8s/alertmanager/alertmanager-pvc.yaml <<EOF apiVersion: v1 kind: PersistentVolumeClaim metadata:name: alertmanager-data-pvc spec:accessModes:- ReadWriteManystorageClass…

zabbix6.4告警配置(短信告警和邮件告警),脚本触发

目录 一、前提二、告警配置1.邮件告警脚本配置2.短信告警脚本配置3.zabbix添加报警媒介4.zabbix创建动作4.给用户添加报警媒介 一、前提 已经搭建好zabbix-server 在需要监控的mysql服务器上安装zabbix-agent2 上述安装步骤参考我的上篇文章&#xff1a;通过docker容器安装za…

【论文解读】End-to-End Autonomous Driving through V2X Cooperation

UniV2X 摘要引言方法Sparse-Dense Hybrid Data GenerationCross-View Data Fusion (Agent Fusion)Temporal Synchronization with Flow PredictionSpatial Synchronization with Rotation-Aware Query TransformationCross-View Query Matching and FusionEgo Identification a…

Go语言第二篇-基本数据类型与转义字符

-———————————————————————————— 随便记录没什么顺序&#xff1a; ———————————————————————————— &#x1f523;本部分内容记录了以下知识点&#xff1a; &#x1f30f;转义字符的使用 &#x1f30f;基本数据类型的使…

C++ Primer Plus

第一章 初始C #include <iostream> //#:预处理int main(void) //void:无参数 {using namespace std;int carrots; //定义声明语句:开辟内存空间&#xff0c; int:整型 cout << "how many corrots do you have?" << endl; //cout:输出流(out) end…