A Philosophy of Software Design

Part I

软件设计的唯一问题: 复杂度

  • 被不断扩大的修改范围
  • 被不断增加的认知负担
  • 不被认知到的未知


如何定义复杂度

任何使你的系统变得不易被理解、不易被修改的事物。

表现在我们的场景里,比如:

 

  1. Hybrid Programming
  2. 接近 30w 行代码
  3. 花式同步
  4. 五颜六色的权限体系
  5. 八仙过海般的任务相关的操作
  6. etc

一些可能的解决方法

  • 使用一些适当的编程技巧做抽象、保证 关注点分离
  • 不要仅仅用行数的多少去衡量设计,有时候同一个设计,更多行数的那个实现可能要比更短的那个更 简单
  • 尽可能避免使用全局变量
  • 保证代码的一致性
  • 将重要的业务系统设计的逻辑变得 显而易见不要藏之于黑盒

 

仅仅能工作的代码是不够的

战术性编程 vs 战略性编程

 

战术性编程

优点: 以最快的速度完成可工作的功能

缺点: 以长期的视角来看, 容易积累技术债,增加复杂度

战略性编程

优点: 不仅仅可以工作, 还要易于修改、拓展

缺点: 每个功能需要时间去设计、去思考

战略投资不是无限的,如何平衡?

书中给的答案是: 10% ~ 20%

模块化设计

一个理想的世界里,每一个模块应该完全独立于其他模块,工程师们应该可以修改、运行它们而不用知道别的模块的存在。

 

呵呵,理想是丰满的、现实是骨感。大家醒醒。

我们的模块往往必须知道某些信息,才能被运作,而这些东西最后都成为了依赖。

Interface

接口用于表达一个模块是什么,能做什么事,而不应该描述它如何做这些事。(同时,作为使用者,我们也不应该关注它们)。

 

在很大程度上,接口的概念可以帮助我们去屏蔽不相关性。

 

WHAT

Implemention

实现则恰恰相反,由一系列的代码组成,而最后这些代码相互运作,可以保证完成 interface 向用户 (工程师) 做出的承诺。

 

这些代码可能是异构的、可能是晦涩。但对于实现而言,最重要的是保证行为的一致。

 

HOW

一个好的设计里,接口应该要比它的实现要简单的多。

构建抽象

抽象是一个被 剥离了不重要 的内容的实体的概览

 

而往往,大部分的设计中,抽象这个概念本身都以 interface 的形式被表现。

一个抽象的好坏,取决于你对一个模块究竟可以正确的 剥离出多少 不重要的 内容

比如对于 Nodejs 而言,异步就是一种抽象,它抽象了 kernel 的真实的 IO 行为,  并且屏蔽了它的性质,开发者不需要知道它的真实平台是什么。 windows / unix ? overlapped io / select etc。这些东西都是不重要的。

但相应的,将接口暴露成异步风格是重要的。它被用于表达: 什么时候,某件事真的被做完了。这些信息对于开发者而言则是必须的。

Deep module vs Shallow module

一个好的模块:

  • 尽可能少的接口
  • 尽可能多的功能

Deep module Example:

Unix basic system call for I/O

(5个调用用了一辈子)

Garbage Collection

(直接消灭了所有的内存管理相关的复杂度)

Shallow module Example:

private void addNullValueForAttribute(String attr) {
  data.push(attr, null)
}

(函数并没有提供任何抽象,名字更长了,但从目的来看不如直接操作数据来的直接)