如何设计系统做系统 系统设计与实现步骤详解?

作者
章烨明杏仁医生CTO。中老年程序员,关注各种技术和团队管理。概述首先,系统是什么?根据《系统架构》一书的定义,系统是由一组实体和这些实体之间的关系所构成的集合,其功能要大于这些实体各自的功能之和。对于我们的场景,系统可能是 App、Web 应用、服务、批处理程序等,也可能是包括所有这些的一个大系统。随着互联网和传统企业的结合越来越深入,业务会越来越复杂。我们该如何设计我们的系统呢?从产品到研发从产品作出原型,到研发编程实现,中间有巨大的鸿沟。越复杂的业务需求,这条鸿沟就越大。一般而言,我们至少还要有两个步骤:业务分析与架构设计。业务分析,主要处理的是业务领域的建模。解决的问题是业务上如何实现。然后是技术与架构方面的设计,主要针对的是技术实现,解决的问题是技术上如何实现。这两个方面是会互相影响的,设计的时候往往需要来来回回的考虑这两个方面。甚至系统开发时也时常会需要调整模型或者架构,当然相应的也需要更新文档。基本原则设计与分析的过程就是不停的进行抽象和封装,并且确定各个系统实体的细节。抽象是指将业务抽象为软件领域的元素(系统、模块或类);封装则是指定义元素的边界,隐藏实现,开放接口。相应的,分析与设计时,最基本的原则就是抽象性和封装性。当然,我们有 SOLID、DRY、高内聚低耦合、设计模式等各种原则和方法,具体方式本文不详述了,但最终它们都可以归类到以上两点。业务分析分析方法业务分析是针对业务领域的建模,产出就是分析模型。分析模型描述系统的逻辑设计与结构,一般会包括需求用例、实体模型以及业务场景的交互流程、状态转换等。分析模型让非技术人员能够理解系统是如何构造的,让技术人员能够以此为基础搭建系统。分析的过程是不断迭代的。特别对于复杂的、涉及多个业务领域的需求,第一步往往需要在整体系统的层级进行分析,然后将模型划分到多个子系统,然后再在子系统的层级进行更细节的分析与建模。另一方面,分析过程中需要不断的优化和调整。例如在确定实体的行为细节时,发现两个实体的耦合很高,那么可能需要重新进行抽象,调整实体的功能范围。理清业务需求理清业务需求是所有分析与设计的前提:确定系统的利益相关者(Stakeholder)及他们的关注点。确定系统的业务需求,即「谁」使用该系统「做什么」。确定系统的功能范围,即该系统「包含什么」,不包含「什么」。系统需要满足利益相关者的关注点,所以要确保所有这些关注点都有涉及到。最重要的利益相关者当然就是用户(有时还会细分为不同类型的用户),此外还应该包括供应商、合作方、运营、销售、老板甚至政府等等,也同样包括研发测试和运维。具体到系统的用户,还需要细分到角色,即使有些角色实际可能是同一个人。比如对于门诊,可能有护士、顾问或系统管理员等等,可以进行不同的操作。需求范围简单话用一个列表即可,复杂的系统可以考虑使用用例图。例如,门诊预约系统的用例图可以这样画:角色有医生、患者和门诊员工。用例有设置排班、管理预约以及预约/取消医生。建立实体模型实体模型是确定系统包含的实体以及它们之间的关联的过程:理清业务概念,统一业务词汇。抽象业务实体,包括事件、人/角色、地点和事物等。识别实体关系:继承、聚合、关联等。实体模型也叫 ER(Entity-Relation)模型。可以考虑使用四色法建模,一般可以使用类图或组件图表示。需要注意的是一定要理清业务的概念,统一命名和定义业务相关词汇,这是进一步沟通的基础。例如,预约系统的实体关系图可以这样画:分析业务场景场景分析用于确定具体业务场景中,各个参与者的交互过程,从而进一步完善分析模型:分析具体业务场景,确定业务规则,梳理业务流程。如果涉及复杂的状态转换,需要确定状态转换逻辑。补充和完善实体模型的内容描述。对于一个业务场景,参与者可能包括人、内部模块、外部服务等,这一步需要理清楚整个业务过程和规则。需要注意的是,对于一些次要路径或者异常路径,也一定要考虑到。对于业务过程和规则,可以使用普通的流程图、泳道图,也可以考虑UML的活动图,状态转换过程则可以通过UML的状态图展示。对于场景分析中不太确定的需求,或者可能会有技术难点地方,可以记录下来,后面确认和验证。例如,下面是预约系统的预约状态图。例如,下面是我们和一家药品供应商对接的流程图。架构与技术设计架构方法架构设计不一定要深入到具体的实现细节,但是应该尽量全面的考虑系统的各个方面。关键是要对项目风险有比较大的把握,这样才能避免开发过程出现不可控的问题。具体设计需要多详细,是需要设计者自己去把握度的。对于暂时无法确定的内容,应该在文档中注明,在开发过程早期进行试验和验证。如果对项目比较关键,可以考虑先行开发原型来进行验证。架构设计常见的是4+1视图,即逻辑视图、开发视图、过程视图、物理视图,再加上场景。另外一种我更喜欢的是视点和视角的方法,如果再加上场景的话,可能会更全面。确定整体架构首先需要在整体上考虑系统的位置和职责:确定系统在整个上下文中的位置,与其他系统的关联。确定系统自身以及各个外部系统的职责。整体架构对应的就是情景视图。这一步将系统看作一个黑盒,确认系统自己的范围和职责,相关的外部系统的职责,以及他们之间的关联。例如,交易系统的整体架构大概是这样的:设计功能模块其次需要确定系统内部的功能模块及其职责:确定系统的模块划分。确定每个模块的职责以及模块间的关联。功能模块对应的就是功能视图。这一步需要明确系统的内部结构。内部模块划分主要有基于业务功能的划分,以及基于实现层次的划分,稍复杂的系统可能会两者都有。也有一些系统会采用CQRS等架构,那么模块划分可能会不一样。功能模块可以使用UML的组件图表示。例如,下图是一个系统模块示意图:明确架构关注点然后需要确定系统架构的理念:理清架构设计需要考虑的关注点。确定系统的架构设计上的取舍。这一步需要考虑各种架构视角,主要有(但不限于)以下关注点:安全性:身份验证、权限控制和授权、操作日志、安全审计、数据一致性等。性能:响应时间、吞吐量等。稳定性:停机时间、故障恢复、数据一致性、数据备份等。扩展性:未来可能的变更,以及如何应对变更。其他:国际化、易用性、合规性等。以上所有的关注点,和开发资源、时间、范围各个方面,往往很难同时满足,所以必须要明确哪些是关注的重点,哪些则可以有所妥协。例如,为了满足性能要求,可能需要降低数据的一致性;为了合规性,可能不得不花更多开发时间。对于未来可能的变更,也一定要考虑到。通常情况下,我们至少要考虑未来半年的架构,但可能只实现当前需要的版本,但是确保未来可以很容易的扩展。一旦确定了关注的重点,在设计和开发的每个过程中,我们都要把这个重点放在心上。例如,对于订单系统,因为涉及到钱和交易,数据的一致性和可追溯性极其重要。下单和支付的API都必须是幂等的,每一笔收入变动都必须记录日志,必须有严格的核对和对账。为了安全,每一次API调用都必须进行权限验证。例如,亚马逊有个知名的原则,所有的系统间调用都必须通过定义清楚的 API,不允许共享数据库。这也是一个架构原则。设计数据库模型如果分析时有了完善了实体模型,设计数据库模型就不是什么难事了。开发完成后,数据库模型应该以数据库为准,架构文档就不需要保留这一部分了。需要注意的是,数据库模型是实体模型在关系数据库的实现,但不一定是严格的映射。数据库可能会有范式、冗余、一致性、同步、分表分库方面的考虑,必要时可能会使用非关系型数据库如ElasticSearch、Cassandra等。有时候还会涉及到数据处理的流程。例如,一张图片提交后可能需要进行预处理,然后有运营人员进行审核和标记,最后进行发布。过程中数据的保存形式或者状态标记可能是不一样的。数据库设计的更多规范可以参考数据库规范。设计接口然后就需要确定 API 细节了。一般我们的服务的 API 是 JSON 格式 HTTP 形式的请求和回调。API 可能是接口定义,也可能会有其他接口形式,例如消息队列等。设计阶段,API 文档可以通过 Markdown 文档、RAP 等记录,开发完成后可以独立维护,或者使用 Swagger 和代码一起维护。接口设计需要注意几点:接口的设计应该以系统提供的领域资源或服务为基础,同时考虑调用方的需求。接口的粒度很重要,太细则调用方很不方便需要多次调用,太粗则无法灵活的满足各种需求,需要仔细权衡。接口的设计也需要从调用方的角度考虑如何进行调用。必要的话可以画流程图、时序图、状态图详细说明调用顺序即状态转换等。接口的文档一定要清楚的说明调用接口的方法、前置条件,参数作用、不同条件的处理、返回接口等。场景实现一般情况下,有了业务场景分析,有了数据库模型和 API,系统的实现一般是比较简单了。但可能还会有一些细节需要进一步考虑实现细节,以避免风险。可以考虑更细节的活动图、时序图甚至伪代码。例如:复杂业务场景的详细设计,或者复杂算法的实现描述。离线任务的执行方式、时间和步骤。非业务的一些场景,例如网络断开、缓存失效、第三方系统宕机等。例如,对于支付系统的微信 Web 支付过程,涉及系统较多,交互比较复杂,可以通过时序图来定义清楚:其他考虑对于我们的后台系统来说,基本的技术框架都已确定,可以解决很多基础的非业务需求。不过设计系统时,也还是需要考虑以下等方面:通用处理的方式,例如日志、错误处理、代码规范、单元测试等。数据迁移、同步和回滚方案:对于老系统的重构,需要仔细考虑并且提前演练。系统部署和发布:如果系统涉及多个子系统,需要考虑系统的部署架构。特别是同时涉及到数据迁移的,一定要仔细考虑发布的过程。系统监控和告警:除了常规的监控和告警,是否有特殊的指标需要监控?并发和数据量:如果系统可能面临对高并发和大数据量的问题, 需要设计对应的方案,以及相关的性能测试和压力测试。缓存设计:如果需要使用缓存,除了要考虑缓存的选型、方案,而且要把缓存放到整个系统中去进行设计。技术选型:涉及到新技术的引入时,则需要仔细分析备用技术的优缺点,选择最合适的方案。设计方法和工具UML面向对象设计(OOAP)领域驱动设计(DDD)CQRS & Event Sourcing参考书分析模式:可复用的对象模型软件系统架构:使用视点和视角与利益相关者合作UML和模式应用架构即未来:现代企业可扩展的Web架构、流程和组织文章从用例分析到方案评审,我们是如何进行业务系统设计的互联网公司研发RD如何撰写总体设计与详细设计文档用UML做好系统分析运用四色建模法进行领域分析从“四色建模法”到“限界纸笔建模法”浅谈我对DDD领域驱动设计的理解领域驱动设计参考架构详解领域驱动设计和实践我对CQRS/EventSourcing架构的思考架构设计基础知识整理全文完我们正在招聘 Java 工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com 。欢迎搜索关注微信公众号:杏仁技术站(微信号 xingren-tech)。
开发者: 张津瑞项目名称: 可视化网盘系统 (基于开源项目:奇文网盘)Email:jinrui.zhang@accenture.com功能介绍 多文件格式分类查看
支持网格、表格视图、时间线三种展示视图
支持极速秒传功能,提高上传效率
多人上传同一文件,可多人并行上传,共享他人上传进度,极大提高上传效率
拒绝冗余,每份文件只存一份,提高硬盘使用效率
上传文件前台实时显示上传文件进度,上传速率,百分比等信息
安全的下载机制,断点下载,权限校验,他人拿到了下载地址也无法下载您的文件
支持视频音频播放,进度条拖拽,倍速播放
拥有回收站功能
高效的垃圾回收机制
响应式前端布局 软件架构该项目采用前后端分离的方式进行开发和部署,主要用到以下关键技术前端:Element UI、Vue CLI@3、Node.js、Webpack后台:Spring Boot、Spring Data Jpa、Spring Security开发工具:Jetbrain 全家桶 idea datagrip webstorm项目管理工具:maven数据库 : MySQL、H2数据结构:递归算法,树的遍历和插入…设计模式:工厂模式、单例模式…源码地址线上访问地址http://disk.jerry-cloud.top系统详细功能分析登录功能实现前端程序实现逻辑登录功能前端组件实现逻辑脑图后端程序实现逻辑登录API实现逻辑脑图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JeKT2YZV-1641392663059)(https://cdn.jsdelivr.net/gh/neusoftzhangjinrui/image-hosting@master/20211214/登录功能接口(userlogin)].5wqqrc8ghzo0.png)忘记密码1,发送邮件获取令牌API实现逻辑脑图忘记密码2,修改密码API实现逻辑脑图注册功能实现前端程序实现逻辑注册功能前端组件实现逻辑脑图后端程序实现逻辑获取注册令牌功能API实现逻辑脑图上传功能实现逻辑上传功能实现脑图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uxIVddvo-1641392663063)(https://cdn.jsdelivr.net/gh/neusoftzhangjinrui/image-hosting@master/20220102/上传功能.32f3tzlbgva0.webp)]新建文件夹功能实现逻辑新建文件夹功能实现脑图批量删除功能实现逻辑批量删除功能实现脑图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x4RP4jMZ-1641392663064)(https://cdn.jsdelivr.net/gh/neusoftzhangjinrui/image-hosting@master/20220102/批量删除功能.3zksa4r5j2a0.webp)]批量移动功能实现逻辑批量移动功能实现脑图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AqWzF3dY-1641392663065)(https://cdn.jsdelivr.net/gh/neusoftzhangjinrui/image-hosting@master/20220102/批量移动功能.4flaivfitzq0.webp)]批量下载功能实现逻辑批量下载功能实现脑图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cyqedfxc-1641392663065)(https://cdn.jsdelivr.net/gh/neusoftzhangjinrui/image-hosting@master/20220102/批量下载功能.67vsc8oicgg0.webp)]搜索功能实现逻辑搜索功能思维笔记[外链图片转存中…(img-tpAsuzsN-1641392663066)]重命名按钮功能实现逻辑重命名按钮功能脑图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fIJszoRX-1641392663067)(https://cdn.jsdelivr.net/gh/neusoftzhangjinrui/image-hosting@master/重命名.webp)]删除按钮功能实现逻辑删除按钮功能脑图移动按钮功能实现逻辑移动按钮功能脑图获取文件列表数据功能实现逻辑获取文件列表数据功能脑图[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jG1bGGLP-1641392663069)(https://cdn.jsdelivr.net/gh/neusoftzhangjinrui/image-hosting@master/获取文件列表数据功能.5kdaio0r9l00.webp)]部分功能截图图片预览[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Qe4ec9qG-1641392663070)(https://cdn.jsdelivr.net/gh/neusoftzhangjinrui/image-hosting@master/20211210/图片查看.1k62mimcx79c.png)]视频播放

