软件流程、类型、设计和Worse Is Better

柏舟   新冠4年 07-26

业务的系统设计本质上是对复杂度进行管理,对外提供简单、所需的抽象,内部实现屏蔽掉多余的复杂度。这是最常见的瀑布式开发流程:

flowchart TD; A[规格需求]-->B["性能需求(定量)"]-->C[概念方案设计]-->D[物理实现]-->E[验证];

从用户角度看的五种软件

用户的不同需求就决定了软件开发没有统一的开发流程,比如依赖不公开的第三方库,使用TDD不一定好用。

终端APP

可以是开源软件、商业软件,也可以是网站,特点是被大量用户自由使用的软件。问题:

一般有:

内部软件

只需要在一家公司内部运行,可以对运行环境进行假定。因为只有少数人使用,导致以下特点:

  1. 可以依赖系统其它软件,比如Excel等等,因为公司内部没有可以装。
  2. 可用性在内部软件开发中优先级很低,使用者别无选择。
  3. 分摊到每个使用者的成本很高,所以资源匮乏,需要开发速度快,以节约成本。
  4. 不需要很高的质量,只需要完成既定功能,所以可能bug更多,界面不美观。

嵌入式软件

  1. 不可升级。
  2. 质量要求高。
  3. 硬件性能有限,需要追求优化而不是代码优雅。

游戏

  1. 赢家通吃:某些游戏很热门,但大多数是失败品。
  2. 质量要求高,玩家通常不会玩第二次。

一次性软件

为了得到某些信息写的临时性代码,比如Shell脚本。临时性的代码可能会转化成内部软件。

从状态角度看的软件

软件常见的状态来源:

不同软件对状态的要求不同,具体体现在:

特性 B/S C/S Standalone
概述 所有状态以服务器为准 用户APP含有状态 离线应用,自身维护状态,不需要同步
典型应用 网页 游戏、聊天应用 设计软件
状态维护方式 每次拉取所有状态 通过消息同步 本地存储状态
场景 状态简单的,小的 状态复杂的,需要离线的 工具
特点 设计简单 状态同步复杂 设计简单

需求之外的问题:持续演进和极端情况

系统设计中,除了基本的需求分析,常常需要考虑以下问题:

  1. 需要考虑到硬件的演进、升级,需要中间层对硬件能力进行抽象。
  2. 需要与开发团队的组织架构相适应。
  3. 为了可靠性在上层引入额外的抽象(如事务、错误处理等等),引入额外的复杂度。
  4. 抽象的保证有限,极端情况会泄露:虽然TCP提供可靠的连接,但是网断了TCP仍然不能工作。

关于问题3和4,由于常常需要权衡可靠性和效率。如果为了效率暴露过多的底层能力,那么上层开发需要了解很多底层概念,并且底层的模型实现也很容易限制死了,最典型的是Python的GIL锁。如果为了抽象的简洁,提供有限的能力,可能接口的性能不佳,限制开发人员使用。

由于现实的复杂多变,很容易遇到一些极端的需求,导致抽象泄露,不得不在原有的抽象上打各种补丁,而且还需要上层的开发人员了解底层的技术细节。还有的为了潜在的需求,对系统进行过度设计,引入各种毫无意义的复杂度,不仅难以理解,而且还不好Debug。

这些问题没有银弹,你需要:

  1. 仔细了解实际的需求,验收标准。
  2. 平衡好业务、编程语言、基础设施的复杂度。
  3. 管理好软件质量和开发周期的平衡。

Worse Is Better

Common Lisp专家Gabiel于1989年提出Worse Is Better,它强调强调简单压倒一切,为了简单性,其他方便都可以做出牺牲。牺牲正确性、一致性——不一定要完全兼容、完整性——覆盖所有场景。

“失败案例”:

复杂但正确的模型的特点是有一个简单、正确、不妥协的接口。但是可能由于现有的系统足够使用,概念过于复杂不易理解没有得到发展的机会。

成功案例:

Worse Is Better成功的关键是它由于简单成功的传播,它不拘泥于正确、本质之类的东西,它能够吸引更多人使用,快速演化。它只要一个点做得好打到痛点就能成功,它不用很完美但不要犯大错。但是当遇到极端情况时,业务和基础设施的复杂性就会暴露出来,你最终还是要使用正确的方法解决它。

总结

我认为简单且解决问题的模型>复杂但正确的模型>各种莫名其妙的抽象徒增复杂度的模型。因为总是存在复杂但正确的模型处理不了的,或者只能解决部分的。

  1. 写软件的时候会发现,通往正确的路径只有一条,但是中间的错误路径千千万。有些路径是你能够处理的,有些错误是无法处理的,还有一些是你没有考虑到的和留下的bug。当代码量大的时候,软件不可靠是必然的事情,更何况你的软件还依赖操作系统、数据库等各种基础设施
  2. 业务总有一些极端情况,比如需要极低延迟或超大数据传输。
  3. 物理上的、或者数学上的限制,比如CAP原理,以及系统固有的复杂度

换句话说,任何理论任何系统都有适用条件,没有例外,比如$ F=ma $。(虽然这句话本身有没有例外有待商榷)。所以如无必要,勿增实体,在需求内使用简单的方案,遇到解决不了的,你可以:

风潮

很多事情是分久必合,合久必分,循环迭代。

  1. 复杂的系统的概念越发的简化,留下的是最核心的、适用面广的、易于理解的、能正确工作的最佳实践。GFS(Google File System)的分布式文件系统实践中,系统的复杂度并没有减小,最后还是使用了C++实现了类似Erlang的一套容错逻辑。但是GFS仍然存在并发写入的问题,Kafka消息队列采用Append-Only日志,简化了系统设计,并且能够提供顺序保证。从70年代之后,计算机领域的方法并没有革命性进步,通信的问题和采用的容错方案仍是那么几种。更多的变化来自于硬件的升级,设计流程从瀑布式转换为更佳敏佳的流程,更快的响应需求。基础设施的概念更聚焦,简化,做好一件事。
  2. 由于“极端情形”对性能的追求,导致了特化模型:Linux异步抽象uring,外设接口,GPU,Json的可读性和RPC等等。早期的Http等协议是纯文本的,但是你会发现网络的5层模型有出现融合的趋势,底层的通信协议越来越难以优化了,就像Http3使用基于UDP的QUIC协议一样。

参考

  1. Joel on Software
  2. Unix编程艺术
  3. 为啥 Erlang 没有像 Go、Scala 语言那样崛起? - 布丁的回答 - 知乎