我们第三章 “服务端开发篇” 就快要结束了。我们原计划的第三章会分拆为两章:
第三章:服务端开发篇。主要介绍服务端的基础架构与业务架构。
第四章:服务治理篇。主要介绍服务端程序上线与线上服务如何管理的问题。
原先计划的 “第五章:通用架构范式篇” 会取消,核心内容会融合到其他的章节中。详细的调整结果,近期我们会与大家同步新的大纲。
今天我们把话题重新回到架构上。
关于架构,前面我们已经聊了第一步的需求分析和第二步系统的概要设计:
17 | 架构:需求分析(上)
18 | 架构:需求分析(下)- 实战案例
32 | 架构:系统的概要设计
需求分析并不是纯技术的东西,和编程这件事情无关。它关乎的是用户需求的梳理、产品的清晰定义、可能的演变方向。
需求分析的目标和最终结果,都是要最终形成清晰的产品定义。产品定义将明确产品的元素,明确产品的边界,与产业上下游、合作伙伴的分工。
在需求分析阶段,我们关注用户需求的精确表述。我们会引入角色,也就是系统的各类参与方,以及角色间的交互方式,也就是用户故事。
在概要设计阶段,我们一般以子系统为维度来阐述系统各个角色之间的关系。对于关键的子系统,我们还会进一步分解它,甚至详细到把该子系统的所有模块的职责和接口都确定下来。
这个阶段我们的核心意图并不是确定系统完整的模块列表,我们的焦点是整个系统如何被有效地串联起来。如果某个子系统不做进一步的分解也不会在项目上有什么风险,那么我们并不需要在这个阶段对其细化。
为了降低风险,概要设计阶段也应该有代码产出。
这样做的好处是,一上来我们就关注了全局系统性风险的消除,并且给了每个子系统或模块的负责人一个更具象且确定性的认知。
代码即文档。代码是理解一致性更强的文档。
经过系统的概要设计,整个系统的概貌就了然于胸了。详细设计阶段,是需要各个子系统或模块的负责人,对他负责的部分进行进一步的细化。
详细设计关注的是子系统或模块的全貌。
请记住,详细设计并不是只谈实现就完事,更不是一个架构图。它包括以下这些内容。
现状与需求
现在在哪里,遇到了什么问题,要做何改进。
需求满足方式
要做成啥样?交付物的规格,或者说使用界面(接口)。
怎么做到?交付物的实现原理。
概要设计和详细设计的工作内容会有一定的重叠。
概要设计的核心目标是串联整个系统,消除系统的重大风险。在这个过程中,对一些关键模块的实现细节有所考虑是非常正常的。但从另一个角度来说,分解粒度也不能过粗,不应该把特别庞大的子系统直接分出去,这样项目执行的风险就太高了。
但两者的分工不同,考虑的问题重心不同。
比如,从使用界面(接口)来说,概要设计不一定会把子系统或模块的完整接口都列出来,实际上它只关注最核心的部分。但是从详细设计角度来说,接口描述的完备性是必需的。
我们先看看现状与需求。
从逻辑自洽的角度,我们任何一篇文档,首先关注的都应该是要解决的问题与目标。
现状与需求的陈述,要简明扼要。
现状大家都知道,所以不要长篇累牍。更多的是陈述与我们要做的改变相关的重要事实,侧重点在于强调这些事实的存在性和重要性。
比如,假设我们要对某个模块重构。那么,现状就是要谈清楚现在的业务架构是怎样的?它到底有什么样的问题。
需求陈述是对痛点和改进方向的一次共识确认。痛点只要够痛,大家都知道,所以同样不需要长篇累牍。
每个子系统或模块,都有自己的角色分工与用户故事。我们不用重新做一遍需求分析,但对需求分析的核心结论,在详细设计开始之前需要明确。
这很重要。它是我们详细设计所要满足的业务目标。
聊完了现状与需求,接着我们就要谈需求的满足方式。它分两个方面:一方面是交付物的规格,或者说使用界面(接口)。另一方面是背后的实现原理,我们怎么做到的。
规格,或者说使用界面,体现的是别人要怎么使用我。
我们前面一直在强调,使用界面(接口)应该自然体现业务需求,就是强调程序是为用户需求服务的。而我们的架构设计,在需求分析与后续的概要设计、详细设计等过程之间也要有自然的延续性。
使用界面这一部分要详细写,它是团队共识确认的关键。
我们的交付物有哪些可执行文件,有哪些包(package)?如果可执行文件,那么它是一个界面程序,还是服务?如果是服务,网络协议是什么样的?如果是包,它又包含哪些公开的类或函数。
在 “32 | 架构:系统的概要设计” 这一讲中,我们花了非常长的篇幅介绍使用界面(接口)是怎么回事,今天我们就不对这一点进行展开。
需要强调的是,使用界面需要有明确的书写规范。它也是团队共识管理的重要组成,是团队效率、团队默契形成的象征。
更需要强调的是,使用界面的稳定是至关重要的。
接口的变更需谨慎!
对使用界面的不兼容调整,可能出现严重的后果。技术上,可能会导致客户异常,出现编译失败需要重写代码,或者更严重的是,可能导致他们的系统崩溃。商业上,则可能导致大量的客户流失。
聊完使用界面,接下来就要谈实现原理了,它要体现的是我如何做到。
在 “42 | 实战(二):“画图”程序后端实战” 一讲中,我们提到过以下这个大家耳熟能详的公式:
程序 = 数据结构 + 算法
它是一个很好的指导思想。当我们谈程序的实现时,我们总是从数据结构和算法两个维度去描述它。
我们先看数据结构。
数据结构从大的层面分,可分为基于内存的数据结构,和基于外存(比如 SSD 盘)的数据结构。
对于桌面程序,大部分情况下我们打交道的都是基于内存的数据结构。外存数据结构也会有所涉及,但往往局限于 IO 子系统。
但对于服务端程序,数据结构不完全是我们自己能够做主的。数据结构大部分情况下都是基于外存的,而且有极高的质量要求。
在 “36 | 业务状态与存储中间件” 这一讲中我们也说过,存储即数据结构。所以,服务端程序在数据结构这一点上,最为重要的一件事是选择合适的存储中间件。然后我们再在该存储中间件之上组织我们的数据。
这是数据库这样的存储中间件流行起来的原因。无论是关系型数据库,还是文档型数据库,他们都被设计为一种泛业务场景的数据结构,有很好的业务适应性。
所以在服务端我们谈数据结构,谈的不是内存数据结构,往往谈的是数据库的表结构设计。当然表(Table)是在关系型数据库中的说法,在 mongodb 中我们叫集合(Collection)。但不管我们用的是哪种数据库,出于惯例我们往往还是以 “定义表结构” 一词来表达我们想干什么。
描述表结构,核心需要包含以下内容:
字段名;
类型;
字段含义,以及是否指向另一个表的某个字段;
索引。
你会发现,其实定义表结构和定义内存数据结构本质是完全一致的。定义内存中的一个类(或结构体),我们也关心字段名(成员变量名)和类型,也关心字段的含义,以及它是否指向另一个类(或结构体)的某个字段(成员变量)。
但表结构比内存数据结构多了一个概念:索引。
索引为何存在?我认为有这样几方面的原因。一方面是因为数据库是泛业务场景的通用数据结构,它是动态的,需要依赖索引来提升数据访问的效率。另一方面是因为多租户。多租户导致数据量的爆发式增长,导致大部分情况下遍历查找变得不现实。
索引怎么设计?它完全取决于算法。算法里面使用了哪些数据访问的特征,这些数据访问的频次预期是多少,这些决定了我们添加哪些索引是最划算的。
在涉及的类比较多,或数据库的表结构比较复杂的时候,有时我们会用 UML 类图来对数据结构进行直观的呈现。
谈清楚了数据结构,我们接着聊算法。
在 “程序 = 数据结构 + 算法” 这个说法中,“算法” 指的是什么?在 “42 | 实战(二):“画图”程序后端实战” 一讲中,我们这么说:
在架构过程中,需求分析阶段,我们关注用户需求的精确表述,我们会引入角色,也就是系统的各类参与方,以及角色间的交互方式,也就是用户故事。
到了详细设计阶段,角色和用户故事就变成了子系统、模块、类或者函数的使用界面(接口)。我们前面一直在强调,使用界面(接口)应该自然体现业务需求,就是强调程序是为用户需求服务的。而我们的架构设计,在需求分析与后续的概要设计、详细设计等过程之间也有自然的延续性。
所以算法,最直白的含义,指的是用户故事背后的实现机制。
数据结构 + 算法,是为了满足最初的角色与用户故事定义,这是架构的详细设计阶段核心关注点。
那么,怎么描述一个用户故事对应的算法?
一种方式是基于 UML 时序图(Sequence Diagram)。以下是我个人用过的很好的在线版 UML 时序图制作工具:
https://www.websequencediagrams.com/
另一种方式是基于伪代码(Pseudo Code)。在逻辑较为复杂时,伪代码往往有更好的呈现效果。比如,服务端程序对数据库的 SQL 操作往往比较复杂,但是从 UML 时序图来说流程却并不长,这个时候去画 UML 时序图的意义就不大。
今天我们聊的是怎么做详细设计。
详细设计并不是只谈实现就完事,更不是一个架构图。它包括以下这些内容。
现状与需求
现在在哪里,遇到了什么问题,要作何改进。
要做成啥样?交付物的规格,或者说使用界面(接口)。
怎么做到?交付物的实现原理。
“程序 = 数据结构 + 算法” 是我们很熟悉的一个公式。它其实是怎么描述实现原理的很好的指导方针。当我们谈程序的实现时,我们总是从数据结构和算法两个维度去描述它。