一、系统分析与设计的逻辑性框架
在日常的工作中,「软件分析」与「软件设计」这样的词眼经常听到,然而要真正理解「软件分析」和「软件设计」的本质是比较难的,它依赖极强的工作经验,又加上软件分析与设计没有标准的程式化步骤,导致不同的人有自己不同的方法,也就造成了很多人认为软件分析与设计是非常「空洞」,还不如写具体的代码实在,而大部分的人写的是业务型代码,被嘲弄写CRUD的代码没成就感。
软件分析与设计如其它行业一样,具有很强的逻辑性,没有逻辑性支撑,很难做好事。比如写作,有「谋篇布局」、「起承转合」、「遣词造句」等这些「原则」和「方法」,没有洞悉到这些底层逻辑,一个高手写的文章和一个普通人写的文章,效果是天差地别。因此,首先要讲清楚的是软件分析与设计的「逻辑性」到底是什么,下图是软件分析与设计的逻辑全景图。
1.1 方法
1.1.1 分析阶段
软件分析与设计并没有那么神秘,本质来讲还是为了解决现实的问题,和「医生看病」、「工人修车」、「厨师做菜」一样的,都需要方法作为指导,否则没有任何头绪,只能抓瞎。方法是具有普适性,只是不同的行业有各自的特性,具体落地上有差异。
既然是要解决问题,那么总得知道问题是什么吧,就好比医生看病,做各种检查、化验,都是为了全面地了解疾病,所以第一步是需要定义好问题,然而当下很多人都忽略了这一步,直接上来想我要用哪种中间件、哪个框架,连问题都没有定义好,直接想实现无疑是本末倒置。
问题是理想与实现差距的矛盾,现实是不满足理想的诉求,因此,首先需要了解用户的诉求是什么,想解决怎样的问题,这也即为是需求。需求分析最大的挑战是什么是真正想要的,就好比一个病人说了一大堆的症状,他所说的症状表现与书本上的描述有时是有出入的,定义出真正的需求至关重要,接下来就要思考通过怎样的方法去梳理清楚用户需求。
用例是表达用户使用系统实现某些目标文本化的情节描述,它强调的是用户目标、观点。我们应该从用户目标的角度出发思考,也有的人称之为利益相关者的关注点,道理很简单,我们做出来的软件是为了服务于用户的,不是用户想要的,就算用了多高科技的的技术也是失败的。
用例只是一个概括的描述,因此还需要细化,一个用例包括一个或多个场景,场景是参与者与系统之间的活动和交互,比如用户下单,有下单成功,有下单失败两个场景。因此到这里还只是分析阶段,分析的目的是了解现状和目标,以及系统要素组成,它不关心如何实现做、如何实现。分析不仅限于软件行业,其它的行业也是如此,只是在分析过程的程式化步骤、方法不一样而已。
1.1.2 设计阶段
当我们清楚地知道要做什么之后,接下来要思考如何去实现,实现的途径有很多种,如同一百个厨师做同样的菜,做出来的效果不一样。设计阶段有两点需要考虑的:一是如何将功能细化实现;另一点是如何更好地实现。第一点不管用什么方法都可以实现,难的是第二点,更好地实现是需要遵循一些章法,也需要评判体系,要不然你怎么知道好与不好呢,常见的定量评判指标有:成本、性能、可靠性、效率等,还有一类是定性的评判指标有:开放性、体验性等。
度量的指标相对容易,就像医生看病,看疗效、看成本,软件设计也是一样,归根到底是「多快好省」。然而软件设计的章法就复杂得多,它具有很强的艺术性,正所谓「文无第一,武无第二」,比武一定可以比出个高下,谁打赢了就是胜利一方,然而比文就难了,不能说你写的就一定比我写的好,只是不同人的喜好不一样而已,当然这里指的旗鼓相当地对比,不同层次的对比一眼还是能看得出来的。
在设计阶段最为关键的是定义出「关键的技术问题」,一般分类两类:一是站在用户视角的设计,它重点考量的是「便捷性」和「易理解」;另一个是站在系统层面,重点考量的是「复用性」、「扩展性」和「稳定性」。贴近用户的设计,让用户用最少的理解就能使用它,用户无须感知底层复杂的设计,核心是回答用户最朴素的原始诉求。系统层面的设计要灵活,多用组合正交设计提升系统的复用性和扩展性,在具体方案设计中要考虑到稳定性因素,比如写日志会带来性能问题,这个在方案设计中就要考虑到。洞察到关键技术问题,并非一朝一夕就能练就成,需要在工作中大量地实践,总结经验,保持技术的敏感度才行,在第二节中通过实际的案例方便大家加深理解。
1.2 工具
有了方法,接下来要有工具来帮我们更好地做事,与方法对应的,我们软件设计的工具是UML,接下来介绍UML中常用的图。
1.2.1 活动图
软件从本质上是在模拟现实业务运行的过程,是由一个个交互活动组成的,因此,在分析阶段需要梳理出业务的活动是怎样的,通过图形的方式记录下来。
活动图要体现出「参与者」、「活动起点」、「活动关键路径」、「活动终点」,比如用户下单,有「浏览」、「加购」、「支付」等活动。通过活动图可以看出业务的生命周期是怎样的,能够抓住业务的关键流程。
1.2.2 用例图
用例图是强调用户的目标和观点,是文本化的情节描述,用例从本质上讲并不是图,它是文本,用图形是简化了表达形式,它核心有三点:「参与者」、「要做什么」、「结果是怎样的」。用例图是对活动图的细化,对其中的一个活动定义出要实现怎样的目标。场景又是对用例图的细化,你会发现,从目标到实现,一步一步地细化下来,细化是对扩大对认识的理解,不在认识范围内,也就不会去做。
1.2.3 顺序图
将场景通过顺序图表达出来,它核心强调的是系统应该提供怎样的能力,注意顺序图与时序图的区别,顺序图是人与系统的交互,它想表达的是系统应该提供怎样的能力满足用户的诉求,而时序图系统内部实现,强调的是如何实现这种能力。
其实到顺序图这一步,基本上系统要提供的能力就清楚了,当然,这里是知道系统要做什么,至于要怎么实现它并不关心。以上我认为它是分析阶段,把用户想实现的内容清楚地定义出来,接下来就是思考怎么实现。
1.2.4 时序图
时序图有两种作用:一是表达功能是如何实现的;另一个是看责任分配是否合理。第一点比较好理解,一个功能实现是由多个不同的对象组合来实现,对象间有交互依赖。第二点是评判对象设计是否合理,如何两个对象频繁交互,是不是可以合并在一起,如何一对象中的操作过多,是不是可以拆解。
1.2.5 类图
类图的作用也有两种:一是表达属性和职责;另一个是层次结构。类中的属性和职责是一个统一体,属性体现的是认知能力,职责体现的是行为能力,拥有怎样的认识,就会产生怎样的行为。类不是一个孤零零的个体,它与其它的类之间有依赖、协作关系,因此,类图中体现继承、依赖、泛化、包含等关系。
1.3 原则篇
软件设计原则汗牛充栋,简化下来就三点:「复用」、「变化」、「认知复杂度」,好的设计处处体现设计原则,把这些原则刻画到骨子里,而不是刻意体现,如同「没有规矩不成方圆」一样,重点是要理解为什么要这些原则,从本质上讲是为了软件能够「多快好省」地完成。
利润 = 收入 - 成本,从这个公式中,很明显我们想要实现利润最大化,怎么办呢,有两个方法:一是收入变多,最好地方式是实现规模化;二是成本降低,不需要或者很少投入成本。从这两点中,引申出「复用」和「变化」两个原则,复用是不投入或者少投入实现功能,相比从头做是不是要节省成本呢,我们的产品不可能一成不变的,那么变化是在所难免的,如果能支撑灵活地扩展是不是也能节省成本呢。
1.3.1 复用
从上面的分析看,「复用性」的重要程度不言而喻,比起烟囱式开发,复用的成本要低得多,所以产生出了xx平台、xx中台,它们的本质目的还是为了复用,减少重复开发成本。实现复用的手段有很多,复用的程度也不一样,这个就要靠平时的积累,就像医生积累「药」和「药方」一样,这些都是我们解决问题的「工具」。先要有复用的思维,否则只有工具也是无用的,不知道要怎么用、在哪里用。
不同的场景,复用采用的设计方法是不一样的,举几个例子方便大家理解。
完全复用
最简单的复用是100%的复用,比如加法计算操作,它肯定是100%复用的,只用传输不同的数字进去,就可以计算出结果。一般完全复用的是工具型的能力,它与具体的业务语义无关。
配置化复用
这一类的复用程度接近100%,只用配置一些与业务相关的具体的参数即可,比如对账场景,配置两个不同的数据源表,再配置对账规则即可完成对账。这一类场景适用于业务比较固定,流程是通用的,并且差异变化是可枚举的。
部分复用
然而,在实现世界中,没有太多像完全复用的事情,如果变化还不能通过配置化来实现,可以使用「模板方法」或「策略模式」,将变化延迟到子类中去实现,这种方法在大家日常工作很常见,也有的使用SPI扩展点实现。这一类复用场景是有明确的「主流程」,只有少量的变化随业务变化,变化也是可枚举的,那么就可以抽象出扩展点。
还有一类变化是很难枚举的,不知道会有怎样的变化,此时最好的方法是通过「事件」解耦,主流程完成之后,发一个事件消息出来,谁关注就去实现该功能,本质上讲Spring中Aware机制也属于事件的处理方法。
完全不能复用
还有一类完全不能复用,但要抽象出标准的接口,比如常用的数据库操作,Connection、Statement就是标准的接口,不同的数据库厂商实现具体的数据库操作。不要觉得这种没有意义,它从更高的层面定义了规范,使用者是面向抽象使用,可以不关注具体的实现是怎样的。
1.3.2 变化
复用和变化是一起出来的,软件唯一不变的是变化,怎么支撑未来更好地扩展是我们要思考的,如果一个功能千年不变,怎么简单就怎么实现,而如果有变化的话,那就需要好好地设计,用最少的成本去支撑未来的变化。比较难的是要洞察出什么在变化,这个还真不是那么好想到的,需要有行业经验积累,看多了、实践多了,会发现里面的一些门道。
举一个应对变化的例子,税务在计税时,不同的业务计税规则不一样,有的金本位要计税,有的不需要计税,有的非金本位要计税,有的不需要。如果放在一个大的扩展点中实现,那么这就是典型的面向过程的设计思维,我们抽象出了「计税表达式」这个实体来应对变化,「计税项」要不要计税、计税口径是怎样的,新业务接入通过「配置化」来解决。
1.3.3 认识复杂度
认识是分层次的,最高层越简单,最低层越复杂,对于使用者来讲,他希望看到的是简单的内容,如果太复杂,很难上手,比如命令行式的操作系统和桌面式的操作系统,明显桌面式的操作系统更受大众的欢迎,这也是微软在目前依然在操作系统市场占用份额上还是大头的原因。软件分层的目的不仅是让关注点分离,还有另外一个目的是降低认识复杂度,从简单到复杂,这和我们软件分析一样的,从粗到细。
类的设计也是一样的,举一个例子,税务在开发票时,开票这个模型结构需要调用者感知吗,肯定不需要,因为发票中有很多的领域概念,如开票主体、发票行等,用户的目的就是开一张发票,他只用告诉你他知道的信息,不关心你内部要怎么实现,基于这个思考,我们抽象出了「开票申请」这个实体出来,它本质是贴近业务场景的实体,所包含的信息也是有限的,极大地降低了认知复杂度。
二、系统分析与设计的2个案例 2.1 日志框架
2.1.1 日志框架分析
打印日志在我们日常工作几乎是人人都会接触到,日志的核心作用是记录关键有效的信息,帮助我们快速地排查、定位问题,否则没有日志信息两眼一抓瞎。根据我们的经验,希望日志中包含时间、类、方法、代码行、关键日志信息,用了这些信息就能方便我们排查问题。
根据上面的分析,我们很快可以画出日志的概念模型,如下图所示。从本质上讲,我们是将日志信息存储到指定的地方,如存储文件中,输出到控制台上,另外还有日志存储的格式可以有多种,比如普通的格式,还有XML、HTML的格式等。
从概念模型上看,设计一个日志框架并不复杂,但在设计阶段中,还需要挖掘更多的信息,我们输入的信息更多,设计时考虑的因素也就越全,更能满足用户的诉求。
2.1.1 日志框架设计
2.1.1.1 贴近用户的设计
站在用户的视角,他关注的是信息以怎样的格式存储在哪里,因此,有两个概念用户是要关注的,一个是「存储目的地」,另一个是「存储样式」,这两个是可以根据用户的喜好配置的,将这两个概念抽象下,「存储目的地」抽象成「Appender」,「存储样式」抽象成「LayoutPattern」。除了配置外,用户在使用时需要一个接口类,将其抽象成「Logger」门面类,只用简单的调用日志打印接口即可。
2.1.1.1 系统视角的设计
站在系统视角上,用户在日志打印时,他关注的是日志信息,如时间、类名、方法名等信息他不会显示去写,因此还需要抽象一个概念出来表达日志信息的概念,抽象成「LogRecord」,这样概念类图如下图所示。
按照这个设计,很快可以设计出一个简易的日志框架,代码结构如下所示。
Appender类如下所示,它定义的是一个模板方法,先调用LayoutPattern获取格式化的日志数据,然后再输出到目标存储上,Appender和LayoutPattern是可以独立变化的,同时将写操作延迟到子类中实现。
再细细想一下,日志本身是为了方便排查问题,但额外日志的存储是有性能开销的,这个在设计时就要着重考虑了。如何减少写日志带来的性能开销呢,从三个方面考虑:
大文件变小文件:打开一个1G的文件和打开1M的文件肯定是不一样的,复杂问题的治理也是同样的思路,拆分成小问题处理,因此在写文件时可以按照「时间」、「容量大小」切分成小的文件。
内存映射写文件:传统的IO写数据,操作系统需要从用户态切换到内核态,性能开销会很大,可以使用内存映射的方式写文件,提升IO性能。
同步变异步写文件:同步写文件是需要等文件写好了后再往下执行业务代码,而异步就不一样,它将日志记录存储在一个队列中,开户另一个线程慢慢写到文件中,不阻塞业务逻辑执行。
通过上面的分析,一个简单的日志框架随着对它的理解加深,设计方案也在变化,核心是要能看到关键的技术问题有哪些,能提供哪些增量价值或差异化的价值。
2.2 定时任务框架
2.2.1 定时任务框架分析
定时任务在我们平时的工作中也经常使用,如每天定时发送邮件、定时做数据检查等,完成定时任务的诉求也比较简单,就是在指定的时间执行指定的任务,从这个角度看,业务模型并不复杂,如下图所示。调度任务和调度时间是两个独立的变化维度。为了让任务能够调度起来,还有一个「调度器」的概念并没有在业务模型上体现出来,将要执行的任务,以及调度时间告知给调度器,调度器就能根据要求调度任务。
2.2.2 定时任务框架设计
2.2.2.1 贴近用户的设计
站在用户视角,他关心的有三点:
任务要按照怎样的规范去编写,因此需要一个概念「Job」去表达,它定义了一个execute接口。
调度时间如何去表达,抽象出「Trigger」这个概念,表示到时就触发任务执行。
任务如何提交,需要一个门面类「Scheduler」去表达,接受用户提交的任务。
2.2.2.2 系统视角的设计
站在系统的视角,很快能想到解决方法,用户提交任务后要保存至一个队列中「JobQueue」,「JobQueue」中存储的是「JobDetail」,「JobQueue」包含了「Job」和「Trigger」两部分信息,然后有一个调度线程「SchedulerThread」不断扫描「JobQueue」,判断当前任务是否要被执行,如果需要执行就调用「Job」的execute方法,类的概念模型如下图所示。
按照这个设计,很快可以设计出一个简易的任务调度框架,代码结构如下所示。
定时任务最关键的技术挑战是查找要调度执行的任务,很容易想到对「JobQueue」中的「JobDetail」排序,发现Job的executeTime快到了就执行,然而排序是消耗CPU资源的,不同的排序算法时间复杂度也不一样。怎么降低排序算法的时间复杂度呢,最简单的用最小堆排序算法,每次从堆顶获取任务执行,而每次添加任务,又涉及到堆的调整,这个过程也是消耗CPU资源的。有没有更快的算法呢,有人提出了时间轮的解决方法,类似钟一样,如一分钟是60格,类似将任务的时间计算好,放到对应的格中,如果有多个相同的任务,就有一个链表链起来,xxl-job就用了时间轮的方法,将未来周期性需要 调度执行的任务放在时间轮中,时间轮的数据结构是一个Map。
这还是一个简单的任务调度框架,还有很多问题没有考虑到,比如任务分片、分布式定时任务等,还是回到需求分析上,我们要做的功能边界是什么,目标是什么,再去设计对应的解决方案。
三、总结
在日常技术方案设计时,最为关键的是要能定义出关键的技术问题,有两类问题是我们要着重考虑的:一是贴近用户视角的便捷性设计,主要是对业务概念的抽象,用户以最小的知识感知系统;另一个是系统的视角设计,除了完成功能外,还要定义出关键的技术问题以及度量的方法,如打印日志带来的系统开销,怎么做到对系统产生最小的影响;定时任务调度的调度算法设计,选用不同的数据结构的效果是不一样的,普通的排序算法没有堆排序算法好,但堆排序同样涉及到性能开销,这就让我们不断想更好地方案去解决。
作者:不拔
本文为阿里云原创内容,未经允许不得转载。返回搜狐,查看更多
责任编辑:

我要回帖

更多关于 如何设计系统 的文章

 

随机推荐