浅谈互联网业务系统设计规范

注明:本文已投递到《极客时间》电子刊物上:从用例分析到方案评审,我们是如何进行业务系统设计的?

前言

工作这些年,通过自身由0到1的成长,结合所见所闻,发现无论是刚入职场的新兵还是有些工作经验的老兵,在业务系统的设计上非常之薄弱。

一般来讲,职场新兵在设计业务系统的时候,容易出以下几个问题:

1 需求沟通完,提笔就开始设计数据表结构,就想着有哪几张表,要建哪些索引,有哪些唯一键,如何保证事务,如何设计触发器等等。出现这类问题很正常,毕竟在学校学的都是理论,授课的老师也都没有太多的工程经验,也指导不了你的设计。

2 要他设计一个系统,完全摸不着头脑,一提笔就是写代码,想到哪里写到哪里,没有一丝设计。出现这类问题的同学,一般都是在学校没好好学习的同学,或者是非计算机专业的同学。

而一些工作经验的人,做出来的设计也是一团糟,要么就是考虑不周全,要么就是画的图别人看不懂。这类人一般是平时不喜欢看书学习,或者是之前刚工作的时候也没有有经验的人指导等等。

另外,撇开以上个人问题不说,目前市场上关于“互联网业务系统设计”相关的文档和书籍真是少之又少。用搜索引擎搜索“业务系统设计”关键字,出现的基本上都是系统宏观架构类的相关文章,或者就是各种传统软件管理系统的设计文档等不适用的内容,而目前市面上诸如软件设计或者uml设计等之类的书有个共同点就是:“实在是太厚了,讲的实在是太细了!”。如果按照书上面的步骤来,写文档就得写上一周,根本适应不了互联网业务快速迭代,小步快跑的特点。

针对这些共性问题,结合我这些年的所学,所感,所悟,写成一篇文章,供大家一起学习,若有不对之处,还望指正。

为什么要有设计

一个好的设计可以帮我们梳理业务逻辑且抓住核心需求,设计稳定可扩展的业务系统,评估业务开发周期和开发成本,有效的规避风险。就如,盖房子,咱得有建筑图纸,有了图纸,才能核算施工周期,施工成本等建筑指标一样。

如何进行优雅的设计

首先说说“优雅”二字。之前在部门内部也看过很多同学的设计文档之类的,主要存在如下问题:

一上来就贴数据表的设计;时序图画的不规范(没有面向对象的思想),生搬硬套,前后毫无逻辑等等,这些都是源于没有系统学习设计相关的知识,其次不会画图,不懂如何用图形正确的表达自己的思想和观点,导致输出的东西让人看不懂或者产生误解,所以,“优雅”二字很关键!

了解需求

了解需求就是跟业务方(一般是产品)进行沟通:我们要做什么东西,这个东西包含哪些内容,其中最核心的是哪块儿,要达到什么效果,访问量预估多少,后期的产品需求迭代方向是怎样的。

一般在此过程中,产品会提供经过产品内部评审过的产品文档,同时,产品还会对产品文档中的内容进行宣讲。在产品宣讲产品文档之前,大家一定要仔细阅读产品文档中的内容,对于不懂的地方及时跟产品进行沟通;在产品宣讲文档的过程中,如果有不懂的地方,或者你自己针对其中某个问题有更好的解决方案,都要踊跃的提出来。在这种反反复复思辩的过程中,有利于培养业务工程师的产品意识。

注意,作为一名优秀的工程师,针对产品需求,一定要学会问为什么,千万不要沦为写代码的工具。

接下来,为了便于读者能够清晰的理解我说阐述的内容,我以目前热门直播圈的礼物系统的为例进行阐述。

用例分析

消化产品文档,借助“用例分析”这一手段,理解参与者和系统之间的关系,捕获系统行为和方法,梳理用例场景,抓住核心需求,避免在设计和之后写代码的过程中本末倒置。另外,这也是评估你开发周期中必不可少的一个环节。

如下图所示,用例图中描述了礼物系统中有两个参与者:用户和后台管理员;另外还有三个用例:礼物管理(创建礼物和修改礼物),获取礼物列表和送礼物。

用例图

业务实体模型

前面介绍的用例主要用来描述我们要达到什么样的目标,而业务实体代表参与者执行用例时所使用的“事物”。而这个“事物”才正是我们所需要分析的核心概念。比如说,“先定个小目标,我先挣他一个亿”。一个亿是一个目标,但是如何挣到一个亿才是我们真正需要分析的东西。

言归正传,我们还是以“礼物系统”为例,从产品层面来看,送礼物是最核心的用例,那么我们就以送礼物这个场景进行分析。我们可以把这里用例映射到我们现实生活中,当你遇到一个心仪的姑娘,给她送礼物的场景:在商店选礼物,付钱,买了后让送货小哥儿去送礼物,收到回执(礼物已送达的通知)。通过以上场景的描述,我们就可以分析出“”送礼物“”这个过程所需要的业务实体,如下图所示:

