2021-07-23

复杂软件系统开发的第一原则: KISS

Views: 839 | Add Comments

俗话说:

Keep It Simple, Stupid!

由于大部分新手程序员在从学生转换成为工程师之前, 都经过所谓的"算法"编程训练, 特别是不少人还主动进行大量的"刷题"行为, 因此, 对"性能"的追求被潜移默化地植入了所有程序员的基因, 这就造成了程序员往往把细节上的所谓性能优化放到第一优先的位置.

这种片面追求细节性能, 从而缺少大局观的思维, 其实是非常错误的. 比如 C++ 程序员, 几乎把性能优化等同于减少内存拷贝和无锁(lock free), 认为内存拷贝等于性能差, 认为加锁等于阻塞, 因此, C++ 程序员的代码主要在语法和语句层面上做优化, 逻辑非常别扭. 这种观念是非常落后的, 没有大局观, 这种程序员的技术视野极其狭窄.

根据我多年的软件开发经验, 我想针对那些认为"性能最重要"的程序员, 表明一个事实: 所有的代码层面性能优化, 必然以增加逻辑分支, 增加更多的 Indirection 作为代价.

什么意思呢? 也就是说, 所谓的性能优化, 必然会在代码里增加 if 分支, 增加新的类, 从而造成代码行数膨胀, 类的数量变多, 类的交叉调用关系变得更加复杂混乱. 这些后果都是非常负面的, 甚至无法抵消减少1%内存拷贝带来的性能提升.

那么, 如果我们简化设计, 是不是一定会造成性能的下降呢? 并不是, 采取简单清晰的代码逻辑, 不仅对 CPU 友好, 而且对人友好, 是双赢的 - 赢了两次!

既然性能优化必然带来复杂度的增加, 而简化系统并往往能提高性能. 那么, 我们有什么理由在细节上抠性能优化呢? 我们应该脱离低级的形式化的语法层面的优化, 而把精力放在思想和逻辑上进行极大地简化.

对于复杂的软件系统, 需要每个人的思维里, 永远把 KISS 原则(Keep It Simple, Stupid!)放在最优先的位置, 而且, 要把所谓的"性能"放在最低的优先级.

我常常对新手程序员说:"如果有两个方案, 一个简单, 一个复杂, 你必须永远选择简单的方案, 没有任何理由去选择复杂的方案!". 但是, 新手程序员把片面的性能优化看得太重, 影响了他们的判断力.

对于一个团队, 会有一定比例人员流动. 如果选择了复杂的方案, 那就相当于封闭团队. 说句不好听的, 如果现有的成员离职了, 代码那么复杂, 别人怎么接手? 来了新人, 代码搞得七拐八绕的, 新手不花一年半载的话能改一行代码吗?

复杂的系统, 一定是持续进化的, 不可能一次就全做完了然后把人都开除掉. 公司不能有这种思想, 程序员也不能想着一次把性能全抠完. 公司要给时间, 给投入, 期待团队持续产出. 而程序员要选择简单的方案, 写简单的代码, 留下可以持续改进的架构复杂度空间.

除了性能优化, 过度设计也是违反 KISS 原则的. 很多程序员, 常常超前想多两步, 然后多加了一两个字段, 多留了一两个接口, 比如某些经验经常预留一些 reserved 字段. 这些经验有一定的历史作用, 但是, 对于他们想要解决的问题, 站的层次太低了, 依然是在表面形式化上面去解决问题.

思想可以超前, 但是行动上一定要保守.

在行动前, 可以多想两步, 但是, 不要多走一步. 把暂时用不到的代码全删掉, 用不到的就是没用的, 不需要写出来, 写出来了也不需要保留. 否则, 不仅对别人造成干扰, 还会把自己的思路带坏.

多想的那两步, 通过团队讨论传播出去, 成为团队的共识. 同时通过文档记录下来, 成为团队的传承. 说出来, 记录下来, 但是不要编出来(代码).

举一个实际遇到的例子. 某个程序员本能地设计了一个内存缓存类:

class Memcache {
	const static int kHashSlotNum = 32;
	std::mutex locks[kHashSlotNum];
	std::map<std::string, std::string> mms[kHashSlotNum];
};

可以看到, 他做了 Sharding, 用多个资源多个锁减少锁粒度, 根据 hash 决定将数据保存在哪个资源槽里. 我们已经分析过了, Sharding 是并发编程两原则 - ideawu.net中的第一原则. 虽然这个程序员不说, 或者仅仅出于"本能", 我们一眼就看出他在做表面功夫的"性能优化", 所以写出了这种丑陋复杂的代码.

在没有任何数据支撑, 没有任何现实场景需求的前提下, 程序员"本能"地就去做所谓的"性能优化", 从而被误导选择了复杂的方案, 这是多么普遍的错误啊!

所以, 我推荐他换成简单的, 直观的, 清晰的方案:

class Memcache {
	std::mutex mux;
	std::map<std::string, std::string> mm;
};

如果未来有实际数据证明一把大锁导致系统吞吐量达不到业务需求, 再改成多把锁, 现在没有必要在没有任何场景需求的前提下过度设计, 没事别"走两步", 走一步恰到好处即可.

Related posts:

  1. 并发编程两原则
  2. C++ const& 的坑
  3. Brian Hyland – Sealed With A Miss
  4. C++ Latch 实现
  5. C/C++编程的现代习惯
Posted by ideawu at 2021-07-23 21:44:28

Leave a Comment