系统设计是开发者最重要的能力之一。
本文对系统设计中的心态和意识做个人总结, 它们是许多设计思路和方法的源泉。 仁者见仁,智者见智,将直抒胸臆。
心态总是做好事情的第一步,系统设计亦如此。
我总结了 6 条: 设计先行、长期主义、明确目标、持续精进 、保持简单、闭环思维。
设计先行 ¶
如 《软件设计的哲学》 中所言:
我们的首要目标应该是输出一个好的设计,而不是恰好可以工作的系统
设计先行的核心要义在于 先想再做 。 这个意识如此简单,却至关重要。
许多人是撞过南墙、痛过之后才明白其重要性。 我的一位前技术领导曾对我说过 “应该把你的手绑起来,先做思考,而不是去写代码”,言辞激进却十分在理。 “勤奋” 不等于正向输出,在错误的方向上开发,反而是在积累重构点。
值得一提,照搬他人设计的做法,也是惰于思考的表现。 参考前人经典的设计思想是推荐的,但是不考虑具体情况,就认定 “别人是这么做的,我也要这么做”,是偷懒行径。
设计是方向性问题,其重要性甚至高于实现。 在没有做好设计之前,花时间去写代码,或者边做边想,都是本末倒置的。
长期主义 ¶
长期主义的核心要义是:面向未来,不要 “短视”。
通俗讲,不能只看眼前,要多考虑系统未来要面对的场景、可能的变化。 短期的捷径并不一定适应长期的发展。这挑战系统设计者的业务经验和前瞻能力。
如果一种设计方案不是未来,就可以大胆否定它,所谓 以终为始。 例如,系统未来的终端是移动端的话,那么大可不必花精力去做 PC 端的网页开发。 思考未来是方案抉择的有效手段,如果对两种思路犹豫不清,不妨问自己,这个事情的终态是什么样的? 相信会有帮助。
面向未来做设计,想的多不一定做的多。我常用的说法是, “想三步、走一步” 。 看得远的好处在于选对方向,具体当下做多少量,取决于当前的具体需求。 以订单系统举例,考虑到未来多仓发货的拆单场景,早期在数据建模上可以考虑子母单的形式, 但是具体的拆单逻辑,眼前则不必关心。
错的路宁肯一步不走,对的路也不必一次走完。
明确目标 ¶
短期目标应服务于长期目标 , 同前面 长期主义 所讲,不再赘述。
通常,业务的长期愿景是确定的,但达成路径依赖于环境因素和运营者意志, 设计者则需要根据业务的规划路径,来确定系统的短期和长期目标。 具象化长期目标并非易事,思路之一,是去想象未来场景,以确定系统长期要有哪些能力。 短期目标则相对清晰,一个方法是考虑系统最小闭环的能力。 比方说,外卖业务长期要有即时配送系统,但是短期并非必需,商家自配即可最小闭环。
先考虑目标的必要性,再考虑实现难度 。 这一点和 设计先行 的心态相辅相成。 经常有人把 “应该做成什么样” 和 “有没有资源做、实现多么困难” 混为一谈。 不先想清楚前者,而只考虑后者,实际是在,以 “更快落地” 之名,行 “懒于思考” 之实。
除功能本身外,系统设计还应考虑非功能性关注点, 比如 性能、一致性、可用性、 时空复杂度、可扩展性、 即时响应性、兼容性等。许多关注点无法定量,有时也不必定量、定性就好,系统开发伊始就锚定 N 个 9 的可用性大可不必。 总之,设计者要明确系统关注哪些性质。 例如,一般地,通信关注延时、可靠性 等;任务分配系统关注算法的性能、正确性、稳定性等; 游戏决策系统关注性能、即时响应性等;客户端软件关注设备兼容性;几乎所有系统都关注可维护性、可扩展性。
系统的关注点之间也要有所侧重,因为经常会有「鱼和熊掌不可兼得」的情况。 比如算法的时间和空间开销、系统的一致性和可用性 (CAP 定理) 、 功能灵活性和实现简单性、算法的人工可控性和自治能力等,它们并非严格地无法兼顾,强调更侧重哪个,而不是严格地 “二选一”。 在某些情况下才必须要做出选择。例如,分布式系统中发生网络分区时, 系统可以选择停机来坚持输出的正确性,或者选择不停机来保证可用性但是正确性可能丧失。
系统非功能性关注点的偏好不同,设计思路可能完全不同。 比如,如果未关注到性能,系统性能糟糕,有时不得不做设计上的重构。 下面这句架构领域常说的话,即在说明,完成功能并非系统的唯一目标,非功能性目标足以决定架构的不同。
架构往往并不由系统的功能决定,而是由系统的非功能属性决定。
总结来讲,目标有远近和侧重。
持续精进 ¶
《软件设计的哲学》 划分了两种开发思维: 战术的 和 战略的。后者强调了系统设计是一种需要持续精进的长期工作:
你必须投入时间来改进系统的设计,而不是采用最快的方式来完成当前的项目。这些投入在当下会稍微拖慢你,但是在长期会帮你提速。
系统迭代过程中,复杂度是与日俱增的,设计也应持续改进。 比如支持更广义的场景、舍弃过时的实现、优化部分接口的性能,都是在持续改善系统设计。 所以,日常迭代不应只考虑业务功能需求,还应适当搭配技术优化项。
人们习惯推崇保证 向后兼容性 , 但是变化并非总如人意,breaking changes 有时不可避免。 不过我认为,适度节奏的、局部化的激进改动和小重构,有助于更快地把系统拉向更先进的设计上,而且比一次性大规模重构要可控得多。 我甚至有一种观点,重构点总会累积,日常局部重构有益系统健康。 如果系统模块化做的好的话,局部重构的做法是完全可能的。
保持简单 ¶
我觉得下面这句话对「保持简单」的理解十分恰当:
As simple as possible, as complex as necessary.
尽可能简单,必要时复杂。
除非万不得已,不要凭空添加复杂性, 如果可以达到目标的设计有多种,选择更简单的那一种。 平白的、易懂的、统一的方案,常被认为是「简单的」。如果你的方案复杂、难以理解、散碎而不统一, 那很可能还有更简单的思路,最好重新思考。
必须说明,简单不等于简陋,也不等于原始。 试问,为何不用 Brainfuck 来做 Web 开发呢? 再问,为何大多不用 Python 语言 而是用更复杂的 C++ 语言 来写游戏呢? 要指出的是,简单性是有限定的,即 在能力对等的前提下,追求简单性才有意义, 虽然在图灵完备的意义上,编程语言在所能和所不能上是相同的,但是它们在时空性能、表达能力、生态完善上却有高低, 倘若 Python 可以睥睨 C++ 的性能, 那么其简单性或许可以让它在游戏领域胜出。
保持简单并不意味着拒绝复杂的事物。 过于简单的工具反而会平添用户做事的复杂度,复杂的工具也可能会提供强大的能力。 比如,学习 Vim 编辑器是相对复杂的,但它赋予了程序员灵活高效的编辑能力。 对于系统设计而言,工具和技术栈的选型并不能单看其简单性,其提供的能力也是重要的参考量。
简单的方案一般更面向未来,因为它做的更少,对未来的掣肘更少。 但是倘若一条捷径并不通往罗马,我们还是要坚持面向未来。
一言蔽之,简单的设计,强大的能力 。
实现简单性的重要方法是 分解和封装。 分解是说把复杂的事情拆成多个模块,分而治之;封装是说把相近的能力纳入成一个模块;也就是模块化的思想。
系统设计上常说的 耦合性 就是模块分解的干净程度。 模块化讲究正交设计,即模块间无能力和概念的重叠。但是 冗余设计 有时必要, 因为容错能力一般借助冗余部件,比如存储上的多副本策略、通信上的纠错码、 技术上的补偿手法 等,冗余设计确实会增加复杂性,重要的是不要外溢 。 冗余能力也应闭环在对应模块内,局部复杂性的增加,不应进一步影响整体的简单性。
更进一步,模块的组织方法是 分层设计 ,它在计算机中广泛存在。 水平方向上相近的模块切割为层,垂直方向上下层为上层提供服务,将系统犹如 “搭积木” 一般组织起来, 更易于人的理解,更简单。其背后的手法,无非 分解和封装。
计算机是一个由构造块组成的分层体系,各层为其上层提供服务。要理解计算机,你只需理解此分层体系就行了。 – 《通灵芯片》
层级结构具有近可分解性。单元内联系一般比单元间联系强。 – 《人工科学》
分层体系提倡 复杂性下沉,上层对下层的理解尽量保持简单。 例如把兼容性问题下沉,以一个适配器封装其上,进而简化了对此部件的整体理解。
Pull Complexity Downwards. – 《软件设计的哲学》
闭环思维 ¶
控制论 中, 闭环控制 是指 “将受控者的反馈同时作为控制者的输入” 的控制方式。 与之相对的,不考虑反馈的方式是 开环控制。 所谓 “闭环” 的说法,就是说反馈信息可以由 “旁链” 回馈,形成不断迭代的控制循环。
举个通俗的例子,人拿着 “步枪打鸟” ,子弹的飞行路线是预先规划好的,一旦鸟逃跑了,就会打不中,这是开环控制。 老鹰抓兔子的过程中,老鹰会根据兔子位置的变化,不断调整自己追击的速度和方向,这样更容易命中目标,这就是闭环控制。
闭环思维是我做机器人开发时学到的, 当时有个生动的例子,两个人闭着眼睛面对面握手,互相约定各伸出一定距离的方式,是开环的。 如果伸出手来,再根据触碰感觉持续探索直到握手成功的方式,则是闭环的。 因为实际操作中误差不可避免,所以必须采用闭环方式。
二者的主要区别,在于是否利用反馈。
对控制论的进一步参考,推荐一本极为通俗的书 – 《控制论与科学方法论》 和 我对此书的笔记。
我们推崇闭环系统,因其适应性、容错能力、稳定性都更强。 最出色的系统,是双闭环系统,即协作中的两个系统最好都是闭环的。
闭环思维的核心要义: 必须认识到输入误差、环境变化和异常是不可避免的,重点在于系统要有自我调整的能力。 例如导航软件必须根据位置变化和路障信息不断调整最优路线。 除此 “环境变化导致重新计算” 的情况之外,自我调整也强调异常处理, 冗余设计 是最常用的容错思路,它对 “预料之中” 的错误有效。 比如,分布式系统中常用的 故障转移策略 , 当某个节点损坏时,则采用备用节点取而代之。 另外,对于系统协作场景,不可假定协作方总是符合预期的。 例如,我做移动机器人搭乘电梯的场景时,如果某台电梯无响应,则机器人要换一台电梯或更换路线。 再比如,一些关键场景的系统对接 (比如支付),经常需要推拉结合的方式。
此外,区别于控制论中对 “闭环” 的理解,我认为闭环思维还有 自我闭合 的含义。 意即,系统的概念、功能、异常不外溢,强调自我能力和异常处理的完整性。 系统内的概念不要扩散到系统外部被其他部件理解,系统该提供的能力 “不可假手他人代劳” , 系统内能处理的异常不要甩给外部,不然系统就是开环的。
(完)
相关阅读: 系统设计中的经验之谈