软件里的"上帝"不只一个。它落过两次:一次落在一个类 上,叫上帝类;一次落在一次操作 上,叫上帝之手。前一个看体积,后一个看位置。下面看下不同领域对上帝的描述,再看它落在软件工程中的表现。

一、系统化:七个领域里的"上帝"
这个词不是软件造的。它在七个地方被用过,每次都在借同一个意思。
| 领域 | 上帝在那里指什么 |
|---|---|
| 足球 · 1986 | 一个不在场的手替他进了球 |
| 圣经 | 神的手直接干预人间 |
| 戏剧 · 机械降神 | 剧情解不开,神被吊下来收场 |
| 游戏 · God Hand | 玩家拥有一切权限 |
| 法学 · Act of God | 人力无可奈何的自然力 |
| 物理 · 上帝粒子 | 一切质量的源头 |
| 哲学 · 上帝视角 | 不站任何位置,却看得见一切 |
看着散,其实只回答三个问题。
| 组 | 领域 | 上帝在那里的角色 | 软件借成什么 |
|---|---|---|---|
| 做 | 足球、圣经、戏剧 | 替人动手 | 上帝之手 |
| 定 | 游戏、法学 | 定人定不了的事 | 上帝之手 |
| 知 | 物理、哲学 | 知人知不了的事 | 上帝类 |
软件只借了两个:"做"和"定"借成上帝之手,"知"借成上帝类。
七次借用里,没有一个是"受约束的"。上帝粒子是最小单位,上帝视角是无所不见,Act of God 是人力无可奈何------它们不是"管得多"那么简单,是"没有上限"。
顺带一句:1986 年那次,赛后发布会没有官方翻译,英媒当天也没按原声报道------这个词从第一天起就是转述出来的。软件借的也不是足球,是它背后那个**不受约束**的意思。
二、全局化:软件里的两个上帝
1、上帝类:一个类装得太多
java
class DeptService {
void add();
void remove();
void move();
int totalBudget();
boolean checkPermission();
void syncToOtherSystem();
void sendNotify();
}
七个方法,四类不相干的事:增删、算钱、鉴权、对接外部系统。这就是上帝类。
Riel 在 1996 年给过一条最好用的直觉:**看到名字里带 Driver、Manager、System、Subsystem 的抽象,要非常怀疑。**
2、上帝之手:一次操作被抢走
java
class TreeService {
void addChild(Node p, Node c) {
p.children.add(c);
c.parent = p;
}
}
四行,小得不能再小。可它还是上帝------因为它在替节点做联系。
3、上帝之手怎么长出来的
三个原因叠在一起:需求一条条加进来,每次都"先放这儿能跑";没人问一句"该不该它管";这句没问,边界就没了。
什么时候冒头?第二次改同一个文件的时候。 第一次改是正常开发;第二次发现"怎么又是这儿",那就是它长出来了。
4、后果
谁受罪:改需求的人怕碰坏别处,写测试的人要造五个依赖才能测一个方法。长在哪层:Service 层最容易------它天生叫"服务",什么都能往里放。
代价还不一样。上帝类的代价是改不动;上帝之手的代价是可能已经错了,而且没人知道。
5、名字:为什么不叫那些
不叫耦合。 耦合管的是"谁依赖谁":DeptService 依赖 DeptRepository,DeptRepository 一改,DeptService 就得跟着改。可就算把它抽成接口、换个实现,c.parent = p 那一行还在 DeptService 里,一动没动。降耦合换掉的是依赖,不是这一行。
不叫中心、控制器。 这两个词说的是代码摆在哪儿、是不是所有人都在调它。看这个:
java
class SchoolService { void addGrade(Grade g, School s) { g.school = s; } }
class GradeService { void addClass(Cls c, Grade g) { c.grade = g; } }
class ClassService { void addStudent(Stu t, Cls c) { t.cls = c; } }
三个类平级、互不依赖,谁也不在中心。可三行父子关系都是别人给的。没有中心,手还在。
不叫迪米特法则。 迪米特管的是"你该不该知道别人的内部",上帝之手管的是"这一行谁执行"。给调用方一个 addChild 句柄:他不知道 children 是 List 还是 Map、走不走缓存,但他能让两个节点变成父子。他不知道你的内部,他照样能定关系。
而且迪米特能被形式合规:包一层 Facade、加一个 getter,检查就过了,那一行赋值一动没动。规则可以绕,赋值绕不过。
这里也就顺手答了那个问题------什么时候用比喻,什么时候用规则:
要人承认"这说的是我" ,用比喻;要人判定"这是错的",用规则。
比喻不需要精确,只需要让人一眼看见自己。上帝类、上帝之手都是比喻,所以它们都缺一句话的判据。
三、判据与边界
1、一行判据
java
c.parent = this; // 写在 Dept 里,还是写在 DeptService 里
上帝之手只有一个判据:这一行赋值写在谁家。
为什么落在这一行,而不落在"方法在谁家"?因为方法能搬,判断它的那句话搬不走------addChild 里调一个外面的 validator,形式上方法在节点,判的还是外面。方法能搬,那一行赋值搬不掉。
上帝类的判据也只有一句:随便挑两个方法,你说不出它们为什么在同一个类里。
两个动作可以对着代码验:把树中间的一个节点摘下来,它应当是一棵完整可用的子树;把那个专门帮它挂孩子的类整个删掉,树应当照活。
2、边界:它还要跟别的类打交道
关系由节点自己做,可它不孤立------一次「把部门调走」,往往还得开事务、查权限、加锁。这些它自己做不了。
但这里有个容易混的地方:给它一个环境,不等于替它做动作。
java
@Transactional // 环境:整段要么成功要么全废
public void transfer(Long id, Long pid) {
Dept d = repo.find(id);
Dept p = repo.find(pid);
d.attachTo(p); // 动作:还是节点自己走的
}
换一种写法,事务照样在外面:
java
class DeptService {
@Transactional
void transfer(Dept p, Dept c) {
p.children.add(c); // 动作被替了
c.parent = p;
}
}
两段代码里 @Transactional 一模一样,位置都在外面。差别只在 parent 那一行谁写。
所以事务、锁这类东西,套在外面不算上帝之手。环境可以包在外面,动作不能替。
权限要单独看。如果规则是「只有本级管理员能删」,那是节点自己的判断,把用户当参数递进去就行:
java
void remove(User by) {
if (!by.isAdminOf(this)) throw ...;
}
只有「这个用户有没有某个系统权限码」那种才留在外面------那得问权限系统,节点问不到。
这一节要回答的其实是:关系归节点,资源归外面------这两件事不冲突,而且必须同时成立。
四、每个类都应该是自己的上帝
上帝不允许有上帝------不许有一个凌驾在所有类之上的上帝。
把所有事揽进一个类,是上帝类;把别人的关系替他写好,是上帝之手。反过来只有一种做法:把神权切成很多份,每份只有一个类那么大。 上帝没有被取消,是被切开了。
于是两个落点:
- 对上帝类:一个类管的地方,不该超过它自己那一块
- 对上帝之手:一个类不该替别的类做关联。
判断一个类合不合格,最后只看一句:它管的是不是它自己那一块。