关于架构:基于Nodejs打造Web架构中间层

前言Node.js自2009年诞生以来,倒退速度相当惊人,目前各种开发框架层出不穷,国内外各大公司都在应用,如国内的阿里的淘宝、天猫、阿里云、蚂蚁金服,腾讯视频、携程、百度、网易、苏宁、京东、爱奇艺、去哪儿、有赞、贝壳找房等等好多企业都在应用,大部分企业把Node.js作为中间层去利用,明天和大家简略说说对于基于Nodejs打造Web架构中间层的一些常识。 一、中间层与中间件1、什么是中间层中间层(Middle Tier)也称作应用程序服务器层或应用服务层,是用户接口或 Web 客户端与数据库之间的逻辑层。典型状况下 Web 服务器位于该层,业务对象在此实例化。中间层是生成并操作接管信息的业务规定和函数的汇合。它们通过业务规定(能够频繁更改)实现该工作,并由此被封装到在物理上与应用程序程序逻辑自身相独立的组件中。 1.1 Node作为中间层模式以Node作为中间层,当客户端关上一个网站时,先申请到node服务器这一层,通过node服务器转发申请到后端的服务器,获取数据,而后返给node的模板引擎,依据视图模板渲染好模板字符串页面,再返回给客户端,间接展现页面,如图: 1.2 负载均衡器-NginxNginx是一个高性能的WEB服务器和反向代理服务器,最罕用的软件负载均衡器。 当访问量比拟大时,频繁的申请,会给服务带来很大压力,通过负载平衡、分流,加重服务器的压力;另一方面,网站部署在多台服务器,当某台服务器故障的时候,能够马上切换到其它服务器,还能保障网站能失常拜访,这就是负载平衡的劣势。 2、什么是中间件2.1 中间件概念中间件(MiddleWare)是一种独立的系统软件服务程序,分布式应用软件借助这种软件在不同的技术之间共享资源,中间件位于客户机服务器的操作系统之上,治理计算资源和网络通信。从这个意义上能够用一个等式来示意中间件:中间件=平台+通信,这也就限定了只有用于分布式系统中能力叫中间件,同时也把它与撑持软件和实用软件辨别开来。 在NodeJS中,中间件次要是指封装所有Http申请细节解决的办法。一次Http申请通常蕴含很多工作,如记录日志、ip过滤、查问字符串、申请体解析、Cookie解决、权限验证、参数验证、异样解决等,但对于Web利用而言,并不心愿接触到这么多细节性的解决,因而引入中间件来简化和隔离这些基础设施与业务逻辑之间的细节,让开发者可能关注在业务的开发上,以达到晋升开发效率的目标。中间件能够了解为一个对用户申请进行过滤和预处理的货色,它个别不会间接对客户端进行相应,而是将解决之后的后果传递上来。简略来说就是实现某种性能的函数。 Express是一个本身性能极简,齐全是路由和中间件形成一个web开发框架:从实质上来说,一个Express利用就是在调用各种中间件,中间件机制如图所示: 2.2 中间件机制外围实现中间件是从Http申请发动到响应完结过程中的解决办法,通常须要对申请和响应进行解决,因而一个根本的中间件的模式如下: `const middleware = (req, res, next) => { // TODO next()}` 二、中间层的意义Node.js是一个Javascript运行环境。Node.js 应用事件驱动, 非阻塞I/O 模型而得以轻量和高效,非常适合在分布式设施上运行数据密集型的实时利用。Node.js是单过程、单线程运行机制,通过事件轮询(event loop)来实现并发操作,而且性能很好。 Node.js最大的改进架构就是"减少了中间层",前后端拆散,应用Node.js来做‘BBF(backend of frontend)’在传统后端退出了Node.js这一层,通过此有两点益处,前端接管了view层,后端渲染也开始全副由前端掌控,另一个就是接口层减少了一层。在前后端拆散的人造抉择下,Node.js中间层能够承当更多的责任。 1、Node.js中间层可做的工作代理:在开发环境下,咱们能够利用代理来,解决最常见的跨域问题;在线上环境下,咱们能够利用代理,转发申请到多个服务端。缓存:缓存其实是更凑近前端的需要,用户的动作触发数据的更新,Node.js中间层能够间接解决一部分缓存需要。限流:Node.js中间层,能够针对接口或者路由做响应的限流。日志:相比其余服务端语言,Node.js中间层的日志记录,能更方便快捷的定位问题(是在浏览器端还是服务端)。监控:善于高并发的申请解决,做监控也是适合的选项。鉴权:有一个中间层去鉴权,也是一种繁多职责的实现。路由:前端更须要把握页面路由的权限和逻辑。服务端渲染:Node.js中间层的解决方案更灵便,比方SSR、模板直出、利用一些JS库做预渲染等等。2、Node.js中间层带来的益处通过PC Web本人的中间层,能够依照业务定制化接口,扩充前端展示的能力和范畴;中间层接口由应用接口的前端工程师开发,对展示和接口的性能更加相熟,防止了以前的工作模式中接口方跟各方的需要对接、沟通、联调工夫,这样使得我的项目的推动更加顺利,我的项目迭代会更快;中间层应用NodeJS,开发语言是JavaScript,跟当初前端工程师的工作语言一样,缩小了学习老本;中间层接口的开发由前端工程师同时负责开发,既节俭了人力老本,同时又进步了前端开发人员的技术能力,使得前端工程师向全栈工程师迈进。3、Node.js中间层的劣势性能拆散,加重板块累赘;跨零碎、跨终端均可重用页面数据校验、逻辑代码,无需因为新零碎、终端的接入而重写校验;只在中间件中做一次数据校验,防止了前端做数据校验的同时后端也要做校验的反复,在无效保证数据的有效性的同时升高了团队整体的工作量;解决数据逻辑,解放了前端既要做页面渲染又要写简单的逻辑,使得页面开发人员专一于页面渲染,不仅使得分工更为明确,我的项目合作效率更高,更重要的是疾速响应页面使得页面加载更快,用户体验更好,防止了浏览器长时间显示空白页面的不敌对体验,真正的前后端拆散。三、中间层的实现后面写了很多实践方面的常识,接下来本人手动来简略实现Node.js基于Koa框架实现的中间层。 1、后端提供的接口先理解一下后端提供的一个接口,依据前端页面输出不同账号信息,后端接口会返回不同的值,如图: 这段PHP代码是依据前端传给不同的用户名和明码状态返回不同的状态码。 2、搭建前端页面前端页面用了ejs模板引擎采纳服务端渲染形式来进行。前端页面次要有三个代码的文件,app.js,admin.js,admin.ejs。 2.1 我的项目代码构造 2.2 我的项目代码展现1、是app.js代码 `const Koa = require('koa');// 路由const Router = require('koa-router');// 模板引擎const ejs = require('koa-ejs');// 数据解析const body = require('koa-bodyparser');// 解决动态文件const static = require("koa-static");const path = require('path');const app = new Koa();ejs(app,{ ...

July 31, 2020 · 2 min · jiezi

关于架构:保障服务稳定之服务限流

一、前言对于一个零碎而言,最重要的要求之一必定就是服务的稳定性了,一个不稳固的零碎可能给企业带来微小的损失,包含经济和品牌等等方面的损失。 咱们都心愿零碎能稳固牢靠地对外提供服务,响应快,不宕机,不故障,然而在理论状况中,经常会遇到一些异样的状况,这就考验咱们零碎的稳定性了。 明天就来讲讲保障服务稳定性的伎俩之一的服务限流。 二、解决的问题咱们零碎运行过程中有时候可能会遇到突发异样大流量,如果零碎无奈正确处理这些忽然涌入大量申请,就会导致系统轻则响应慢,常常超时,重则导致整个零碎宕机,因而这就要求咱们零碎能以肯定的策略解决大流量的涌入,这样才不对被突发大流量压垮,导致齐全无奈对外提供服务。 留神,这里大流量说的是突发异样大流量,是非正常状况,所以咱们的解决策略是对局部申请进行抛弃或者排队解决,保障咱们零碎对外还是可用的,而非全副申请都须要解决完。而对于零碎失常的一般流量来说,如果其申请量逐步达到了咱们零碎的负载能力的下限的话,这时候须要进行的就是服务的扩容,而不是限流并抛弃申请了。 咱们零碎可能遇到的突发大流量的场景有很多,但对立的体现都是,某些接口申请量激增,导致超过了零碎的解决能力了,例如: 突发热点事件(例如微博热搜)爬虫歹意攻打歹意刷单(如12306)··· 面对突发大流量,咱们零碎能应用的伎俩之一就是服务限流了。限流是通过对一个时间段解决内的申请量进行限度来爱护零碎,一旦达到限度速率则能够抛弃申请,从而管制了零碎解决的申请量不会超过其解决能力。 限流可能在整个网络申请过程的各个层面产生,例如nginx,业务代码层等,这里次要介绍的是限流的思维,也就是限流算法,并给出业务层的代码实现例子。 三、限流算法1、计数器算法计数器限流算法是比较简单粗犷的算法,次要通过一个或者多个计数器来统计一段时间内的申请总量,而后判断是否超过限度,超过则进行限流,不超过则对应的计数器数量加1。 计数器限流算法又能够分为固定窗口和滑动窗口两种。 固定窗口固定窗口计数器限流算法是统计固定一个工夫窗口内的申请总数来判断是否进行限流,例如限度每分钟申请总数为100,则能够通过一个计数器来统计以后分钟的申请总数,每来一个申请判断以后分钟对应的计数器的数量,没有超过限度则在以后分钟对应的计数器加1,超过则拒绝请求。 PHP实现逻辑如下: /** * 固定窗口计数器限流算法 * @param $key string 限流根据,例如uid,url等 * @param $time int 限流时间段,单位秒 * @param $limit int 限流总数 */function limit($key, $time, $limit) { //以后工夫所在分片 $current_segment=floor(time() / $time); //按以后工夫和限流参数生成key $current_key = $key . '_' . $time . '_' . $limit . '_' . $current_segment; $redis = new Redis(); //key不存在才设置,且设置过期工夫 $redis->set($current_key, 0, ['nx', 'ex' => $time]); $current = $redis->incr($current_key); //为了解决申请并发的问题,代码实现上要先加再判断 if ($current > $limit) { return false; } return true;}毛病 ...

July 28, 2020 · 2 min · jiezi

关于架构:来聊一聊前端架构之一前端架构认知

没有一种架构是能够满足所有迭代的需要的前言架构并不是只限于技术选型是架构设计作为软件生命周期的一部分,并不是说开始的时候 设计实现后就会变化无穷,软件的生命周期蕴含了迭代、保护、重构等过程,架构设计亦是如此,所以说架构是须要变动的,目标就是适应当前情况的开发场景。 而架构产生的工夫,必然是受到过后的约束条件,如人力、团队技术积攒、工夫、业务定位等等需要。所以,以后架构可能并不能满足将来的需要,咱们要凋谢看待这个问题,只有以后的架构合乎肯定的设计准则,将来进行架构的演进就不是问题。 “架构”这个词解释也没有一个明确的定义,每个层级,每个场景都有本人的解释,所以到底什么是架构呢? 软件架构的定义软件架构(software architecture),是一系列相干的形象模式,用于领导大型软件系统各个方面的设计。-摘自百度百科其实软件开发和盖一栋大楼一样,都须要布局、设计、施行等一系列的阶段,最开始设计修建图纸,主体架构,还要思考绿化、资料、平安等因素。通过一系列的决策,才有一套成熟的建筑施工计划,依照标准建造,能力保证质量和速度。 而开发一个软件或前端工程也是一个“修建”的过程,咱们要通过业务来定位系统间的关系,探讨技术栈和框架的选用,依据以后团队的技术水平进行技术的选型、思考各个模块间的界线和交互,上线部署策略,问题回滚策略等一系列的决策能力设计出合乎当前情况的技术架构。 前端开发过程中须要怎么的架构大体来看根本要求点如下: 合乎以后的业务定位前端架构必须具备可施行性必须匹配以后技术储备有足够的人力资源去实现具备可伸缩、可扩展性【就地取材】应该是开始设计架构的基本准则,每套架构的产生都有他的外界因素影响,所以,各个公司,各个团队之间的架构不能照搬,如果强制搬过去,可能会事与愿违,就像你那 alibaba 的技术架构去搬到 初创 公司,那是基本行不通的,人员,资源不匹配,是没方法去施行架构的。 因而咱们在我的项目前端开发的生命周期中冀望的架构应该具备哪些要点呢? 精确的业务定位,业务可能是影响架构的次要因素之一,所以咱们要找准业务上的定位明确与其余零碎之间的关系,确定与其余零碎的档次关系,相互间的通信依赖等设计好零碎内子模块之间的关系,如A业务模块须要与B模块交互,该采取怎么的形式根底模块明确,我的项目中,必然含有根底模块去提供一些专用的办法,数据去提供给各个子模块组件布局,这就是再细一层的布局了,制订组件的交互方式,开发范式开发标准和上线流程,用于领导开发中的过程架构设计 setp设计须要进行一系列的技术及非技术的相干工作 收集利益相关者的需要,产品经理、业务人员、我的项目负责人等与相应技术人员进行探讨,确定架构上的潜在限度和要求寻找可行性的技术计划细化技术计划细节,确定一些性能列表中的技术可行性确定危险点与技术人员重复探讨,汇合大家意见对技术计划进行demo的概念验证联合以后业务,细化架构的局部施行细节进行排期架构设计准则不同的架构师可能会有不同的观点,然而能被人大多数架构师认同的有一下三点 不多也不少:不做多余的设计,也不短少外围局部设计过少则为设计有余,会使一个框架的伸缩性和扩展性不强,不能灵便的面对行将产生的业务需要的迭代。设计适度也不肯定是件坏事,针对将来的技术框架或者需要的变更,咱们不能保障目前的架构就肯定能兼容这些变动,适度设计反而会让咱们一会的架构重构产生很多无用的开发变更,减少老本反而得不到相应的输入价值。 演进式:一直的演进以架构适应以后的环境适应环境可能生存下来的物种,并不是那些最强的,也不是最聪慧的,而是那些对变动做出疾速反映的。 -达尔文 演进事架构是指在开发过程中,事后设计好重要的局部如零碎模块通信,功能模块划分,具体组件颗粒度等,而后在编码过程中,再进行细颗粒度的划分,遇到不适宜的中央,进行疾速的反馈,找到最优解,最现实的状态是,20%的打算,80%的演进设计 持续性:长期的架构的改良持续性和演进式有肯定的共同性,演进式的指标是在开发过程中进行架构的细化,持续性则指在迭代过程中进行框架的进阶,在迭代过程中,难免会呈现架构不合乎当前状况的状况,咱们要灵便应答,进行持续性的优化,这样能力在迭代开发过程中,做到最优框架的目标 【提早决策】有时候“迁延症”也并不一定是害处,哈哈,开个玩笑,在咱们设计架构的时候,会经验一系列的方向决策,然而一个框架的倒退并不是总是一帆风顺,当遇到演进方向决策的时候,没有找到最优解,咱们能够进行提早决策,等等,兴许就会有答案,这样兴许会比过后匆忙做出的决定要更合乎预期。 前端架构设计的档次不同阶段形成架构的因素是不同的,基于这个思路,架构设计能够分为四个层级 1.零碎级 对于其余业务零碎,咱们该如何设计之间的通信、合作、交互。比方A零碎承载了内容库模块,B零碎须要用其中的抉择内容组件,咱们设计了一套csi。去托管通信 对于前后端的服务通信形式。ws or http,鉴权,config记录等对于报错收集解决的根底信息建设是否采纳微前端等架构2.利用级 利用级是指多个子利用间接的关系设计,可能是子利用,子模块,lib包、共享模块等,也就是架构设计的进一步细化 脚手架的设计,能够标准利用的根底,有利于疾速的构建子模块或者子利用,退出一些格式化插件,能够无效的避免一些不符合要求的代码上传到近程仓库lib包设计,能够把一些专用的,细颗粒度,反复利用性高的作为一个lib包抽取组件零碎模板,根底组件设计好模板,有利于进步开发效率3.模块级 模块级是深刻到子利用的级别的档次,颗粒度更加细化,蕴含一些设计模式和UI层面的规定,比方,单项还是双向数据流,采纳的UI模板,公共css等 4.代码级 代码级的层级用于标准开发人员的代码,进步代码品质,此档次要做的工作有: 初期的开发领导文档开发过程中的 codeReview定期的技术交换分享开发文档或代码正文的生产【小结】本文在作为一个引子,先介绍了对于架构的一个认知,简略的阐明了在开发过程中咱们须要怎么的步骤去设计一个架构,以及架构设计中咱们应该留神什么,及架构设计者应该关注的档次,接下来的文章会更深刻的介绍下前端架构 继续更新 【来聊一聊前端架构之二】前端架构的落地施行【来聊一聊前端架构之三】构建合乎以后我的项目的架构开发流程【来聊一聊前端架构之四】前端脚手架在我的项目中的利用【来聊一聊前端架构之五】前端架构中组件化的拆分【来聊一聊前端架构之六】微前端架构在我的项目中利用关注一波哦~

July 17, 2020 · 1 min · jiezi

揭秘Java架构技术体系值得一看

能够说,Java是现阶段中国互联网公司中,覆盖度最广的研发语言,把握了Java技术体系,不论在成熟的大公司,疾速倒退的公司,还是守业阶段的公司,都能有立足之地。 有不少敌人问,除了把握Java语法,还要零碎学习哪些Java相干的技术? 明天,就为大家整顿一份目前互联网公司最支流的技术选型: 想要理解更多Java架构技术的,能够关注我一下,我后续也会整顿更多对于架构技术这一块的知识点分享进去,外面会分享一些:spring,MyBatis,Netty源码剖析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化,并发编程这些成为架构师必备的常识体系.想要获取学习zl的能够微我哦:xuanwo013 一、浏览源码 程序员每天都和代码打交道。通过数年的基础教育和职业培训,大部分程序员都会「写」代码,或者至多会抄代码和改代码。然而,会读代码的并不在少数,会读代码又真正读懂一些大我的项目的源码的,少之又少。这也造成了很多谬误看源码的形式。 那要如何正确的剖析源码呢? 二、分布式架构 随着咱们的业务量越来越大和越重要,单体的架构模式曾经无奈对应大规模的利用场景,而且零碎中决不能存在单点故障导致整体不可用,所以只有垂直或是程度拆分业务零碎,使其造成一个分布式的架构,利用分布式架构来冗余零碎打消单点的故障,从而进步整个零碎的可用性。同时分布式系统的模块重用度更高,速度更快,扩展性更高是大型的我的项目必不可少的环节。 三、微服务 对于微服务架构的取舍 1、在适合的我的项目,适合的团队,采纳微服务架构收益会大于老本。 2、微服务架构有很多吸引人的中央,但在拥抱微服务之前,也须要认清它所带来的挑战。 3、须要防止为了“微服务”而“微服务”。 4、微服务架构引入策略 – 对传统企业而言,开始时能够思考引入局部适合的微服务架构准则对已有零碎进行革新或新建微服务利用,逐渐摸索及积攒微服务架构教训,而非全盘施行微服务架构。 四、性能优化 咱们不仅仅对我的项目要指挥若定,还要能解决所有性能问题。只有深刻学习JVM底层原理,Mysql底层优化以及Tomcat调优,能力达到知其然,知其所以然的成果。除了性能优化之外,也能提供通用的常见思路以及计划选型的思考点,帮忙大家造就在计划选型时的意识、思维以及做各种衡量的能力。 五、并发编程 次要造就编程者深刻理解最底层的运作原理,增强编程者逻辑思维,这样能力写出高效、平安、牢靠的多线程并发程序。 六、开发工具 通过一小段形容信息来治理我的项目的构建,报告和文档的软件项目管理工具。用于监控继续反复的工作,旨在提供一个凋谢易用的软件平台,使软件的继续集成变成可能。 能够无效、高速的解决从很小到十分大的我的项目版本治理 七、我的项目实战 要想立足于互联网公司,且能在互联网浪潮中不被吞没,对于我的项目的开发实战演练是不用可少的技能,也是对本身能力的一个掂量,有多少的量对等于取得多少的回报。看似简略的一个我的项目需要图谱,其中的底层原理,实现原理又能晓得多少? 以上这些如何学习,有没有收费材料? 对Java技术,架构技术以及算法内容感兴趣的同学,微我xuanwo013,一起学习,相互讨论。 想要理解更多Java架构技术的,能够关注我一下,我后续也会整顿更多对于架构技术这一块的知识点分享进去,外面会分享一些:spring,MyBatis,Netty源码剖析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化,并发编程这些成为架构师必备的常识体系.微微:xuanwo013

July 9, 2020 · 1 min · jiezi

什么才是真正的架构设计你又知道多少

一. 什么是架构和架构本质(文末有惊喜)在软件行业,对于什么是架构,都有很多的争论,每个人都有自己的理解。此君说的架构和彼君理解的架构未必是一回事。因此我们在讨论架构之前,我们先讨论架构的概念定义,概念是人认识这个世界的基础,并用来沟通的手段,如果对架构概念理解不一样,那沟通起来自然不顺畅。 Linux有架构,MySQL有架构,JVM也有架构,使用Java开发、MySQL存储、跑在Linux上的业务系统也有架构,应该关注哪一个?想要清楚以上问题需要梳理几个有关系又相似的概念:系统与子系统、模块与组建、框架与架构: 1.1. 系统与子系统 系统:泛指由一群有关联的个体组成,根据某种规则运作,能完成个别元件不能独立完成的工作能力的群体。 子系统:也是由一群关联的个体组成的系统,多半是在更大的系统中的一部分。 1.2. 模块与组件 都是系统的组成部分,从不同角度拆分系统而已。模块是逻辑单元,组件是物理单元。 模块就是从逻辑上将系统分解, 即分而治之, 将复杂问题简单化。模块的粒度可大可小, 可以是系统,几个子系统、某个服务,函数, 类,方法、 功能块等等。 组件可以包括应用服务、数据库、网络、物理机、还可以包括MQ、容器、Nginx等技术组件。 1.3. 框架与架构 框架是组件实现的规范,例如:MVC、MVP、MVVM等,是提供基础功能的产品,例如开源框架:Ruby on Rails、Spring、Laravel、Django等,这是可以拿来直接使用或者在此基础上二次开发。 框架是规范,架构是结构。 我在这重新定义架构:软件架构指软件系统的顶层结构。 架构是经过系统性地思考, 权衡利弊之后在现有资源约束下的最合理决策, 最终明确的系统骨架: 包括子系统, 模块, 组件. 以及他们之间协作关系, 约束规范, 指导原则.并由它来指导团队中的每个人思想层面上的一致。涉及四方面: 系统性思考的合理决策:比如技术选型、解决方案等。明确的系统骨架:明确系统有哪些部分组成。系统协作关系:各个组成部分如何协作来实现业务请求。约束规范和指导原则:保证系统有序,高效、稳定运行。因此架构师具备能力:理解业务,全局把控,选择合适技术,解决关键问题、指导研发落地实施。 架构的本质就是对系统进行有序化地重构以致符合当前业务的发展,并可以快速扩展。 那什么样的系统要考虑做架构设计 技术不会平白无故的出和自驱动发展起来,而架构的发展和需求是基于业务的驱动。 架构设计完全是为了业务, 需求相对复杂.非功能性需求在整个系统占据重要位置.系统生命周期长,有扩展性需求.系统基于组件或者集成的需要.业务流程再造的需要.二. 架构分层和分类架构分类可细分为业务架构、应用架构、技术架构, 代码架构, 部署架构 业务架构是战略,应用架构是战术,技术架构是装备。其中应用架构承上启下,一方面承接业务架构的落地,另一方面影响技术选型。 熟悉业务,形成业务架构,根据业务架构,做出相应的应用架构,最后技术架构落地实施。 如何针对当前需求,选择合适的应用架构,如何面向未来,保证架构平滑过渡,这个是软件开发者,特别是架构师,都需要深入思考的问题。 2.1. 业务架构(俯视架构): 包括业务规划,业务模块、业务流程,对整个系统的业务进行拆分,对领域模型进行设计,把现实的业务转化成抽象对象。 没有最优的架构,只有最合适的架构,一切系统设计原则都要以解决业务问题为最终目标,脱离实际业务的技术情怀架构往往会给系统带入大坑,任何不基于业务做异想天开的架构都是耍流氓。 所有问题的前提要搞清楚我们今天面临的业务量有多大,增长走势是什么样,而且解决高并发的过程,一定是一个循序渐进逐步的过程。合理的架构能够提前预见业务发展1~2年为宜。这样可以付出较为合理的代价换来真正达到技术引领业务成长的效果。 看看京东业务架构(网上分享图): 2.2. 应用架构(剖面架构,也叫逻辑架构图): 硬件到应用的抽象,包括抽象层和编程接口。应用架构和业务架构是相辅相成的关系。业务架构的每一部分都有应用架构。 类似: 应用架构:应用作为独立可部署的单元,为系统划分了明确的边界,深刻影响系统功能组织、代码开发、部署和运维等各方面. 应用架构定义系统有哪些应用、以及应用之间如何分工和合作。这里所谓应用就是各个逻辑模块或者子系统。 应用架构图关键有2点: ①. 职责划分: 明确应用(各个逻辑模块或者子系统)边界 逻辑分层子系统、模块定义。关键类。②. 职责之间的协作: ...

July 7, 2020 · 1 min · jiezi

架构设计模式策略模式

俗话说:条条大路通罗马。在很多情况下,实现某个目标的途径不止一条,例如我们在外出旅游时可以选择多种不同的出行方式,如骑自行车、坐汽车、坐火车或者坐飞机,可根据实际情况(目的地、旅游预算、旅游时间等)来选择一种最适合的出行方式。在制订旅行计划时,如果目的地较远、时间不多,但不差钱,可以选择坐飞机去旅游;如果目的地虽远、但假期长、且需控制旅游成本时可以选择坐火车或汽车;如果从健康和环保的角度考虑,而且有足够的毅力,自行车游或者徒步旅游也是个不错的选择。 在软件开发中,我们也常常会遇到类似的情况,实现某一个功能有多条途径,每一条途径对应一种算法,此时我们可以使用一种设计模式来实现灵活地选择解决途径,也能够方便地增加新的解决途径。我们将介绍一种为了适应算法灵活性而产生的设计模式——策略模式。 概述在策略模式中,我们可以定义一些独立的类来封装不同的算法,每一个类封装一种具体的算法,在这里,每一个封装算法的类我们都可以称之为一种策略(Strategy),为了保证这些策略在使用时具有一致性,一般会提供一个抽象的策略类来做规则的定义,而每种算法则对应于一个具体策略类。 策略模式的主要目的是将算法的定义与使用分开,也就是将算法的行为和环境分开,将算法的定义放在专门的策略类中,每一个策略类封装了一种实现算法,使用算法的环境类针对抽象策略类进行编程,符合“依赖倒转原则”。在出现新的算法时,只需要增加一个新的实现了抽象策略类的具体策略类即可。策略模式定义如下: 策略模式(Strategy Pattern):定义一系列算法类,将每一个算法封装起来,并让它们可以相互替换,策略模式让算法独立于使用它的客户而变化,也称为政策模式(Policy)。策略模式是一种对象行为型模式。策略模式结构并不复杂,但我们需要理解其中环境类Context的作用,其结构如图所示: 在策略模式结构图中包含如下几个角色: Context(环境类):环境类是使用算法的角色,它在解决某个问题(即实现某个方法)时可以采用多种策略。在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略。Strategy(抽象策略类):它为所支持的算法声明了抽象方法,是所有策略类的父类,它可以是抽象类或具体类,也可以是接口。环境类通过抽象策略类中声明的方法在运行时调用具体策略类中实现的算法。ConcreteStrategy(具体策略类):它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类对象,使用一种具体的算法实现某个业务处理。策略模式是一个比较容易理解和使用的设计模式,策略模式是对算法的封装,它把算法的责任和算法本身分割开,委派给不同的对象管理。策略模式通常把一个系列的算法封装到一系列具体策略类里面,作为抽象策略类的子类。在策略模式中,对环境类和抽象策略类的理解非常重要,环境类是需要使用算法的类。在一个系统中可以存在多个环境类,它们可能需要重用一些相同的算法。 在使用策略模式时,我们需要将算法从Context类中提取出来,首先应该创建一个抽象策略类,其典型代码如下所示: abstract class AbstractStrategy{ abstract public function algorithm(); //声明抽象算法}然后再将封装每一种具体算法的类作为该抽象策略类的子类,如下代码所示: class ConcreteStrategyA extends AbstractStrategy{ //算法的具体实现 public function algorithm() { //算法A }}其他具体策略类与之类似,对于Context类而言,在它与抽象策略类之间建立一个关联关系,其典型代码如下所示: class Context{ /** * 维持一个对抽象策略类的引用 * @var AbstractStrategy */ private $strategy; /** * @param AbstractStrategy $strategy */ public function setStrategy(AbstractStrategy $strategy) { $this->strategy = $strategy; } //调用策略类中的算法 public function algorithm() { $this->strategy->algorithm(); }}在客户端代码中只需注入一个具体策略对象,可以将具体策略类类名存储在配置文件中,通过反射来动态创建具体策略对象,从而使得用户可以灵活地更换具体策略类,增加新的具体策略类也很方便。策略模式提供了一种可插入式(Pluggable)算法的实现方案。 案例Sunny软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下: 学生凭学生证可享受票价8折优惠;年龄在10周岁及以下的儿童可享受每张票减免10元的优惠(原始票价需大于等于20元);影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。该系统在将来可能还要根据需要引入新的打折方式。 为了实现打折算法的复用,并能够灵活地向系统中增加新的打折方式,Sunny软件公司开发人员使用策略模式对电影院打折方案进行设计,基本结构如图所示: ...

July 5, 2020 · 1 min · jiezi

架构设计模式观察者模式

在软件系统中,有些对象之间存在类似交通信号灯和汽车之间的关系,一个对象的状态或行为的变化将导致其他对象的状态或行为也发生改变,它们之间将产生联动,正所谓“触一而牵百发”。为了更好地描述对象之间存在的这种一对多(包括一对一)的联动,观察者模式应运而生,它定义了对象之间一种一对多的依赖关系,让一个对象的改变能够影响其他对象。 概述观察者模式是使用频率最高的设计模式之一,它用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出反应。在观察者模式中,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间可以没有任何相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。 观察者模式定义如下: 观察者模式(Observer Pattern):定义对象之间的一种一对多依赖关系,使得每当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式的别名包括发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。观察者模式是一种对象行为型模式。观察者模式结构中通常包括观察目标和观察者两个继承层次结构,其结构如图所示: 在观察者模式结构图中包含如下几个角色: Subject(目标):目标又称为主题,它是指被观察的对象。在目标中定义了一个观察者集合,一个观察目标可以接受任意数量的观察者来观察,它提供一系列方法来增加和删除观察者对象,同时它定义了通知方法notify()。目标类可以是接口,也可以是抽象类或具体类。ConcreteSubject(具体目标):具体目标是目标类的子类,通常它包含有经常发生改变的数据,当它的状态发生改变时,向它的各个观察者发出通知;同时它还实现了在目标类中定义的抽象业务逻辑方法(如果有的话)。如果无须扩展目标类,则具体目标类可以省略。Observer(观察者):观察者将对观察目标的改变做出反应,观察者一般定义为接口,该接口声明了更新数据的方法update(),因此又称为抽象观察者。ConcreteObserver(具体观察者):在具体观察者中维护一个指向具体目标对象的引用,它存储具体观察者的有关状态,这些状态需要和具体目标的状态保持一致;它实现了在抽象观察者Observer中定义的update()方法。通常在实现时,可以调用具体目标类的attach()方法将自己添加到目标类的集合中或通过detach()方法将自己从目标类的集合中删除。观察者模式描述了如何建立对象与对象之间的依赖关系,以及如何构造满足这种需求的系统。观察者模式包含观察目标和观察者两类对象,一个目标可以有任意数目的与之相依赖的观察者,一旦观察目标的状态发生改变,所有的观察者都将得到通知。作为对这个通知的响应,每个观察者都将监视观察目标的状态以使其状态与目标状态同步,这种交互也称为发布-订阅(Publish-Subscribe)。观察目标是通知的发布者,它发出通知时并不需要知道谁是它的观察者,可以有任意数目的观察者订阅它并接收通知。 下面通过示意代码来对该模式进行进一步分析。首先我们定义一个抽象目标Subject,典型代码如下所示: abstract class Subject{ /** * 定义一个观察者集合用于存储所有观察者对象 * @var Observer[] */ protected $observers = []; //注册方法,用于向观察者集合中增加一个观察者 public function attach(Observer $observer) { $this->observers[] = $observer; } //注销方法,用于在观察者集合中删除一个观察者 public function detach(Observer $observer) { unset($this->observers[array_search($observer, $this->observers)]); } //声明抽象通知方法 abstract public function notify();}具体目标类ConcreteSubject是实现了抽象目标类Subject的一个具体子类,其典型代码如下所示: class concreteSubject extends Subject{ //实现通知方法 public function notify() { foreach ($this->observers as $observer) { $observer->update(); } }}抽象观察者角色一般定义为一个接口,通常只声明一个update()方法,为不同观察者的更新(响应)行为定义相同的接口,这个方法在其子类中实现,不同的观察者具有不同的响应方法。抽象观察者Observer典型代码如下所示: interface Observer{ //声明响应方法 public function update();}在具体观察者ConcreteObserver中实现了update()方法,其典型代码如下所示: ...

July 4, 2020 · 2 min · jiezi

架构设计模式迭代器模式

在软件开发中,存在一些类,它们可以存储多个成员对象(元素),这些类通常称为聚合类(Aggregate Classes),对应的对象称为聚合对象。为了更加方便地操作这些聚合对象,同时可以很灵活地为聚合对象增加不同的遍历方法,我们也需要类似电视机遥控器一样的角色,可以访问一个聚合对象中的元素但又不需要暴露它的内部结构。本章我们将要学习的迭代器模式将为聚合对象提供一个遥控器,通过引入迭代器,客户端无须了解聚合对象的内部结构即可实现对聚合对象中成员的遍历,还可以根据需要很方便地增加新的遍历方式。 概述在软件开发中,我们经常需要使用聚合对象来存储一系列数据。聚合对象拥有两个职责:一是存储数据;二是遍历数据。从依赖性来看,前者是聚合对象的基本职责;而后者既是可变化的,又是可分离的。因此,可以将遍历数据的行为从聚合对象中分离出来,封装在一个被称之为“迭代器”的对象中,由迭代器来提供遍历聚合对象内部数据的行为,这将简化聚合对象的设计,更符合“单一职责原则”的要求。 迭代器模式定义如下: 迭代器模式(Iterator Pattern):提供一种方法来访问聚合对象,而不用暴露这个对象的内部表示,其别名为游标(Cursor)。迭代器模式是一种对象行为型模式。在迭代器模式结构中包含聚合和迭代器两个层次结构,考虑到系统的灵活性和可扩展性,在迭代器模式中应用了工厂方法模式,其模式结构如图所示: 在迭代器模式结构图中包含如下几个角色: Iterator(抽象迭代器):它定义了访问和遍历元素的接口,声明了用于遍历数据元素的方法,例如:用于获取第一个元素的first()方法,用于访问下一个元素的next()方法,用于判断是否还有下一个元素的hasNext()方法,用于获取当前元素的currentItem()方法等,在具体迭代器中将实现这些方法。ConcreteIterator(具体迭代器):它实现了抽象迭代器接口,完成对聚合对象的遍历,同时在具体迭代器中通过游标来记录在聚合对象中所处的当前位置,在具体实现时,游标通常是一个表示位置的非负整数。Aggregate(抽象聚合类):它用于存储和管理元素对象,声明一个createIterator()方法用于创建一个迭代器对象,充当抽象迭代器工厂角色。ConcreteAggregate(具体聚合类):它实现了在抽象聚合类中声明的createIterator()方法,该方法返回一个与该具体聚合类对应的具体迭代器ConcreteIterator实例。在迭代器模式中,提供了一个外部的迭代器来对聚合对象进行访问和遍历,迭代器定义了一个访问该聚合元素的接口,并且可以跟踪当前遍历的元素,了解哪些元素已经遍历过而哪些没有。迭代器的引入,将使得对一个复杂聚合对象的操作变得简单。 在迭代器模式中应用了工厂方法模式,抽象迭代器对应于抽象产品角色,具体迭代器对应于具体产品角色,抽象聚合类对应于抽象工厂角色,具体聚合类对应于具体工厂角色。 在抽象迭代器中声明了用于遍历聚合对象中所存储元素的方法,典型代码如下所示: interface Iterator { //返回游标当前指向元素 public function current(); //将游标指向下一个元素 public function next(); //返回当前元素的键 public function key(); //检查当前位置是否有效 public function valid(); //重置游标指向第一个元素 public function rewind();}在具体迭代器中将实现抽象迭代器声明的遍历数据的方法,如下代码所示: class ConcreteIterator implements Iterator{ //维持一个对具体聚合对象的引用,以便于访问存储在聚合对象中的数据 private $concreteAggregate; //定义一个游标,用于记录当前访问位置 private $cursor = 0; public function current() { ... } public function next() { ... } public function key() { ... } public function valid(): bool { ... } public function rewind() { ... }}需要注意的是抽象迭代器接口的设计非常重要,一方面需要充分满足各种遍历操作的要求,尽量为各种遍历方法都提供声明,另一方面又不能包含太多方法,接口中方法太多将给子类的实现带来麻烦。因此,可以考虑使用抽象类来设计抽象迭代器,在抽象类中为每一个方法提供一个空的默认实现。如果需要在具体迭代器中为聚合对象增加全新的遍历操作,则必须修改抽象迭代器和具体迭代器的源代码,这将违反“开闭原则”,因此在设计时要考虑全面,避免之后修改接口。 ...

June 29, 2020 · 2 min · jiezi

架构设计模式命令模式

装修新房的最后几道工序之一是安装插座和开关,通过开关可以控制一些电器的打开和关闭,例如电灯或者排气扇。在购买开关时,我们并不知道它将来到底用于控制什么电器,也就是说,开关与电灯、排气扇并无直接关系,一个开关在安装之后可能用来控制电灯,也可能用来控制排气扇或者其他电器设备。开关与电器之间通过电线建立连接,如果开关打开,则电线通电,电器工作;反之,开关关闭,电线断电,电器停止工作。相同的开关可以通过不同的电线来控制不同的电器。 在软件开发中也存在很多与开关和电器类似的请求发送者和接收者对象,例如一个按钮,它可能是一个“关闭窗口”请求的发送者,而按钮点击事件处理类则是该请求的接收者。为了降低系统的耦合度,将请求的发送者和接收者解耦,我们可以使用一种被称之为命令模式的设计模式来设计系统,在命令模式中,发送者与接收者之间引入了新的命令对象,将发送者的请求封装在命令对象中,再通过命令对象来调用接收者的方法。 概述在软件开发中,我们经常需要向某些对象发送请求(调用其中的某个或某些方法),但是并不知道请求的接收者是谁,也不知道被请求的操作是哪个,此时,我们特别希望能够以一种松耦合的方式来设计软件,使得请求发送者与请求接收者能够消除彼此之间的耦合,让对象之间的调用关系更加灵活,可以灵活地指定请求接收者以及被请求的操作。命令模式为此类问题提供了一个较为完美的解决方案。 命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不必知道如何完成请求。 命令模式定义如下: 命令模式(Command Pattern):将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作(Action)模式或事务(Transaction)模式。命令模式的定义比较复杂,提到了很多术语,例如“用不同的请求对客户进行参数化”、“对请求排队”,“记录请求日志”、“支持可撤销操作”等,在后面将对这些术语进行一一讲解。 命令模式的核心在于引入了命令类,通过命令类来降低发送者和接收者的耦合度,请求发送者只需指定一个命令对象,再通过命令对象来调用请求接收者的处理方法,其结构如图所示: 在命令模式结构图中包含如下几个角色: Command(抽象命令类):抽象命令类一般是一个抽象类或接口,在其中声明了用于执行请求的execute()等方法,通过这些方法可以调用请求接收者的相关操作。ConcreteCommand(具体命令类):具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中。在实现execute()方法时,将调用接收者对象的相关操作(Action)。Invoker(调用者):调用者即请求发送者,它通过命令对象来执行请求。一个调用者并不需要在设计时确定其接收者,因此它只与抽象命令类之间存在关联关系。在程序运行时可以将一个具体命令对象注入其中,再调用具体命令对象的execute()方法,从而实现间接调用请求接收者的相关操作。Receiver(接收者):接收者执行与请求相关的操作,它具体实现对请求的业务处理。命令模式的本质是对请求进行封装,一个请求对应于一个命令,将发出命令的责任和执行命令的责任分割开。每一个命令都是一个操作:请求的一方发出请求要求执行一个操作;接收的一方收到请求,并执行相应的操作。命令模式允许请求的一方和接收的一方独立开来,使得请求的一方不必知道接收请求的一方的接口,更不必知道请求如何被接收、操作是否被执行、何时被执行,以及是怎么被执行的。 命令模式的关键在于引入了抽象命令类,请求发送者针对抽象命令类编程,只有实现了抽象命令类的具体命令才与请求接收者相关联。在最简单的抽象命令类中只包含了一个抽象的execute()方法,每个具体命令类将一个Receiver类型的对象作为一个实例变量进行存储,从而具体指定一个请求的接收者,不同的具体命令类提供了execute()方法的不同实现,并调用不同接收者的请求处理方法。 典型的抽象命令类代码如下所示: abstract class Command{ abstract public function execute();}对于请求发送者即调用者而言,将针对抽象命令类进行编程,可以通过构造注入或者设值注入的方式在运行时传入具体命令类对象,并在业务方法中调用命令对象的execute()方法,其典型代码如下所示: class Invoker{ private $command; //构造注入 public function __construct(Command $command) { $this->command = $command; } //设值注入 public function setCommand(Command $command) { $this->command = $command; } //业务方法,用于调用命令类的execute()方法 public function call() { $this->command->execute(); }}具体命令类继承了抽象命令类,它与请求接收者相关联,实现了在抽象命令类中声明的execute()方法,并在实现时调用接收者的请求响应方法action(),其典型代码如下所示: class ConcreteCommand extends Command{ //维持一个对请求接收者对象的引用 private $receiver; public function execute() { //调用请求接收者的业务处理方法action() $this->receiver->action(); }}请求接收者Receiver类具体实现对请求的业务处理,它提供了action()方法,用于执行与请求相关的操作,其典型代码如下所示: class Receiver{ public function action() { //具体操作 }}案例Sunny软件公司开发人员为公司内部OA系统开发了一个桌面版应用程序,该应用程序为用户提供了一系列自定义功能键,用户可以通过这些功能键来实现一些快捷操作。Sunny软件公司开发人员通过分析,发现不同的用户可能会有不同的使用习惯,在设置功能键的时候每个人都有自己的喜好,例如有的人喜欢将第一个功能键设置为“打开帮助文档”,有的人则喜欢将该功能键设置为“最小化至托盘”,为了让用户能够灵活地进行功能键的设置,开发人员提供了一个“功能键设置”窗口,该窗口界面如图所示: ...

June 28, 2020 · 3 min · jiezi

架构设计模式职责链模式

在类似“斗地主”这样的纸牌游戏中,某人出牌给他的下家,下家看看手中的牌,如果要不起上家的牌则将出牌请求再转发给他的下家,其下家再进行判断。一个循环下来,如果其他人都要不起该牌,则最初的出牌者可以打出新的牌。在这个过程中,牌作为一个请求沿着一条链在传递,每一位纸牌的玩家都可以处理该请求。在设计模式中,我们也有一种专门用于处理这种请求链式传递的模式,它就是职责链模式。 概述很多情况下,在一个软件系统中可以处理某个请求的对象不止一个,例如SCM系统中的采购单审批,主任、副董事长、董事长和董事会都可以处理采购单,他们可以构成一条处理采购单的链式结构,采购单沿着这条链进行传递,这条链就称为职责链。职责链可以是一条直线、一个环或者一个树形结构,最常见的职责链是直线型,即沿着一条单向的链来传递请求。链上的每一个对象都是请求处理者,职责链模式可以将请求的处理者组织成一条链,并让请求沿着链传递,由链上的处理者对请求进行相应的处理,客户端无须关心请求的处理细节以及请求的传递,只需将请求发送到链上即可,实现请求发送者和请求处理者解耦。 职责链模式定义如下: 职责链模式(Chain of Responsibility Pattern):避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。职责链模式是一种对象行为型模式。职责链模式结构的核心在于引入了一个抽象处理者。职责链模式结构如图所示: 在职责链模式结构图中包含如下几个角色: Handler(抽象处理者):它定义了一个处理请求的接口,一般设计为抽象类,由于不同的具体处理者处理请求的方式不同,因此在其中定义了抽象请求处理方法。因为每一个处理者的下家还是一个处理者,因此在抽象处理者中定义了一个抽象处理者类型的对象(如结构图中的successor),作为其对下家的引用。通过该引用,处理者可以连成一条链。ConcreteHandler(具体处理者):它是抽象处理者的子类,可以处理用户请求,在具体处理者类中实现了抽象处理者中定义的抽象请求处理方法,在处理请求之前需要进行判断,看是否有相应的处理权限,如果可以处理请求就处理它,否则将请求转发给后继者;在具体处理者中可以访问链中下一个对象,以便请求的转发。在职责链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织链和分配责任。 职责链模式的核心在于抽象处理者类的设计,抽象处理者的典型代码如下所示: abstract class Handler{ //维持对下家的引用 protected $successor; public function setSuccessor(Handler $successor) { $this->successor = $successor; } abstract public function handleRequest(string $request);}上述代码中,抽象处理者类定义了对下家的引用对象,以便将请求转发给下家,该对象的访问符可设为protected,在其子类中可以使用。在抽象处理者类中声明了抽象的请求处理方法,具体实现交由子类完成。 具体处理者是抽象处理者的子类,它具有两大作用:第一是处理请求,不同的具体处理者以不同的形式实现抽象请求处理方法handleRequest();第二是转发请求,如果该请求超出了当前处理者类的权限,可以将该请求转发给下家。具体处理者类的典型代码如下: class ConcreteHandler extends Handler{ public function handleRequest(string $request) { if (满足请求条件) { //处理请求 } else { //转发请求 $this->successor->handleRequest($request); } }}需要注意的是,职责链模式并不创建职责链,职责链的创建工作必须由系统的其他部分来完成,一般是在使用该职责链的客户端中创建职责链。职责链模式降低了请求的发送端和接收端之间的耦合,使多个对象都有机会处理这个请求。 案例Sunny软件公司承接了某企业SCM(Supply Chain Management,供应链管理)系统的开发任务,其中包含一个采购审批子系统。该企业的采购审批是分级进行的,即根据采购金额的不同由不同层次的主管人员来审批,主任可以审批5万元以下(不包括5万元)的采购单,副董事长可以审批5万元至10万元(不包括10万元)的采购单,董事长可以审批10万元至50万元(不包括50万元)的采购单,50万元及以上的采购单就需要开董事会讨论决定。如图所示: 如果采用一个处理类PurchaseRequestHandler统一处理所有审批请求的话,会造成以下几个问题: PurchaseRequestHandler类较为庞大,各个级别的审批方法都集中在一个类中,违反了“单一职责原则”,测试和维护难度大。如果需要增加一个新的审批级别或调整任何一级的审批金额和审批细节(例如将董事长的审批额度改为60万元)时都必须修改源代码并进行严格测试,此外,如果需要移除某一级别(例如金额为10万元及以上的采购单直接由董事长审批,不再设副董事长一职)时也必须对源代码进行修改,违反了“开闭原则”。审批流程的设置缺乏灵活性,现在的审批流程是“主任-->副董事长-->董事长-->董事会”,如果需要改为“主任-->董事长-->董事会”,在此方案中只能通过修改源代码来实现,客户端无法定制审批流程。为了让采购单的审批流程更加灵活,并实现采购单的链式传递和处理,Sunny公司开发人员使用职责链模式来实现采购单的分级审批,其基本结构如图所示: 抽象类Approver充当抽象处理者(抽象传递者),Director、VicePresident、President和Congress充当具体处理者(具体传递者),PurchaseRequest充当请求类。完整代码如下所示: <?php//采购单:请求类class PurchaseRequest{ private $amount; //采购金额 private $number; //采购单编号 private $purpose; //采购目的 public function __construct(float $amount, int $number, string $purpose) { $this->amount = $amount; $this->number = $number; $this->purpose = $purpose; } public function setAmount(float $amount) { $this->amount = $amount; } public function getAmount(): float { return $this->amount; } public function setNumber(int $number) { $this->number = $number; } public function getNumber(): int { return $this->number; } public function setPurpose(string $purpose) { $this->purpose = $purpose; } public function getPurpose(): string { return $this->purpose; }}//审批者类:抽象处理者abstract class Approver{ protected $successor; //定义后继对象 protected $name; //审批者姓名 public function __construct(string $name) { $this->name = $name; } //设置后继者 public function setSuccessor(Approver $successor) { $this->successor = $successor; } //抽象请求处理方法 abstract public function processRequest(PurchaseRequest $request);}//主任类:具体处理者class Director extends Approver{ public function __construct(string $name) { parent::__construct($name); } //具体请求处理方法 public function processRequest(PurchaseRequest $request) { if ($request->getAmount() < 50000) { //处理请求 echo '主任审批'; } else { //转发请求 $this->successor->processRequest($request); } }}//副董事长类:具体处理者class VicePresident extends Approver{ public function __construct(string $name) { parent::__construct($name); } //具体请求处理方法 public function processRequest(PurchaseRequest $request) { if ($request->getAmount() < 100000) { //处理请求 echo '副董事长审批'; } else { //转发请求 $this->successor->processRequest($request); } }}//董事长类:具体处理者class President extends Approver{ public function __construct(string $name) { parent::__construct($name); } //具体请求处理方法 public function processRequest(PurchaseRequest $request) { if ($request->getAmount() < 500000) { //处理请求 echo '董事长审批'; } else { //转发请求 $this->successor->processRequest($request); } }}//董事会类:具体处理者class Congress extends Approver{ public function __construct(string $name) { parent::__construct($name); } //具体请求处理方法 public function processRequest(PurchaseRequest $request) { echo '召开董事会审批'; }}class Client{ public function test() { $director = new Director('张主任'); $vicePresident = new VicePresident('杨副总'); $president = new President('王董事长'); $congress = new Congress('董事会'); //创建职责链 $director->setSuccessor($vicePresident); $vicePresident->setSuccessor($president); $president->setSuccessor($congress); //创建采购单 $pr1 = new PurchaseRequest(45000, 10001, '购买办公桌'); $director->processRequest($pr1); $pr2 = new PurchaseRequest(60000, 10002, '购买打印机'); $director->processRequest($pr2); $pr3 = new PurchaseRequest(160000, 10003, '购买台式机'); $director->processRequest($pr3); $pr4 = new PurchaseRequest(800000, 10004, '购买服务器'); $director->processRequest($pr4); }} 如果需要在系统增加一个新的具体处理者,如增加一个经理(Manager)角色可以审批5万元至8万元(不包括8万元)的采购单,需要编写一个新的具体处理者类Manager,作为抽象处理者类Approver的子类,实现在Approver类中定义的抽象处理方法,如果采购金额大于等于8万元,则将请求转发给下家。 ...

June 27, 2020 · 2 min · jiezi

架构设计模式享元模式

当前咱们国家正在大力倡导构建和谐社会,其中一个很重要的组成部分就是建设资源节约型社会,“浪费可耻,节俭光荣”。在软件系统中,有时候也会存在资源浪费的情况,例如在计算机内存中存储了多个完全相同或者非常相似的对象,如果这些对象的数量太多将导致系统运行代价过高,内存属于计算机的“稀缺资源”,不应该用来“随便浪费”,那么是否存在一种技术可以用于节约内存使用空间,实现对这些相同或者相似对象的共享访问呢?答案是肯定,这种技术就是我们将要学习的享元模式。 概述当一个软件系统在运行时产生的对象数量太多,将导致运行代价过高,带来系统性能下降等问题。例如在一个文本字符串中存在很多重复的字符,如果每一个字符都用一个单独的对象来表示,将会占用较多的内存空间,那么我们如何去避免系统中出现大量相同或相似的对象,同时又不影响客户端程序通过面向对象的方式对这些对象进行操作?享元模式正为解决这一类问题而诞生。享元模式通过共享技术实现相同或相似对象的重用,在逻辑上每一个出现的字符都有一个对象与之对应,然而在物理上它们却共享同一个享元对象,这个对象可以出现在一个字符串的不同地方,相同的字符对象都指向同一个实例,在享元模式中,存储这些共享实例对象的地方称为享元池(Flyweight Pool)。我们可以针对每一个不同的字符创建一个享元对象,将其放在享元池中,需要时再从享元池取出。如图所示: 享元模式以共享的方式高效地支持大量细粒度对象的重用,享元对象能做到共享的关键是区分了内部状态(Intrinsic State)和外部状态(Extrinsic State)。下面将对享元的内部状态和外部状态进行简单的介绍: 内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享。如字符的内容,不会随外部环境的变化而变化,无论在任何环境下字符“a”始终是“a”,都不会变成“b”。外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。如字符的颜色,可以在不同的地方有不同的颜色,例如有的“a”是红色的,有的“a”是绿色的,字符的大小也是如此,有的“a”是五号字,有的“a”是四号字。而且字符的颜色和大小是两个独立的外部状态,它们可以独立变化,相互之间没有影响,客户端可以在使用时将外部状态注入享元对象中。正因为区分了内部状态和外部状态,我们可以将具有相同内部状态的对象存储在享元池中,享元池中的对象是可以实现共享的,需要的时候就将对象从享元池中取出,实现对象的复用。通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象在内存中实际上只存储一份。 享元模式定义如下: 享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。享元模式结构较为复杂,一般结合工厂模式一起使用,在它的结构图中包含了一个享元工厂类,其结构图如图所示: 在享元模式结构图中包含如下几个角色: Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。在享元模式中引入了享元工厂类,享元工厂类的作用在于提供一个用于存储享元对象的享元池,当用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象。典型的享元工厂类的代码如下: class FlyweightFactory{ //存储享元对象,实现享元池 private $pool = []; public function getFlyweight(string $name): Flyweight { //如果对象不存在,先创建并添加到享元池中,再返回 if (!isset($this->pool[$name])) { $this->pool[$name] = new CharacterFlyweight($name); } return $this->pool[$name]; }}享元类的设计是享元模式的要点之一,在享元类中要将内部状态和外部状态分开处理,通常将内部状态作为享元类的成员变量,而外部状态通过注入的方式添加到享元类中。典型的享元类代码如下所示: abstract class Flyweight{ //内部状态intrinsicState作为成员变量 //同一个享元对象其内部状态是一致的 private $intrinsicState; public function __construct(string $intrinsicState) { $this->intrinsicState = $intrinsicState; } //外部状态extrinsicState在使用时由外部设置, //不保存在享元对象中, //即使是同一个对象,在每一次调用时也可以传入不同的外部状态 abstract public function operation(string $extrinsicState);}案例Sunny软件公司欲开发一个围棋软件,其界面效果如图所示: Sunny软件公司开发人员通过对围棋软件进行分析,发现在围棋棋盘中包含大量的黑子和白子,它们的形状、大小都一模一样,只是出现的位置不同而已。如果将每一个棋子都作为一个独立的对象存储在内存中,将导致该围棋软件在运行时所需内存空间较大,如何降低运行代价、提高系统性能是Sunny公司开发人员需要解决的一个问题。 为了解决这个问题,Sunny公司开发人员决定使用享元模式来设计该围棋软件的棋子对象,基本结构如图所示: ...

June 27, 2020 · 2 min · jiezi

架构设计模式外观模式

外观模式是一种使用频率非常高的结构型设计模式,它通过引入一个外观角色来简化客户端与子系统之间的交互,为复杂的子系统调用提供一个统一的入口,降低子系统与客户端的耦合度,且客户端调用非常方便。 概述在软件开发中,有时候为了完成一项较为复杂的功能,一个客户类需要和多个业务类交互,而这些需要交互的业务类经常会作为一个整体出现,由于涉及到的类比较多,导致使用时代码较为复杂。此时,特别需要一个类似服务员一样的角色,由它来负责和多个业务类进行交互,而客户类只需与该类交互。外观模式通过引入一个新的外观类(Facade)来实现该功能,外观类充当了软件系统中的“服务员”,它为多个业务类的调用提供了一个统一的入口,简化了类与类之间的交互。在外观模式中,那些需要交互的业务类被称为子系统(Subsystem)。如果没有外观类,那么每个客户类需要和多个子系统之间进行复杂的交互,系统的耦合度将很大;而引入外观类之后,客户类只需要直接与外观类交互,客户类与子系统之间原有的复杂引用关系由外观类来实现,从而降低了系统的耦合度。 外观模式中,一个子系统的外部与其内部的通信通过一个统一的外观类进行,外观类将客户类与子系统的内部复杂性分隔开,使得客户类只需要与外观角色打交道,而不需要与子系统内部的很多对象打交道。 外观模式定义如下: 外观模式:为子系统中的一组接口提供一个统一的入口。外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式。外观模式是迪米特法则的一种具体实现,通过引入一个新的外观角色可以降低原有系统的复杂度,同时降低客户类与子系统的耦合度。 外观模式没有一个一般化的类图描述,下面所示的类图可以作为描述外观模式的结构图: 外观模式包含如下两个角色: Facade(外观角色):在客户端可以调用它的方法,在外观角色中可以知道相关的(一个或者多个)子系统的功能和责任;在正常情况下,它将所有从客户端发来的请求委派到相应的子系统去,传递给相应的子系统对象处理。SubSystem(子系统角色):在软件系统中可以有一个或者多个子系统角色,每一个子系统可以不是一个单独的类,而是一个类的集合,它实现子系统的功能;每一个子系统都可以被客户端直接调用,或者被外观角色调用,它处理由外观类传过来的请求;子系统并不知道外观的存在,对于子系统而言,外观角色仅仅是另外一个客户端而已。引入外观模式之后,增加新的子系统或者移除子系统都非常方便,客户类无须进行修改(或者极少的修改),只需要在外观类中增加或移除对子系统的引用即可。从这一点来说,外观模式在一定程度上并不符合开闭原则,增加新的子系统需要对原有系统进行一定的修改,虽然这个修改工作量不大。 外观模式中所指的子系统是一个广义的概念,它可以是一个类、一个功能模块、系统的一个组成部分或者一个完整的系统。子系统类通常是一些业务类,实现了一些具体的、独立的业务功能,其典型代码如下: class SubSystemA{ public function MethodA() { //业务实现代码 }}class SubSystemB{ public function MethodB() { //业务实现代码 }}class SubSystemC{ public function MethodC() { //业务实现代码 }}在引入外观类之后,与子系统业务类之间的交互统一由外观类来完成,在外观类中通常存在如下代码: class Facade{ private $obj1; private $obj2; private $obj3; public function __construct() { $this->obj1 = new SubSystemA(); $this->obj2 = new SubSystemB(); $this->obj3 = new SubSystemC(); } public function Method() { $this->obj1->MethodA(); $this->obj2->MethodB(); $this->obj3->MethodC(); }}由于在外观类中维持了对子系统对象的引用,客户端可以通过外观类来间接调用子系统对象的业务方法,而无须与子系统对象直接交互。 案例某软件公司欲开发一个可应用于多个软件的文件加密模块,该模块可以对文件中的数据进行加密并将加密之后的数据存储在一个新文件中,具体的流程包括三个部分,分别是读取源文件、加密、保存加密之后的文件,其中,读取文件和保存文件使用流来实现,加密操作通过求模运算实现。这三个操作相对独立,为了实现代码的独立重用,让设计更符合单一职责原则,这三个操作的业务代码封装在三个不同的类中。现使用外观模式设计该文件加密模块。 ...

June 26, 2020 · 1 min · jiezi

架构设计模式装饰模式

尽管目前房价依旧很高,但还是阻止不了大家对新房的渴望和买房的热情。如果大家买的是毛坯房,无疑还有一项艰巨的任务要面对,那就是装修。对新房进行装修并没有改变房屋用于居住的本质,但它可以让房子变得更漂亮、更温馨、更实用、更能满足居家的需求。在软件设计中,我们也有一种类似新房装修的技术可以对已有对象(新房)的功能进行扩展(装修),以获得更加符合用户需求的对象,使得对象具有更加强大的功能。这种技术对应于一种被称之为装饰模式的设计模式。 概述装饰模式可以在不改变一个对象本身功能的基础上给对象增加额外的新行为,是一种用于替代继承的技术,它通过一种无须定义子类的方式来给对象动态增加职责,使用对象之间的关联关系取代类之间的继承关系。在装饰模式中引入了装饰类,在装饰类中既可以调用待装饰的原有类的方法,还可以增加新的方法,以扩充原有类的功能。 装饰模式定义如下: 装饰模式(Decorator Pattern):动态地给一个对象增加一些额外的职责,就增加对象功能来说,装饰模式比生成子类实现更为灵活。装饰模式是一种对象结构型模式。在装饰模式中,为了让系统具有更好的灵活性和可扩展性,我们通常会定义一个抽象装饰类,而将具体的装饰类作为它的子类,装饰模式结构如图所示: 在装饰模式结构图中包含如下几个角色: Component(抽象构件):它是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方式处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作。ConcreteComponent(具体构件):它是抽象构件类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责(方法)。Decorator(抽象装饰类):它也是抽象构件类的子类,用于给具体构件增加职责,但是具体职责在其子类中实现。它维护一个指向抽象构件对象的引用,通过该引用可以调用装饰之前构件对象的方法,并通过其子类扩展该方法,以达到装饰的目的。ConcreteDecorator(具体装饰类):它是抽象装饰类的子类,负责向构件添加新的职责。每一个具体装饰类都定义了一些新的行为,它可以调用在抽象装饰类中定义的方法,并可以增加新的方法用以扩充对象的行为。由于具体构件类和装饰类都实现了相同的抽象构件接口,因此装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。 装饰模式的核心在于抽象装饰类的设计,其典型代码如下所示: class Decorator implements Component{ //维持一个对抽象构件对象的引用 private $component; //注入一个抽象构件类型的对象 public function __construct(Component $component) { $this->component = $component; } public function operation() { //调用原有业务方法 $this->component->operation(); }}在抽象装饰类Decorator中定义了一个Component类型的对象component,维持一个对抽象构件对象的引用,并可以通过构造方法或Setter方法将一个Component类型的对象注入进来,同时由于Decorator类实现了抽象构件Component接口,因此需要实现在其中声明的业务方法operation(),需要注意的是在Decorator中并未真正实现operation()方法,而只是调用原有component对象的operation()方法,它没有真正实施装饰,而是提供一个统一的接口,将具体装饰过程交给子类完成。 在Decorator的子类即具体装饰类中将继承operation()方法并根据需要进行扩展,典型的具体装饰类代码如下: class ConcreteDecorator extends Decorator{ public function __construct(Component $component) { parent::__construct($component); } public function operation() { //调用原有业务方法 parent::operation(); //调用新增业务方法 $this->addedBehavior(); } //新增业务方法 public function addedBehavior() { ... }}在具体装饰类中可以调用到抽象装饰类的operation()方法,同时可以定义新的业务方法,如addedBehavior()。由于在抽象装饰类Decorator中注入的是Component类型的对象,因此我们可以将一个具体构件对象注入其中,再通过具体装饰类来进行装饰;此外,我们还可以将一个已经装饰过的Decorator子类的对象再注入其中进行多次装饰,从而对原有功能的多次扩展。 案例Sunny软件公司基于面向对象技术开发了一套图形界面构件库VisualComponent,该构件库提供了大量基本构件,如窗体、文本框、列表框等,由于在使用该构件库时,用户经常要求定制一些特效显示效果,如带滚动条的窗体、带黑色边框的文本框、既带滚动条又带黑色边框的列表框等等,因此经常需要对该构件库进行扩展以增强其功能,如图所示:如何提高图形界面构件库性的可扩展性并降低其维护成本是Sunny公司开发人员必须面对的一个问题。 如果使用基于继承的设计方案,会存在以下几个问题: 系统扩展麻烦,在某些编程语言中无法实现。代码重复。系统庞大,类的数目非常多。这些问题的根本原因在于复用机制的不合理,在复用父类的方法后再增加新的方法来扩展功能。根据“合成复用原则”,在实现功能复用时,我们要多用关联,少用继承。因此,Sunny公司开发人员决定使用装饰模式来重构图形界面构件库的设计,其中部分类的基本结构如图所示: Component充当抽象构件类,其子类Window、TextBox、ListBox充当具体构件类,Component类的另一个子类ComponentDecorator充当抽象装饰类,ComponentDecorator的子类ScrollBarDecorator和BlackBorderDecorator充当具体装饰类。完整代码如下所示: ...

June 26, 2020 · 1 min · jiezi

架构设计模式组合模式

树形结构在软件中随处可见,例如操作系统中的目录结构、应用软件中的菜单、办公系统中的公司组织结构等等,如何运用面向对象的方式来处理这种树形结构是组合模式需要解决的问题。 组合模式为处理树形结构提供了一种较为完美的解决方案,它描述了如何将容器和叶子进行递归组合,使得用户在使用时无须对它们进行区分,可以一致地对待容器和叶子。 概述对于树形结构,当容器对象(如文件夹)的某一个方法被调用时,将遍历整个树形结构,寻找也包含这个方法的成员对象(可以是容器对象,也可以是叶子对象)并调用执行,牵一而动百,其中使用了递归调用的机制来对整个结构进行处理。由于容器对象和叶子对象在功能上的区别,在使用这些对象的代码中必须有区别地对待容器对象和叶子对象,而实际上大多数情况下我们希望一致地处理它们,因为对于这些对象的区别对待将会使得程序非常复杂。组合模式为解决此类问题而诞生,它可以让叶子对象和容器对象的使用具有一致性。 组合模式定义如下: 组合模式(Composite Pattern):组合多个对象形成树形结构以表示具有“整体—部分”关系的层次结构。组合模式对单个对象(即叶子对象)和组合对象(即容器对象)的使用具有一致性,组合模式又可以称为“整体—部分”(Part-Whole)模式,它是一种对象结构型模式。在组合模式中引入了抽象构件类Component,它是所有容器类和叶子类的公共父类,客户端针对Component进行编程。组合模式结构如图所示: 在组合模式结构图中包含如下几个角色: Component(抽象构件):它可以是接口或抽象类,为叶子构件和容器构件对象声明接口,在该角色中可以包含所有子类共有行为的声明和实现。在抽象构件中定义了访问及管理它的子构件的方法,如增加子构件、删除子构件、获取子构件等。Leaf(叶子构件):它在组合结构中表示叶子节点对象,叶子节点没有子节点,它实现了在抽象构件中定义的行为。对于那些访问及管理子构件的方法,可以通过异常等方式进行处理。Composite(容器构件):它在组合结构中表示容器节点对象,容器节点包含子节点,其子节点可以是叶子节点,也可以是容器节点,它提供一个集合用于存储子节点,实现了在抽象构件中定义的行为,包括那些访问及管理子构件的方法,在其业务方法中可以递归调用其子节点的业务方法。组合模式的关键是定义了一个抽象构件类,它既可以代表叶子,又可以代表容器,而客户端针对该抽象构件类进行编程,无须知道它到底表示的是叶子还是容器,可以对其进行统一处理。同时容器对象与抽象构件类之间还建立一个聚合关联关系,在容器对象中既可以包含叶子,也可以包含容器,以此实现递归组合,形成一个树形结构。 如果不使用组合模式,客户端代码将过多地依赖于容器对象复杂的内部实现结构,容器对象内部实现结构的变化将引起客户代码的频繁变化,带来了代码维护复杂、可扩展性差等弊端。组合模式的引入将在一定程度上解决这些问题。 下面通过简单的示例代码来分析组合模式的各个角色的用途和实现。对于组合模式中的抽象构件角色,其典型代码如下所示: abstract class Component { public abstract function add(Component $c); //增加成员 public abstract function remove(Component $c); //删除成员 public abstract function getChild(int $i): Component; //获取成员 public abstract function operation(); //业务方法}一般将抽象构件类设计为接口或抽象类,将所有子类共有方法的声明和实现放在抽象构件类中。对于客户端而言,将针对抽象构件编程,而无须关心其具体子类是容器构件还是叶子构件。 如果继承抽象构件的是叶子构件,则其典型代码如下所示: class Leaf extends Component{ public function add(Component $c) { //异常处理或错误提示 } public function remove(Component $c) { //异常处理或错误提示 } public function getChild(int $i): Component { //异常处理或错误提示 } public function operation() { //叶子构件具体业务方法的实现 }}作为抽象构件类的子类,在叶子构件中需要实现在抽象构件类中声明的所有方法,包括业务方法以及管理和访问子构件的方法,但是叶子构件不能再包含子构件,因此在叶子构件中实现子构件管理和访问方法时需要提供异常处理或错误提示。当然,这无疑会给叶子构件的实现带来麻烦。 ...

June 26, 2020 · 2 min · jiezi

架构设计模式桥接模式

在正式介绍桥接模式之前,先跟大家谈谈两种常见文具的区别,它们是毛笔和蜡笔。假如我们需要大中小3种型号的画笔,能够绘制12种不同的颜色,如果使用蜡笔,需要准备3×12 = 36支,但如果使用毛笔的话,只需要提供3种型号的毛笔,外加12个颜料盒即可,涉及到的对象个数仅为 3 + 12 = 15,远小于36,却能实现与36支蜡笔同样的功能。如果增加一种新型号的画笔,并且也需要具有12种颜色,对应的蜡笔需增加12支,而毛笔只需增加一支。为什么会这样呢?通过分析我们可以得知:在蜡笔中,颜色和型号两个不同的变化维度(即两个不同的变化原因)融合在一起,无论是对颜色进行扩展还是对型号进行扩展都势必会影响另一个维度;但在毛笔中,颜色和型号实现了分离,增加新的颜色或者型号对另一方都没有任何影响。如果使用软件工程中的术语,我们可以认为在蜡笔中颜色和型号之间存在较强的耦合性,而毛笔很好地将二者解耦,使用起来非常灵活,扩展也更为方便。在软件开发中,我们也提供了一种设计模式来处理与画笔类似的具有多变化维度的情况,即将要介绍的桥接模式。 概述桥接模式是一种很实用的结构型设计模式,如果软件系统中某个类存在两个独立变化的维度,通过该模式可以将这两个维度分离出来,使两者可以独立扩展,让系统更加符合“单一职责原则”。与多层继承方案不同,它将两个独立变化的维度设计为两个独立的继承等级结构,并且在抽象层建立一个抽象关联,该关联关系类似一条连接两个独立继承结构的桥,故名桥接模式。 桥接模式用一种巧妙的方式处理多层继承存在的问题,用抽象关联取代了传统的多层继承,将类之间的静态继承关系转换为动态的对象组合关系,使得系统更加灵活,并易于扩展,同时有效控制了系统中类的个数。桥接定义如下: 桥接模式(Bridge Pattern):将抽象部分与它的实现部分分离,使它们都可以独立地变化。它是一种对象结构型模式,又称为柄体(Handle and Body)模式或接口(Interface)模式。桥接模式的结构与其名称一样,存在一条连接两个继承等级结构的桥,桥接模式结构如图所示: 在桥接模式结构图中包含如下几个角色: Abstraction(抽象类):用于定义抽象类的接口,它一般是抽象类而不是接口,其中定义了一个Implementor(实现类接口)类型的对象并可以维护该对象,它与Implementor之间具有关联关系,它既可以包含抽象业务方法,也可以包含具体业务方法。RefinedAbstraction(扩充抽象类):扩充由Abstraction定义的接口,通常情况下它不再是抽象类而是具体类,它实现了在Abstraction中声明的抽象业务方法,在RefinedAbstraction中可以调用在Implementor中定义的业务方法。Implementor(实现类接口):定义实现类的接口,这个接口不一定要与Abstraction的接口完全一致,事实上这两个接口可以完全不同,一般而言,Implementor接口仅提供基本操作,而Abstraction定义的接口可能会做更多更复杂的操作。Implementor接口对这些基本操作进行了声明,而具体实现交给其子类。通过关联关系,在Abstraction中不仅拥有自己的方法,还可以调用到Implementor中定义的方法,使用关联关系来替代继承关系。ConcreteImplementor(具体实现类):具体实现Implementor接口,在不同的ConcreteImplementor中提供基本操作的不同实现,在程序运行时,ConcreteImplementor对象将替换其父类对象,提供给抽象类具体的业务操作方法。桥接模式是一个非常有用的模式,在桥接模式中体现了很多面向对象设计原则的思想,包括“单一职责原则”、“开闭原则”、“合成复用原则”、“里氏代换原则”、“依赖倒转原则”等。熟悉桥接模式有助于我们深入理解这些设计原则,也有助于我们形成正确的设计思想和培养良好的设计风格。 在使用桥接模式时,我们首先应该识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。通常情况下,我们将具有两个独立变化维度的类的一些普通业务方法和与之关系最密切的维度设计为“抽象类”层次结构(抽象部分),而将另一个维度设计为“实现类”层次结构(实现部分)。例如:对于毛笔而言,由于型号是其固有的维度,因此可以设计一个抽象的毛笔类,在该类中声明并部分实现毛笔的业务方法,而将各种型号的毛笔作为其子类;颜色是毛笔的另一个维度,由于它与毛笔之间存在一种“设置”的关系,因此我们可以提供一个抽象的颜色接口,而将具体的颜色作为实现该接口的子类。在此,型号可认为是毛笔的抽象部分,而颜色是毛笔的实现部分,结构示意图如图所示:

June 26, 2020 · 1 min · jiezi

架构设计模式适配器模式

在软件开发中,有时存在结构之间不兼容的情况,我们也可以像引入一个电源适配器一样引入一个称之为适配器的角色来协调这些存在不兼容的结构,这种设计方案即为适配器模式。 概述与电源适配器相似,在适配器模式中引入了一个被称为适配器(Adapter)的包装类,而它所包装的对象称为适配者(Adaptee),即被适配的类。适配器的实现就是把客户类的请求转化为对适配者的相应接口的调用。也就是说:当客户类调用适配器的方法时,在适配器类的内部将调用适配者类的方法,而这个过程对客户类是透明的,客户类并不直接访问适配者类。因此,适配器让那些由于接口不兼容而不能交互的类可以一起工作。 适配器模式可以将一个类的接口和另一个类的接口匹配起来,而无须修改原来的适配者接口和抽象目标类接口。适配器模式定义如下: 适配器模式(Adapter Pattern):将一个接口转换成客户希望的另一个接口,使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。注:在适配器模式定义中所提及的接口是指广义的接口,它可以表示一个方法或者方法的集合。 根据适配器类与适配者类的关系不同,适配器模式可分为对象适配器和类适配器两种,在对象适配器模式中,适配器与适配者之间是关联关系;在类适配器模式中,适配器与适配者之间是继承(或实现)关系。在实际开发中,对象适配器的使用频率更高,对象适配器模式结构如图所示: 在对象适配器模式结构图中包含如下几个角色: Target(目标抽象类):目标抽象类定义客户所需接口,可以是一个抽象类或接口,也可以是具体类。Adapter(适配器类):适配器可以调用另一个接口,作为一个转换器,对Adaptee和Target进行适配,适配器类是适配器模式的核心,在对象适配器中,它通过继承或实现Target并关联一个Adaptee对象使二者产生联系。Adaptee(适配者类):适配者即被适配的角色,它定义了一个已经存在的接口,这个接口需要适配,适配者类一般是一个具体类,包含了客户希望使用的业务方法,在某些情况下可能没有适配者类的源代码。在对象适配器中,客户端需要调用request()方法,而适配者类Adaptee没有该方法,但是它所提供的specificRequest()方法却是客户端所需要的。为了使客户端能够使用适配者类,需要提供一个包装类Adapter,即适配器类。这个包装类包装了一个适配者的实例,从而将客户端与适配者衔接起来,在适配器的request()方法中调用适配者的specificRequest()方法。因为适配器类与适配者类是关联关系(也可称之为委派关系),所以这种适配器模式称为对象适配器模式。典型的对象适配器代码如下所示: <?phpclass Adapter extends Target{ //维持一个对适配者对象的引用 private $adaptee; public function __construct($adaptee) { $this->adaptee = $adaptee; } public function request() { //转发调用 $this->adaptee->specificRequest(); }}案例Sunny软件公司在很久以前曾开发了一个算法库,里面包含了一些常用的算法,例如排序算法和查找算法,在进行各类软件开发时经常需要重用该算法库中的算法。在为某学校开发教务管理系统时,开发人员发现需要对学生成绩进行排序和查找,该系统的设计人员已经开发了一个成绩操作接口ScoreOperation,在该接口中声明了排序方法sort()和查找方法search()。为了提高排序和查找的效率,开发人员决定重用算法库中的快速排序算法类QuickSort和二分查找算法类BinarySearch。由于某些原因,现在Sunny公司开发人员已经找不到该算法库的源代码,无法直接通过复制和粘贴操作来重用其中的代码;部分开发人员已经针对ScoreOperation接口编程,如果再要求对该接口进行修改或要求大家直接使用QuickSort类和BinarySearch类将导致大量代码需要修改。Sunny软件公司开发人员面对这个没有源码的算法库,遇到一个幸福而又烦恼的问题:如何在既不修改现有接口又不需要任何算法库代码的基础上能够实现算法库的重用?Sunny软件公司开发人员决定使用适配器模式来重用算法库中的算法,其基本结构如图所示: ScoreOperation接口充当抽象目标,QuickSort和BinarySearch类充当适配者,OperationAdapter充当适配器。完整代码如下所示: <?php//抽象成绩操作类:目标接口interface ScoreOperation{ //成绩排序 public function sort(array $array): array; //成绩查找 public function search(array $array, int $key): int;}//快速排序类:适配者class QuickSort{ public function _quickSort(array $array): array { $this->sort($array, 0, count($array) - 1); return $array; } public function sort(array $array, int $p, int $r) { $q = 0; if ($p < $r) { $q = $this->partition($array, $p, $r); $this->sort($array, $p, $q - 1); $this->sort($array, $q + 1, $r); } } public function partition(array $a, int $p, int $r): int { $x = $a[$r]; $j = $p - 1; for ($i = $p; $i <= $r - 1; $i++) { if ($a[$i] <= $x) { $j++; $this->swap($a, $j, $i); } } $this->swap($a, $j + 1, $r); return $j + 1; } public function swap(array $a, int $i, int $j) { $t = $a[$i]; $a[$i] = $a[$j]; $a[$j] = $t; }}//二分查找类:适配者class BinarySearch{ public function _binarySearch(array $array, int $key): int { $low = 0; $high = count($array) - 1; while ($low <= $high) { $mid = ($low + $high) / 2; $midVal = $array[$mid]; if ($midVal < $key) { $low = $mid + 1; } elseif ($midVal > $key) { $high = $mid - 1; } else { return 1; } } return -1; }}//操作适配器:适配器class OperationAdapter implements ScoreOperation{ //定义适配者QuickSort对象 private $sortObj; //定义适配者BinarySearch对象 private $searchObj; public function __construct() { $this->sortObj = new QuickSort(); $this->searchObj = new BinarySearch(); } public function sort(array $array): array { return $this->sortObj->_quickSort($array); } public function search(array $array, int $key): int { return $this->searchObj->_binarySearch($array, $key); }}为了让系统具备良好的灵活性和可扩展性,我们引入了工具类ConfigUtil和配置文件,将适配器类的类名存储在配置文件中。如果需要使用其他排序算法类和查找算法类,可以增加一个新的适配器类,使用新的适配器来适配新的算法,原有代码无须修改。通过引入配置文件和反射机制,可以在不修改客户端代码的情况下使用新的适配器,无须修改源代码,符合“开闭原则”。 ...

June 25, 2020 · 2 min · jiezi

架构设计模式建造者模式

没有人买车会只买一个轮胎或者方向盘,大家买的都是一辆包含轮胎、方向盘和发动机等多个部件的完整汽车。如何将这些部件组装成一辆完整的汽车并返回给用户,这是建造者模式需要解决的问题。建造者模式又称为生成器模式,它是一种较为复杂、使用频率也相对较低的创建型模式。建造者模式为客户端返回的不是一个简单的产品,而是一个由多个部件组成的复杂产品。 概述建造者模式是较为复杂的创建型模式,它将客户端与包含多个组成部分(或部件)的复杂对象的创建过程分离,客户端无须知道复杂对象的内部组成部分与装配方式,只需要知道所需建造者的类型即可。它关注如何一步一步创建一个的复杂对象,不同的具体建造者定义了不同的创建过程,且具体建造者相互独立,增加新的建造者非常方便,无须修改已有代码,系统具有较好的扩展性。 建造者模式定义如下: 建造者模式(Builder Pattern):将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。建造者模式是一种对象创建型模式。建造者模式一步一步创建一个复杂的对象,它允许用户只通过指定复杂对象的类型和内容就可以构建它们,用户不需要知道内部的具体构建细节。建造者模式结构如图所示: 在建造者模式结构图中包含如下几个角色: Builder(抽象建造者):它为创建一个产品Product对象的各个部件指定抽象接口,在该接口中一般声明两类方法,一类方法是buildPartX(),它们用于创建复杂对象的各个部件;另一类方法是getResult(),它们用于返回复杂对象。Builder既可以是抽象类,也可以是接口。ConcreteBuilder(具体建造者):它实现了Builder接口,实现各个部件的具体构造和装配方法,定义并明确它所创建的复杂对象,也提供一个方法返回创建好的复杂产品对象。Product(产品角色):它是被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义它的装配过程。Director(指挥者):指挥者又称为导演类,它负责安排复杂对象的建造次序,指挥者与抽象建造者之间存在关联关系,可以在其construct()建造方法中调用建造者对象的部件构造与装配方法,完成复杂对象的建造。客户端一般只需要与指挥者进行交互,在客户端确定具体建造者的类型,并实例化具体建造者对象(也可以通过配置文件和反射机制),然后通过指挥者类的构造函数或者Setter方法将该对象传入指挥者类中。在建造者模式的定义中提到了复杂对象,那么什么是复杂对象?简单来说,复杂对象是指那些包含多个成员属性的对象,这些成员属性也称为部件或零件,如汽车包括方向盘、发动机、轮胎等部件,电子邮件包括发件人、收件人、主题、内容、附件等部件,一个典型的复杂对象类代码示例如下: <?phpclass Product{ //定义部件,部件可以是任意类型,包括值类型和引用类型 private $partA; private $partB; private $partC;}在抽象建造者类中定义了产品的创建方法和返回方法,其典型代码如下: <?phpabstract class Builder{ //产品对象 private $product; public abstract function buildPartA(); public abstract function buildPartB(); public abstract function buildPartC(); //创建产品对象 public abstract function createProduct(); //返回产品对象 public function getResult(): Product { return $this->product; }}在抽象类Builder中声明了一系列抽象的buildPartX()方法用于创建复杂产品的各个部件,具体建造过程在ConcreteBuilder中实现,此外还提供了工厂方法getResult(),用于返回一个建造好的完整产品。 在ConcreteBuilder中实现了buildPartX()方法,不同的具体建造者在实现buildPartX()方法时将有所区别,而这些对于客户端来说都无须关心,客户端只需知道具体建造者类型即可。 在建造者模式的结构中还引入了一个指挥者类Director,该类主要有两个作用:一方面它隔离了客户与创建过程;另一方面它控制产品的创建过程,包括某个buildPartX()方法是否被调用以及多个buildPartX()方法调用的先后次序等。指挥者针对抽象建造者编程,客户端只需要知道具体建造者的类型,即可通过指挥者类调用建造者的相关方法,返回一个完整的产品对象。在实际生活中也存在类似指挥者一样的角色,如一个客户去购买电脑,电脑销售人员相当于指挥者,只要客户确定电脑的类型,电脑销售人员可以通知电脑组装人员给客户组装一台电脑。指挥者类的代码示例如下: <?phpclass Director{ private $builder; public function __construct(Builder $builder) { $this->builder = $builder; } public function setBuilder(Builder $builder) { $this->builder = $builder; } //产品构建与组装方法 public function build(): Product { $this->builder->createProduct(); $this->builder->buildPartA(); $this->builder->buildPartB(); $this->builder->buildPartC(); return $this->builder->getResult(); }}在指挥者类中可以注入一个抽象建造者类型的对象,其核心在于提供了一个建造方法build(),在该方法中调用了builder对象的构造部件的方法,最后返回一个产品对象。 ...

June 25, 2020 · 2 min · jiezi

架构设计模式原型模式

在设计模式中存在一个模式,可以通过一个原型对象克隆出多个一模一样的对象,该模式称之为原型模式。 概述原型模式的定义如下: 原型模式(Prototype  Pattern):使用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。原型模式是一种对象创建型模式。原型模式的工作原理很简单:将一个原型对象传给那个要发动创建的对象,这个要发动创建的对象通过请求原型对象拷贝自己来实现创建过程。由于在软件系统中我们经常会遇到需要创建多个相同或者相似对象的情况,因此原型模式在真实开发中的使用频率还是非常高的。原型模式是一种“另类”的创建型模式,创建克隆对象的工厂就是原型类自身,工厂方法由克隆方法来实现。 需要注意的是通过克隆方法所创建的对象是全新的对象,它们在内存中拥有新的地址,通常对克隆所产生的对象进行修改对原型对象不会造成任何影响,每一个克隆对象都是相互独立的。通过不同的方式修改可以得到一系列相似但不完全相同的对象。 原型模式的结构如图所示: 在原型模式结构图中包含如下几个角色: Prototype(抽象原型类):它是声明克隆方法的接口,是所有具体原型类的公共父类,可以是抽象类也可以是接口,甚至还可以是具体实现类。ConcretePrototype(具体原型类):它实现在抽象原型类中声明的克隆方法,在克隆方法中返回自己的一个克隆对象。Client(客户类):让一个原型对象克隆自身从而创建一个新的对象,在客户类中只需要直接实例化或通过工厂方法等方式创建一个原型对象,再通过调用该对象的克隆方法即可得到多个相同的对象。由于客户类针对抽象原型类Prototype编程,因此用户可以根据需要选择具体原型类,系统具有较好的可扩展性,增加或更换具体原型类都很方便。注:在PHP语言中,可以使用clone关键字克隆一个对象,此处不再举例,只需要注意在具体原型类中实现特定的__clone方法即可。 总结原型模式作为一种快速创建大量相同或相似对象的方式,在软件开发中应用较为广泛,很多软件提供的复制(Ctrl + C)和粘贴(Ctrl + V)操作就是原型模式的典型应用。 主要优点当创建新的对象实例较为复杂时,使用原型模式可以简化对象的创建过程,通过复制一个已有实例可以提高新实例的创建效率。扩展性较好,由于在原型模式中提供了抽象原型类,在客户端可以针对抽象原型类进行编程,而将具体原型类写在配置文件中,增加或减少产品类对原有系统都没有任何影响。原型模式提供了简化的创建结构,工厂方法模式常常需要有一个与产品类等级结构相同的工厂等级结构,而原型模式就不需要这样,原型模式中产品的复制是通过封装在原型类中的克隆方法实现的,无须专门的工厂类来创建产品。可以使用深克隆的方式保存对象的状态,使用原型模式将对象复制一份并将其状态保存起来,以便在需要的时候使用(如恢复到某一历史状态),可辅助实现撤销操作。主要缺点需要为每一个类配备一个克隆方法,而且该克隆方法位于一个类的内部,当对已有的类进行改造时,需要修改源代码,违背了“开闭原则”。在实现深克隆时需要编写较为复杂的代码,而且当对象之间存在多重的嵌套引用时,为了实现深克隆,每一层对象对应的类都必须支持深克隆,实现起来可能会比较麻烦。适用场景创建新对象成本较大(如初始化需要占用较长的时间,占用太多的CPU资源或网络资源),新的对象可以通过原型模式对已有对象进行复制来获得,如果是相似对象,则可以对其成员变量稍作修改。如果系统要保存对象的状态,而对象的状态变化很小,或者对象本身占用内存较少时,可以使用原型模式配合备忘录模式来实现。需要避免使用分层次的工厂类来创建分层次的对象,并且类的实例对象只有一个或很少的几个组合状态,通过复制原型对象得到新实例可能比使用构造函数创建一个新实例更加方便。

June 25, 2020 · 1 min · jiezi

架构设计的五视图

June 23, 2020 · 0 min · jiezi

架构设计-高并发流量削峰共享资源加锁机制

本文源码:GitHub·点这里 || GitEE·点这里 一、高并发简介在互联网的业务架构中,高并发是最难处理的业务之一,常见的使用场景:秒杀,抢购,订票系统;高并发的流程中需要处理的复杂问题非常多,主要涉及下面几个方面: 流量管理,逐级承接削峰;网关控制,路由请求,接口熔断;并发控制机制,资源加锁;分布式架构,隔离服务和数据库;高并发业务核心还是流量控制,控制流量下沉速度,或者控制承接流量的容器大小,多余的直接溢出,这是相对复杂的流程。其次就是多线程并发下访问共享资源,该流程需要加锁机制,避免数据写出现错乱情况。 二、秒杀场景1、预抢购业务活动未正式开始,先进行活动预约,先把一部分流量收集和控制起来,在真正秒杀的时间点,很多数据可能都已经预处理好了,可以很大程度上削减系统的压力。有了一定预约流量还可以提前对库存系统做好准备,一举两得。 场景:活动预约,定金预约,高铁抢票预购。 2、分批抢购分批抢购和抢购的场景实现的机制是一致的,只是在流量上缓解了很多压力,秒杀10W件库存和秒杀100件库存系统的抗压不是一个级别。如果秒杀10W件库存,系统至少承担多于10W几倍的流量冲击,秒杀100件库存,体系可能承担几百或者上千的流量就结束了。下面流量削峰会详解这里的策略机制。 场景:分时段多场次抢购,高铁票分批放出。 3、实时秒杀最有难度的场景就是准点实时的秒杀活动,假如10点整准时抢1W件商品,在这个时间点前后会涌入高并发的流量,刷新页面,或者请求抢购的接口,这样的场景处理起来是最复杂的。 首先系统要承接住流量的涌入;页面的不断刷新要实时加载;高并发请求的流量控制加锁等;服务隔离和数据库设计的系统保护;场景:618准点抢购,双11准点秒杀,电商促销秒杀。 三、流量削峰 1、Nginx代理Nginx是一个高性能的HTTP和反向代理web服务器,经常用在集群服务中做统一代理层和负载均衡策略,也可以作为一层流量控制层,提供两种限流方式,一是控制速率,二是控制并发连接数。 基于漏桶算法,提供限制请求处理速率能力;限制IP的访问频率,流量突然增大时,超出的请求将被拒绝;还可以限制并发连接数。 高并发的秒杀场景下,经过Nginx层的各种限制策略,可以控制流量在一个相对稳定的状态。 2、CDN节点CDN静态文件的代理节点,秒杀场景的服务有这样一个操作特点,活动倒计时开始之前,大量的用户会不断的刷新页面,这时候静态页面可以交给CDN层面代理,分担数据服务接口的压力。 CDN层面也可以做一层限流,在页面内置一层策略,假设有10W用户点击抢购,可以只放行1W的流量,其他的直接提示活动结束即可,这也是常用的手段之一。 话外之意:平时参与的抢购活动,可能你的请求根本没有到达数据接口层面,就极速响应商品已抢完,自行意会吧。 3、网关控制网关层面处理服务接口路由,一些校验之外,最主要的是可以集成一些策略进入网关,比如经过上述层层的流量控制之后,请求已经接近核心的数据接口,这时在网关层面内置一些策略控制:如果活动是想激活老用户,网关层面快速判断用户属性,老用户会放行请求;如果活动的目的是拉新,则放行更多的新用户。 经过这些层面的控制,剩下的流量已经不多了,后续才真正开始执行抢购的数据操作。 话外之意:如果有10W人参加抢购活动,真正下沉到底层的抢购流量可能就1W,甚至更少,在分散到集群服务中处理。 4、并发熔断在分布式服务的接口中,还有最精细的一层控制,对于一个接口在单位之间内控制请求处理的数量,这个基于接口的响应时间综合考虑,响应越快,单位时间内的并发量就越高,这里逻辑不难理解。 言外之意:流量经过层层控制,数据接口层面分担的压力已经不大,这时候就是面对秒杀业务中的加锁问题了。 四、分布式加锁1、悲观锁机制描述 所有请求的线程必须在获取锁之后,才能执行数据库操作,并且基于序列化的模式,没有获取锁的线程处于等待状态,并且设定重试机制,在单位时间后再次尝试获取锁,或者直接返回。 过程图解 Redis基础命令 SETNX:加锁的思路是,如果key不存在,将key设置为value如果key已存在,则 SETNX 不做任何动作。并且可以给key设置过期时间,过期后其他线程可以继续尝试锁获取机制。 借助Redis的该命令模拟锁的获取动作。 代码实现 这里基于Redis实现的锁获取和释放机制。 import org.springframework.stereotype.Component;import redis.clients.jedis.Jedis;import javax.annotation.Resource;@Componentpublic class RedisLock { @Resource private Jedis jedis ; /** * 获取锁 */ public boolean getLock (String key,String value,long expire){ try { String result = jedis.set( key, value, "nx", "ex", expire); return result != null; } catch (Exception e){ e.printStackTrace(); }finally { if (jedis != null) jedis.close(); } return false ; } /** * 释放锁 */ public boolean unLock (String key){ try { Long result = jedis.del(key); return result > 0 ; } catch (Exception e){ e.printStackTrace(); }finally { if (jedis != null) jedis.close(); } return false ; }}这里基于Jedis的API实现,这里提供一份配置文件。 ...

June 22, 2020 · 2 min · jiezi

架构设计模式单例模式

对于一个软件系统的某些类而言,我们无须创建多个实例。举个大家都熟知的例子——Windows任务管理器,通常情况下,无论我们启动任务管理多少次,Windows系统始终只能弹出一个任务管理器窗口,也就是说在一个Windows系统中,任务管理器存在唯一性。 回到实际开发中,我们也经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在。 概述单例模式定义如下: 单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。 单例模式是结构最简单的设计模式一,在它的核心结构中只包含一个被称为单例类的特殊类。单例模式结构如图所示: 单例模式结构图中只包含一个单例角色: Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。案例Sunny软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键。Sunny公司开发人员通过分析和权衡,决定使用单例模式来设计该负载均衡器,结构图如下: 将负载均衡器LoadBalancer设计为单例类,其中包含一个存储服务器信息的集合serverList,每次在serverList中随机选择一台服务器来响应客户端的请求,实现代码如下所示: <?phpfinal class LoadBalancer{ //私有静态成员变量,存储唯一实例 private static $instance = null; //服务器集合 private $serverList = []; //私有构造函数 private function __construct() { } //公有静态成员方法,返回唯一实例 public static function getLoadBalancer() { if (static::$instance == null) { static::$instance = new static(); } return static::$instance; } //增加服务器 public function addServer($server) { $this->serverList[] = $server; } //删除服务器 public function removeServer($server) { if ($key = array_search($server, $this->serverList)) { unset($this->serverList[$key]); } } //随机获取服务器 public function getServer(): string { $index = mt_rand(0, count($this->serverList)); return (string)$this->serverList[$index]; } //禁止克隆 private function __clone() { } //禁止反序列化 private function __wakeup() { }}总结单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中使用频率相当高,在很多应用软件和框架中都得以广泛应用。 ...

June 21, 2020 · 1 min · jiezi

架构设计模式抽象工厂模式

工厂方法模式通过引入工厂等级结构,解决了简单工厂模式中工厂类职责太重的问题,但由于工厂方法模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。此时,我们可以考虑将一些相关的产品组成一个“产品族”,由同一个工厂来统一生产,这就是我们本文将要学习的抽象工厂模式的基本思想。 产品等级结构与产品族在工厂方法模式中具体工厂负责生产具体的产品,每一个具体工厂对应一种具体产品,工厂方法具有唯一性,一般情况下,一个具体工厂中只有一个或者一组重载的工厂方法。但是有时候我们希望一个工厂可以提供多个产品对象,而不是单一的产品对象,如一个电器工厂,它可以生产电视机、电冰箱、空调等多种电器,而不是只生产某一种电器。为了更好地理解抽象工厂模式,我们先引入两个概念: 产品等级结构:产品等级结构即产品的继承结构,如一个抽象类是电视机,其子类有海尔电视机、海信电视机、TCL电视机,则抽象电视机与具体品牌的电视机之间构成了一个产品等级结构,抽象电视机是父类,而具体品牌的电视机是其子类。产品族:在抽象工厂模式中,产品族是指由同一个工厂生产的,位于不同产品等级结构中的一组产品,如海尔电器工厂生产的海尔电视机、海尔电冰箱,海尔电视机位于电视机产品等级结构中,海尔电冰箱位于电冰箱产品等级结构中,海尔电视机、海尔电冰箱构成了一个产品族。产品等级结构与产品族示意图如图所示: 在图中,不同颜色的多个正方形、圆形和椭圆形分别构成了三个不同的产品等级结构,而相同颜色的正方形、圆形和椭圆形构成了一个产品族,每一个形状对象都位于某个产品族,并属于某个产品等级结构。只要指明一个产品所处的产品族以及它所属的等级结构,就可以唯一确定这个产品。 当系统所提供的工厂生产的具体产品并不是一个简单的对象,而是多个位于不同产品等级结构、属于不同类型的具体产品时就可以使用抽象工厂模式。抽象工厂模式是所有形式的工厂模式中最为抽象和最具一般性的一种形式。抽象工厂模式与工厂方法模式最大的区别在于,工厂方法模式针对的是一个产品等级结构,而抽象工厂模式需要面对多个产品等级结构,一个工厂等级结构可以负责多个不同产品等级结构中的产品对象的创建。当一个工厂等级结构可以创建出分属于不同产品等级结构的一个产品族中的所有对象时,抽象工厂模式比工厂方法模式更为简单、更有效率。抽象工厂模式示意图如下: 每一个具体工厂可以生产属于一个产品族的所有产品,例如生产颜色相同的正方形、圆形和椭圆形,所生产的产品又位于不同的产品等级结构中。如果使用工厂方法模式,图4所示结构需要提供15个具体工厂,而使用抽象工厂模式只需要提供5个具体工厂,极大减少了系统中类的个数。 概述抽象工厂模式为创建一组对象提供了一种解决方案。与工厂方法模式相比,抽象工厂模式中的具体工厂不只是创建一种产品,它负责创建一族产品。抽象工厂模式定义如下: 抽象工厂模式(Abstract Factory Pattern):提供一个创建一系列相关或相互依赖对象的接口,而无须指定它们具体的类。抽象工厂模式又称为Kit模式,它是一种对象创建型模式。在抽象工厂模式中,每一个具体工厂都提供了多个工厂方法用于产生多种不同类型的产品,这些产品构成了一个产品族,抽象工厂模式结构如图所示:

June 21, 2020 · 1 min · jiezi

架构设计模式工厂方法模式

简单工厂模式虽然简单,但存在一个很严重的问题。当系统中需要引入新产品时,由于静态工厂方法通过所传入参数的不同来创建不同的产品,这必定要修改工厂类的源代码,将违背“开闭原则”,如何实现增加新产品而不影响已有代码?工厂方法模式应运而生,本文将介绍第二种工厂模式——工厂方法模式。 概述在简单工厂模式中只提供一个工厂类,该工厂类处于对产品类进行实例化的中心位置,它需要知道每一个产品对象的创建细节,并决定何时实例化哪一个产品类。简单工厂模式最大的缺点是当有新产品要加入到系统中时,必须修改工厂类,需要在其中加入必要的业务逻辑,这违背了“开闭原则”。此外,在简单工厂模式中,所有的产品都由同一个工厂创建,工厂类职责较重,业务逻辑较为复杂,具体产品与工厂类之间的耦合度高,严重影响了系统的灵活性和扩展性,而工厂方法模式则可以很好地解决这一问题。 在工厂方法模式中,我们不再提供一个统一的工厂类来创建所有的产品对象,而是针对不同的产品提供不同的工厂,系统提供一个与产品等级结构对应的工厂等级结构。工厂方法模式定义如下: 工厂方法模式(Factory Method Pattern):定义一个用于创建对象的接口,让子类决定将哪一个类实例化。工厂方法模式让一个类的实例化延迟到其子类。工厂方法模式又简称为工厂模式(Factory Pattern),又可称作虚拟构造器模式(Virtual Constructor Pattern)或多态工厂模式(Polymorphic Factory Pattern)。工厂方法模式是一种类创建型模式。 工厂方法模式提供一个抽象工厂接口来声明抽象工厂方法,而由其子类来具体实现工厂方法,创建具体的产品对象。工厂方法模式结构如图所示: 在工厂方法模式结构图中包含如下几个角色: Product(抽象产品):它是定义产品的接口,是工厂方法模式所创建对象的超类型,也就是产品对象的公共父类。ConcreteProduct(具体产品):它实现了抽象产品接口,某种类型的具体产品由专门的具体工厂创建,具体工厂和具体产品之间一一对应。Factory(抽象工厂):在抽象工厂类中,声明了工厂方法(Factory Method),用于返回一个产品。抽象工厂是工厂方法模式的核心,所有创建对象的工厂类都必须实现该接口。ConcreteFactory(具体工厂):它是抽象工厂类的子类,实现了抽象工厂中定义的工厂方法,并可由客户端调用,返回一个具体产品类的实例。与简单工厂模式相比,工厂方法模式最重要的区别是引入了抽象工厂角色,抽象工厂可以是接口,也可以是抽象类或者具体类,其典型代码如下所示: interface Factory { public function factoryMethod();}在抽象工厂中声明了工厂方法但并未实现工厂方法,具体产品对象的创建由其子类负责,客户端针对抽象工厂编程,可在运行时再指定具体工厂类,具体工厂类实现了工厂方法,不同的具体工厂可以创建不同的具体产品,其典型代码如下所示: class ConcreteFactory implements Factory{ public function factoryMethod(): ConcreteProduct { return new ConcreteProduct(); }}在实际使用时,具体工厂类在实现工厂方法时除了创建具体产品对象之外,还可以负责产品对象的初始化工作以及一些资源和环境配置工作,例如连接数据库、创建文件等。 在客户端代码中,只需关心工厂类即可,不同的具体工厂可以创建不同的产品,典型的客户端类代码片段如下所示: //可通过配置文件实现$factory = new ConcreteFactory();$product = $factory->factoryMethod();可以通过配置文件来存储具体工厂类ConcreteFactory的类名,更换新的具体工厂时无须修改源代码,系统扩展更为方便。 案例Sunny软件公司欲开发一个系统运行日志记录器(Logger),该记录器可以通过多种途径保存系统的运行日志,如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,Sunny公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置过程较为复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。如何封装记录器的初始化过程并保证多种记录器切换的灵活性是Sunny公司开发人员面临的一个难题。Sunny公司开发人员决定使用工厂方法模式来设计日志记录器,其基本结构如图所示: Logger接口充当抽象产品,其子类FileLogger和DatabaseLogger充当具体产品,LoggerFactory接口充当抽象工厂,其子类FileLoggerFactory和DatabaseLoggerFactory充当具体工厂。完整代码如下所示: <?php//日志记录器接口:抽象产品interface Logger{ public function writeLog();}//数据库日志记录器:具体产品class DatabaseLogger implements Logger{ public function writeLog() { echo '数据库日志记录'; }}//文件日志记录器:具体产品class FileLogger implements Logger{ public function writeLog() { echo '文件日志记录'; }}//日志记录器工厂接口:抽象工厂interface LoggerFactory{ public function createLogger(): Logger;}//数据库日志记录器工厂类:具体工厂class DatabaseLoggerFactory implements LoggerFactory{ public function createLogger(): Logger { //连接数据库,代码省略 //创建数据库日志记录器对象 $logger = new DatabaseLogger(); //初始化数据库日志记录器,代码省略 return $logger; }}//文件日志记录器工厂类:具体工厂class FileLoggerFactory implements LoggerFactory{ public function createLogger(): Logger { //创建文件日志记录器对象 $logger = new FileLogger(); //创建文件,代码省略 return $logger; }}class Client{ public function test() { //可引入配置文件实现 $factory = new FileLoggerFactory(); $logger = $factory->createLogger(); $logger->writeLog(); }}改进为了让系统具有更好的灵活性和可扩展性,Sunny公司开发人员决定对日志记录器客户端代码进行重构,使得可以在不修改任何客户端代码的基础上更换或增加新的日志记录方式。 ...

June 21, 2020 · 1 min · jiezi