送礼物业务实体模型图

参考以上的分析,我们回到我们“”礼物系统“”中的送礼物场景,物流这类业务实体对应的是将礼物消息传递给主播的消息通道;钱对应的是虚拟货币;虚拟世界没有回执这类实体。那么,在礼物系统中,送礼物所对应的实体模型图如下:

送礼物业务实体模型图

值得注意的是,在此建模阶段,我分析的思路:找一个生活中类似的场景进行分析入手,看看生活中对应的此类场景是怎样的,达成这一目标需要些什么东西,然后再以生活中的场景作为基础,结合业务特性,做进一步分析。

针对我这类分析思路,有人可能会有以下问题:

你这样分析这不是多此一举吗,如果要我设计,我觉得可以直接由用例分析环节跳到时序图环节,送礼物嘛,挺简单的一个场景?

对,这位同学说的没错。我这里之所以这样描述主要是提供给大家一个分析问题的思路,如果系统简单,或者设计者经验丰富,就可以考虑略过这一步的。当然,如果系统复杂,或者你没有头绪,可以考虑我这种分析思路,对理清系统关系非常有用。

时序图

上一节,我们分析了业务实体之间的关系,那么这一节,我们将从执行顺序的角度出发,描述实体对象之间的操作关系。这里我们还是以核心用例——送礼物为例, 从时序图中,我们就知道自己需要做那些工作了。另外啰嗦一句,上一节我们分析的业务实体模型和时序图中的业务对象不一定是一一对应的关系。比如,在下图中,就多出了仓库这个对象,因为礼物的本身也是一种商品,既然是商品,很自然就有一个仓库来存放和管理它。当然,在最后写代码落地的过程中,你也可以把礼物相关信息放在Gift Center中来维护,这个取决于平台是否有“”仓库“”这类系统以及未来对这类产品的规划。咱们这里只是分析。

送礼物

由上图可见,整个送礼物的流程,依赖于多个外部系统,因此这里需要注意:在实际做项目中,你需要优先跟你需要合作的团队或者人打好招呼,确定好和外部的工作时间和工作量后,再来做自己份内的事情。因为,外界的东西都不可控,自己的事情可控,万一不提前打好招呼,你把自己手里的事儿忙完了,再跟其它团队打招呼时,如果此时其它团队告诉你,这个事儿做不了,那你岂不是白忙活了!

此外,针对时序图的画法,我再举一个我在工作中经常遇到的错误画法,如下图所示,注意红圈处,图中存在一个严重的错误:出现了面向过程的画法,没有把仓库当成一个对象描述,而是一种面向过程的思路,缺乏面向对象的设计理念,另外,此阶段还没有考虑存储选型呢,不一定是用Mysql来进行存储。

错误的时序图

流程图

当梳理完时序图后,如果有些地方需要用到算法之类的东西,或者在时序图中某个操作有些特殊的细节需要具体描述的情况下,一般用流程图来描述。比如说,在送礼物这个操作中,如果用户连续送了10个以上的礼物,就自动触发该礼物的3D特效。那么对于这个关键细节,咱们可以流程图来描述,如下图所示:

礼物特效策略流程图

关于流程图的规范我想啰嗦一下,大家在描述流程的时候,尽量用“数学语言”来描述,相比用文字描述,这样比较言简意赅,而且不会使人产生理解上的偏差。

实体图

前面在业务实体建模和时序图阶段,我们分析了实体之间的关系,那么在此阶段主要描述实体内的属性。在礼物系统中,我们只需要关注礼物本身的属性即可。Gift Detail实体描述礼物明细:某人在什么时候送了什么物品给谁,送了多少。Goods实体用于描述商品(礼物)本身的属性,如创建时间,价格等,如果团队中没有库存系统之类的独立系统来维护商品的话,可以暂时将商品(礼物)放在礼物系统中,等将来有需求,再拆分出去。

另外,有同学会问,实体图中不是还应该有描述操作属性的方法吗?对,一般书中都会这么写,但是,以我个人的经验,只需要属性描述清楚,基本就可以确定一个实体,方法可写也不可写。

实体图

技术选型

语言选型

采用什么语言(php/java/lua/go/c++ 等)来编写你的系统主要取决于两方面:

1 系统需要承载多少访问量,每个语言的特点都不一样,都适用于不一样的场景;

2 团队的风格以及团队的积累。

由于我们团队负责的业务访问量比较大,以及之前团队的积累,一般采用的都是golang。

存储选型

存储选型在整个业务系统的设计中是至关重要的,一般情况下,系统最先出现瓶颈的地方都是存储这块。针对存储选型,我们主要从以下几点来考虑:

