软件流程、类型、设计和Worse Is Better
柏舟 新冠4年 07-26
业务的系统设计本质上是对复杂度进行管理,对外提供简单、所需的抽象,内部实现屏蔽掉多余的复杂度。这是最常见的瀑布式开发流程:
从用户角度看的五种软件
用户的不同需求就决定了软件开发没有统一的开发流程,比如依赖不公开的第三方库,使用TDD不一定好用。
终端APP
可以是开源软件、商业软件,也可以是网站,特点是被大量用户自由使用的软件。问题:
- 用户很多,众口难调,用户界面需要易于使用。还有一些本地化问题,如阿拉伯文等等。
- 需要兼容各种各样的电脑或浏览器(IE),甚至用户会更改系统时间。
一般有:
- 开源软件:没有资金支持,通常可用性不高,因为有些一般用户需要的功能对于开发者来说不够有意思,最典型的就是Linux桌面。
- Web应用:需要界面简单,兼容多个浏览器或者微信小程序。
- 咨询软件:需要定制化开发和部署,比如CMS。
- 商业软件:图片、视频剪辑,工业软件。
内部软件
只需要在一家公司内部运行,可以对运行环境进行假定。因为只有少数人使用,导致以下特点:
- 可以依赖系统其它软件,比如Excel等等,因为公司内部没有可以装。
- 可用性在内部软件开发中优先级很低,使用者别无选择。
- 分摊到每个使用者的成本很高,所以资源匮乏,需要开发速度快,以节约成本。
- 不需要很高的质量,只需要完成既定功能,所以可能bug更多,界面不美观。
嵌入式软件
- 不可升级。
- 质量要求高。
- 硬件性能有限,需要追求优化而不是代码优雅。
游戏
- 赢家通吃:某些游戏很热门,但大多数是失败品。
- 质量要求高,玩家通常不会玩第二次。
一次性软件
为了得到某些信息写的临时性代码,比如Shell脚本。临时性的代码可能会转化成内部软件。
从状态角度看的软件
软件常见的状态来源:
- 用户信息。
- 用户设置。
- 功能中的状态:比如用户写的word文档、浏览器历史、定时任务等等。
不同软件对状态的要求不同,具体体现在:
- 存储位置:服务器还是本地。
- 是否需要同步。
- 存储格式:必须使用公开标准还是可以自定义。
- 状态大小和复杂性。
- 实时性要求:游戏,在线多人编辑。
特性 | B/S | C/S | Standalone |
---|---|---|---|
概述 | 所有状态以服务器为准 | 用户APP含有状态 | 离线应用,自身维护状态,不需要同步 |
典型应用 | 网页 | 游戏、聊天应用 | 设计软件 |
状态维护方式 | 每次拉取所有状态 | 通过消息同步 | 本地存储状态 |
场景 | 状态简单的,小的 | 状态复杂的,需要离线的 | 工具 |
特点 | 设计简单 | 状态同步复杂 | 设计简单 |
需求之外的问题:持续演进和极端情况
系统设计中,除了基本的需求分析,常常需要考虑以下问题:
- 需要考虑到硬件的演进、升级,需要中间层对硬件能力进行抽象。
- 需要与开发团队的组织架构相适应。
- 为了可靠性在上层引入额外的抽象(如事务、错误处理等等),引入额外的复杂度。
- 抽象的保证有限,极端情况会泄露:虽然TCP提供可靠的连接,但是网断了TCP仍然不能工作。
关于问题3和4,由于常常需要权衡可靠性和效率。如果为了效率暴露过多的底层能力,那么上层开发需要了解很多底层概念,并且底层的模型实现也很容易限制死了,最典型的是Python的GIL锁。如果为了抽象的简洁,提供有限的能力,可能接口的性能不佳,限制开发人员使用。
由于现实的复杂多变,很容易遇到一些极端的需求,导致抽象泄露,不得不在原有的抽象上打各种补丁,而且还需要上层的开发人员了解底层的技术细节。还有的为了潜在的需求,对系统进行过度设计,引入各种毫无意义的复杂度,不仅难以理解,而且还不好Debug。
这些问题没有银弹,你需要:
- 仔细了解实际的需求,验收标准。
- 平衡好业务、编程语言、基础设施的复杂度。
- 管理好软件质量和开发周期的平衡。
Worse Is Better
Common Lisp专家Gabiel于1989年提出Worse Is Better,它强调强调简单压倒一切,为了简单性,其他方便都可以做出牺牲。牺牲正确性、一致性——不一定要完全兼容、完整性——覆盖所有场景。
“失败案例”:
- Erlang:是一门由爱立信于1991年发布的语言,具有很高的可靠性,主要用于电信行业。在很早就有类似Actor模型高并发,OTP用于分布式系统,独特的错误处理方式。但是Erlang兴起的时候,没有高并发和分布式需求,互联网的业务也很简单,当时主要的限制是硬件而不是编程模型。当编程模型成为瓶颈已经是移动互联网的时代了。
- Plan9:是bell实验室在80年代开发的操作系统。socket、进程都可以通过文件的读写操作完成,可以用cat读取进程和socket,再也不需要ps和nc了。Plan9天生屏蔽了本地调用和RPC的区别!此外,Plan9还有namespace,每个用户都有自己的系统服务视图,Linux有namespace、cgroup(2006)、docker(2013)已经是很久以后的事情了。
- 事务:在关系型数据库中广泛使用,因为早期的计算机设备主要用于科学计算和金融的事务性处理。但是随着互联网的发展,很多新的NOSQL并没有实现事务。
复杂但正确的模型的特点是有一个简单、正确、不妥协的接口。但是可能由于现有的系统足够使用,概念过于复杂不易理解没有得到发展的机会。
成功案例:
- MySQL
- Unix
- C,Js,Go
Worse Is Better成功的关键是它由于简单成功的传播,它不拘泥于正确、本质之类的东西,它能够吸引更多人使用,快速演化。它只要一个点做得好打到痛点就能成功,它不用很完美但不要犯大错。但是当遇到极端情况时,业务和基础设施的复杂性就会暴露出来,你最终还是要使用正确的方法解决它。
总结
我认为简单且解决问题的模型>复杂但正确的模型>各种莫名其妙的抽象徒增复杂度的模型。因为总是存在复杂但正确的模型处理不了的,或者只能解决部分的。
- 写软件的时候会发现,通往正确的路径只有一条,但是中间的错误路径千千万。有些路径是你能够处理的,有些错误是无法处理的,还有一些是你没有考虑到的和留下的bug。当代码量大的时候,软件不可靠是必然的事情,更何况你的软件还依赖操作系统、数据库等各种基础设施
- 业务总有一些极端情况,比如需要极低延迟或超大数据传输。
- 物理上的、或者数学上的限制,比如CAP原理,以及系统固有的复杂度。
换句话说,任何理论任何系统都有适用条件,没有例外,比如$ F=ma $。(虽然这句话本身有没有例外有待商榷)。所以如无必要,勿增实体,在需求内使用简单的方案,遇到解决不了的,你可以:
- 跑路。
- 都是选型的人的锅。
- 这是一个制造需求的机会。
风潮
很多事情是分久必合,合久必分,循环迭代。
- 复杂的系统的概念越发的简化,留下的是最核心的、适用面广的、易于理解的、能正确工作的最佳实践。GFS(Google File System)的分布式文件系统实践中,系统的复杂度并没有减小,最后还是使用了C++实现了类似Erlang的一套容错逻辑。但是GFS仍然存在并发写入的问题,Kafka消息队列采用Append-Only日志,简化了系统设计,并且能够提供顺序保证。从70年代之后,计算机领域的方法并没有革命性进步,通信的问题和采用的容错方案仍是那么几种。更多的变化来自于硬件的升级,设计流程从瀑布式转换为更佳敏佳的流程,更快的响应需求。基础设施的概念更聚焦,简化,做好一件事。
- 由于“极端情形”对性能的追求,导致了特化模型:Linux异步抽象uring,外设接口,GPU,Json的可读性和RPC等等。早期的Http等协议是纯文本的,但是你会发现网络的5层模型有出现融合的趋势,底层的通信协议越来越难以优化了,就像Http3使用基于UDP的QUIC协议一样。
参考
- Joel on Software
- Unix编程艺术
- 为啥 Erlang 没有像 Go、Scala 语言那样崛起? - 布丁的回答 - 知乎