作为开发者,我们平时做的最多的事情就是读代码和实现需求。读代码并不是一件简单的事情,即使代码组织良好,要充分理解一个项目也不容易。开发人员应该都有过一个长假回来好一阵子看不懂自己代码的经历,如果是全新接触的项目只会更困难。究其原因代码质量不一定是主要问题,而是因为代码本身就不是一种直观的读物。那么有没有办法能帮助我们直观的读懂代码呢?


实现需求也不容易,我们拿到的是用自然语言和不成体系的示意图编写的文档、交互稿。它们不一定严谨、细化,难以完全避免疏漏、前后不一致的情况,但最终要转化成绝对精确的代码。在多次迭代后系统的完整行为也会分散在多个版本的文档和交互中散落各处。有没有办法能更准确完整的表达系统行为呢?


经过小规模的实验后,我觉得 statechart 有潜力解决这两个问题。希望借此机会抛砖引玉,分享一下。


statechart 是一种图表,它可以作为文档的补充用于描述系统的行为,或者作为开发者分析需求的工具,指导后续代码编写甚至生成最终代码。在它之前,还要介绍一下有限状态机,它可以用状态图(state diagram)来描述:

三月即时激励分享_java

一个简单的灯泡

上图由几种元素组成:黑色实心圆是开始,圆角矩形是状态,箭头是状态转移,而箭头上的文字是触发状态转移的事件。图表非常直观,非技术人员也能轻松理解。前端项目天生事件驱动,能很好的对应上状态图,比如常见的网络请求可以表达为下图:

三月即时激励分享_java_02

因为我们的例子简单,所以状态图目前的表现还可以。但当问题规模扩大时它的表现不尽人意。比如在最开始的灯泡例子中,如果我们多加入一个灯泡,状态图的复杂度就会快速增加:

三月即时激励分享_java_03

这种情况叫做状态爆炸(state explosion),statechart 正是为了解决状态图存在的这些问题而提出的多项扩展。要解决上图的问题,可以借助 statechart 的平行状态(parallel states)概念:

三月即时激励分享_java_04

虚线隔开的两个区域互相独立,当系统中增加更多的灯时,状态机的复杂度也只会随之线性增长。我们还可以添加一个总闸,总闸开启时两盏灯各自才能亮或者灭,这里就引入了层级概念:

三月即时激励分享_java_05

上图中“总闸开启”这一状态有两组平行的子状态,只有处于“总闸开启”状态时,两盏灯各自的状态才有意义。层级结构除了能简化一些情况以外,同时还是一个很好的抽象手段。在上图中如果我们省略总闸开启的子状态,整个系统仍是完备可理解的。有需要时,我们又可以展开子状态,查看更深层次的细节。


我们最后再介绍两个概念,让 statechart 能描述更多场景:

三月即时激励分享_java_06

上图中,“[已填写健康状况]”表示守卫(guard),“entry/记录上岗时间”与“exit/记录离岗时间”表示活动(activity)。


守卫的作用是事件触发时,如果条件不满足,则不允许状态转移。如上图,若某同事未填写健康状况,刷卡时不满足守卫要求不得上岗。entry 活动的作用是,当转移到该状态时做某些事情,exit 活动同理。


statechart 还提供了其他许多扩展能力,让它能成为文档的优秀补充,甚至是主要描述工具。构建它的过程中,我们还对需求做了全面的分析和合理抽象,能让最终写出的代码更加直观清晰。实际上由于它是严谨的形式化方法,我们可以借助工具,把上面的图表直接映射为可执行的代码,可视化地看到系统的运行过程:

三月即时激励分享_java_07


推荐一下 xstate.js 这个优秀的开源项目

这样一来 statechart 就能改善需求与代码两个环节,我们可以在需求阶段用它分析交流。定稿的版本转化为可运行的代码框架,保证实际实现与沟通结果一致。在需求多次更迭后,系统内运行的 statechart 始终包含最新的系统行为,且能够以可视化的形式直观的展示出来。


它核心的概念——状态与事件,和前端这种 GUI 系统的契合度非常高。如果能经受住更多实际项目的检验,就有望应用在大量场景中,切实的改善我们每一天的开发体验。由于非开发人员也能轻松理解且具有丰富的表达能力,它还有潜力作为某些 codeless 开发工具的核心,让业务人员不写代码也能自行建模复杂系统。


这是我近期在工作中比较有趣的一点探索,除此之外还有一些心得体会想和大家分享:

拒绝被框架框住


小程序是个优秀的平台,但它提供的开发体验不甚理想。得益于行内的 wxa 框架,许多的坑都已经被填平,但还是有一些地方需要小心谨慎,比如 mixin(behavior) 。这个方案多年前就已经被口诛笔伐过,但现在仍是一些框架推荐、甚至唯一的组件间代码复用方式。


在小程序原生开发的独特机制下,mixin 使用起来更加扑朔迷离。举手投降被动接受是下下策,其实在框架下我们还是能作出一些反抗的:


  • 给 mixin 的状态与函数名添加前缀,制造独立命名空间

  • 尽可能将公共代码抽取为纯函数,不通过 mixin 共享

  • 权衡得失,允许一定代码重复以避免使用让人迷惑的 mixin

  • 对实在无法优雅解决的情况,留下足够详尽的注释

并肩作战


这段时间工作的顺利开展,所有同事的鼎力支持居功至伟。特别是疫情期间远程办公时,遇到了各种各样的问题,有一些隐晦的问题不是独自钻研能解决的。


为了不耽误项目进度,组内同事、运维、PM 和对接的后台还有其他许多同事被我问了个遍。有时提问的频率高的我自己都不好意思了,但几乎所有问题都得到了明确的答案。虽然项目可能是一个人负责,但在合适的时机请求帮助更能发挥团队的优势。

总结、写作、分享


我们的 KM 社区非常活跃,是个很优秀的平台,到现在我也写了一两篇技术细节的分享文章。写作本身就是对知识的总结整理,要有条理的讲给别人,写作者更要思路清晰。技术人员要提炼积累日常经验,写作是个非常好的办法。


期待以后能向大家交流学习更多经验,共同进步。最后祝所有人bug少少,身体健康!