1 实体对象中的数据需要用什么样的数据结构来存储才能满足我们的查询需求(这主要考验你数据结构的基本功底好不好了);

2  了解目前市面上的存储组件(mysql,redis,ssdb,mongo,memcache等等)的特性,看看有哪些能够满足我们所设计的数据结构,能够近似满足即可,然后再根据存储组件的特性对我们的数据结构稍作修改。换句话说,我们设计的数据结构是一种理论情况,而已有的存储组件是现实,当理论与现实产生碰撞的时候,需要折中考虑。

3 实体对象中的数据是否需要持久化。

我们以Goods实体对象为例,从产品层面上分析,用户会频繁请求礼物列表,因此在系统内,此实体对象是读多写少的场景,既然是对查询要求较高,那我们可以想想哪些数据结构具有较高的查询性能,比如,跳表、散列表、树结构。一般场景下,散列表的查询性能较高,为O(1)。

我们确定数据结构后,我们还需要考虑,实体对象中的数据是否需要持久化,由于礼物信息在系统重启后,不能丢失,故需要持久化。然后,我们再去看看市面上的这些流行的存储组件,哪些满足以上两个特性,我们发现redis中的string结构能够跟我们设计的数据结构相契合,而且是内存级数据库,性能好,并支持持久化。

因此,针对Goods实体对象数据的存储,我们选择用redis。

在这里,容易出现的一个问题:有些人不去了解这些数据库底层的实现方案,就轻易做出选择。就如,在redis中string结构,hash结构从功能上都可以满足我们的需求。那么随着数据量的增长,从性能上能否能继续满足我们的需求呢?这就要求我们工程师,知其一,还得知其二。只有深入的了解它,才能更好的使用它。

最后,确定存储方案后,需要进行方案验证。

降级策略(可选)

程序所依赖的宿主机和网络,不可能百分之百的不出问题,所以对于一些核心业务中的核心接口,需要有降级预案。比如数据库挂了,网络不通等诸如策略此类的问题,程序本身需要做降级策略。说白了,不论是做系统设计还是写代码,都得有防御式编程思想。一般常用策略如下:

1 程序需要多机器,多机房部署;

2 程序内部,连接数据库需要有超时时间,这个时间不宜过大;

3 以文件或者内存的方式,保存上一次返回的正确结果。当后端资源有问题的时候,读取本地文件或者内存中的数据。

4 需要一个手动/自动降级开关,用于切断对后端故障资源的访问。避免因后端资源故障,导致接口响应时间过长。

方案评审

项目设计完后,就邀请团队的喷子来喷一喷吧。

其它常见问题

缺乏设计 vs 过渡设计 vs 恰如其分的设计

有时候在进行设计评审的时候,被评审的同学经常会说这样一句话:”我不做呢,说我缺乏设计;我做了呢,又说我过渡设计”。被评审的同学真是左右为难。其实解决这个问题,关键在于一个度,掌握好这个“度”,便可以做到恰如其分的设计。

如何把握这个”度“?

我一般是这样做的,满足当前产品需求,针对未来可能提出的需求(可能产品还没想到)保留较好的扩展性。一句话概括:你必须考虑到,但是你可以不做,让系统保持一定的弹性。缺乏设计就是,你压根儿就没考虑到,导致的结果就是产品一加需求你就得大改或者重构系统;过渡设计就是,你考虑到了,你也做了,但是随着产品的迭代,你发现你做的压根儿没用,并且还增加了系统复杂度和维护成本。

有人又会问,如何保证较好的扩展性? 这就取决于你业务实体本身的设计了,保持一个设计原则:单一职责。(这一说就说到设计模式去了,推荐一本书《敏捷开发》)

理不清实体之间的逻辑我们应该怎么办

解释这个问题,我想引用一句话:“艺术来源于生活又高于生活”。程序设计也是一门艺术,程序也来源于生活。当你理不清业务实体之间的关系时,不妨将程序中的实体同生活中的实体对应起来,看看他们在生活中是怎样的关系。举个简单的例子,如果要你设计一个站内信系统,你理不清的时候,你可以想想生活中,信件的投递是怎样的。

建模工具

Enterprise Architect

推荐学习书籍

《UML精粹:标准对象建模语言简明指南(第3版)》:此书不厚,适合入门,浅显易懂,里面的图都是用Enterprise Architect工具画的,可以作为画图的工具书。

《大象:Thinking in UML(第2版)》:此书重点讲如何建模,并从多个角度产生了面向过程和面向对象之间的区别,以及使用场景。讲的非常细,非常厚。可以选择性阅读部分章节。

《敏捷开发-敏捷软件开发:原则、模式与实践》:重点理解什么是敏捷设计。

《实现领域驱动设计》:领域抽象能力的内功修炼。

推荐阅读更多精彩内容