『把大象放到冰箱里,需要哪三步?』——这是源于春晚小品的一个段子。
如果我们用编程语言Java来表达这个过程,那么大概是:
openFridgeDoor();
putElephantIntoFridge(elephant);
closeFridgeDoor();
如果写到这里就结束,那么本文不过是一个恶作剧罢了。
Are you kidding me?
而实际上,本文比你预想中的要严肃认真得多。
构思过程
假设,真的有这么一个把大象放到冰箱里的需求,并且有可编程的机器人,可以代为实现物理操作,那么,我们该如何设计代码呢?
冰箱的检查
按照原来的框架,第一步和第三步都是非常简单的。我们假定,用Robot
这个类的方法调用,来代表机器人操作。开关冰箱的操作可以表达为:
private void openFridgeDoor() {
Robot.openFridgeDoor(this.fridge);
}
private void closeFridgeDoor() {
Robot.closeFridgeDoor(this.fridge);
}
第二步操作比较复杂,需要细化一下。我首先想到的问题是冰箱。
成年大象的体积,比常见冰箱要大得多,这是一个难点。对此,程序上要做判断与处理。
private void putElephantIntoFridge(Elephant elephant) {
if (elephant.size() > this.fridge.size()) {
findABiggerFridge(elephant.size());
}
Robot.putElephantIntoFridge(elephant, this.fridge);
}
但是,这样就衍生了两个问题。如果大象太大,或者指定大小的大冰箱真的找不到,那该怎么办?
private void findABiggerFridge(long size) throws FridgeNotFoundException {
Fridge newFridge = Robot.findABiggerFridge(size);
if (newFridge != null) {
this.fridge = newFridge;
} else {
throw new FridgeNotFoundException(size);
}
}
此时,我们最不愿意见到的事情发生了。我们处理不了这个异常,代码需要重新调整。
此外,说到异常,Robot.putElephantIntoFridge
似乎也可能抛一个异常:ElephantDefeatRobotException
。
调整代码结构
不仅仅是大冰箱找不到的问题。即使找到了,在更换冰箱后,原冰箱的门没有关,新冰箱的门也没有打开。说到底,为什么要打开冰箱门才能发现不够大?
另外,也有命名问题。编程时,冠词a、an、the不应该出现;openFridgeDoor
也显得冗余,在这个语境中,没有人会认为openFridge
是打开冰箱的电源或后盖吧?
因此,最上层应改为:
public void putElephantIntoFridge(Elephant elephant) throws FridgeNotFoundException {
Fridge fridge = null;
try {
fridge = openFridge(elephant.size());
fridge.putElephant(elephant);
} finally {
if (fridge != null) {
fridge.close();
}
}
}
其中,openFridge
里应该包含发现大冰箱与打开冰箱门两个操作,以及发现不了就丢FridgeNotFoundException
的情况。
在这次调整中,我们把具体操作冰箱的Method都封装到冰箱这个类中。并且,用try-catch-finally来保证冰箱门的开关匹配。
不过,你可能已经发现了,我们还是没有处理异常。
其它问题
到了我们负责实现的最顶层,我们仍然无法找到『找不到大冰箱』、『大象干掉了机器人』这两个异常的处理办法。
其实,这确实不该由我们来处理,而且不能用catch就这么吞掉,不然上层还以为大象已经成功放到冰箱里去了。因此,异常应该传递到上层。
还有一个细节问题,大象到底能不能杀?
如果大象能杀,呃……这虽然有些残忍,并且可能触犯了法律,但是size
这个问题就好解决了。我们可以宰了大象,这样体积就可以减小。如果还是不行,还可以把肉剁碎,压缩一下嘛。
这种情况下,上面的代码又得调整。因为,每个大象有三个size
,一个是活着的大象需要的空间大小,一个是大象的肉的总体积,还有一个是压缩后的最小体积。
而且,你可能已经发现了,我为了简化问题,用的是一个long类型的大小,而非复杂的长宽高。
如果不能杀……说到底,为什么要把大象放到冰箱里?
活活冻死?这好像更残忍。
完整结果
ElephantHandler类,负责提供给外界调用,专门处理『把大象放到冰箱里』这件事。
public final class ElephantHandler {
private Robot robot;
public ElephantHandler(Robot robot) {
this.robot = robot;
}
public void putElephantIntoFridge(Elephant elephant) throws
FridgeNotFoundException, ElephantDefeatRobotException {
try (Fridge fridge = openFridge(elephant.size)) {
fridge.put(elephant);
}
}
private Fridge openFridge(Size size) throws FridgeNotFoundException {
Fridge fridge = this.robot.findBiggerFridge(size);
fridge.open();
return fridge;
}
}
上面的代码又做出了一些改进。
- 用Robot的实例,而非类。
- 冰箱的开关,用Java 1.7的try-with-resource特性来控制。
- 找冰箱的操作,完全委托给机器人。
-
putElephant
改成put
,语意更简洁,在当前情况下也不会混淆。
下面是Fridge类。
final class Fridge implements AutoCloseable {
private final Robot robot;
Fridge(Robot robot) {
this.robot = robot;
}
@Override
public void close() {
this.robot.closeFridge(this);
}
void open() {
this.robot.openFridge(this);
}
void put(Elephant elephant) throws ElephantTooBigException {
this.robot.putElephantIntoFridge(elephant, this);
}
}
还有FridgeNotFoundException
等几个异常类,行文从简,略。
为什么我在哪里都没有处理这个ElephantTooBigException?因为不知道怎么处理。
到这里,你必然已经发现了,重要的操作都在Robot里,而我却没有给出Robot这个类的代码。
这个嘛……就不要纠结了,难道我真的要把大象宰给你看?
意义
应该没有人会真的认为,我写这篇文章是真的想介绍怎么把大象放到冰箱里吧?
我想以此为例,谈谈代码的层次、项目的模块、以及错误的架构。
代码的层次
在这里,有三层代码。
上层,传入Elephant
、调用ElephantHandler.putElephantIntoFridge
的模块;
中层,就是我们实现的部分,做一些业务逻辑的处理;
下层,负责干实事的Robot。
实际的编程,往往都发生在中层。
这样的分工是必要的。每一个实际的项目,都会逐渐变得复杂。唯有模块分明,才能更好地分工协作,最终完成。
本文展示的代码,集中精力解决『把大象放到冰箱里』的步骤,梳理了合适的流程,处理了冰箱、大象、机器人之间的关系,并且给出了可能的异常状况。
代码的责任链
既然是玩面向对象编程,你对责任链模式应该不会陌生。实际上,异常系统就是一个责任链模式。
异常是必须要被处理的,问题是谁来处理。
ElephantTooBigException是我需要处理的异常,然而,如你所见,我没有处理。因为,我已经作了相应的流程控制,确保这个异常不会发生。我写的这两个类,主要目的就是这个。如果真的发生了,那么毫无疑问是我的问题。但我不应该增加catch,而是要去检查流程与逻辑,确保这个异常不会发生。如果我为了确保万无一失,增加catch,这只是自欺欺人,让问题发生时更难找到原因。
Robot也是有一些其它异常的,比如冰箱门打不开,或者关不上。但这是它自身必须确保实现的功能问题,应该由Robot的开发者来解决。所以,我的代码里就根本不考虑这两个操作可能出问题。如果真的出问题,bug应该丢给Robot的开发者,与我无关。
ElephantDefeatRobotException是上层应该处理的异常,毕竟,Robot是上层传递给我的。Robot被干掉了,应该由上层来换一个更强的Robot;如果上层的Robot都被(五杀暴走的大象)干掉了,那么也该是上层向它的上层抛异常,也与我无关。
FridgeNotFoundException这个异常,恐怕上层也无法解决。这有两种可能:一是Robot的问题,明明有够大的冰箱,它却找不到,这情况类似于冰箱门打不开;二是最终用户的问题,市面上根本没有能装下大象的冰箱,你下的这个命令是什么意思?总之,还是与我无关。
(瞧我这精湛的甩锅功力,只问你服不服?)
项目的模块
如果我只是在谈程序员该如何设计代码的层次结构,那么未免太小。实际上,经验丰富的程序员不需要我来提点,而菜鸟们更应该去看《重构》、《完美代码》、《代码整洁之道》之类的大书,看散文没什么用。
我真正想谈的是项目管理。
前面说了多次『与我无关』,建议在看本文的一线程序员,切勿模仿!
因为,无论是懂技术的开发Leader,还是不懂技术的大小Boss,都不喜欢听到这句话。他们更希望听到的是,这个问题与谁有关,最希望听到的是,这个问题怎么解决。『与我无关』,这句话只是把锅甩在地上,让锅没有人背,让问题不能及时解决。人人都说『与我无关』,那么问题由谁来解决?虽然他们是错的,但是人在屋檐下、不得不低头,职场中人还是要懂得明哲保身、趋利避害才是,以后不要这么说了。
刚才我好像说到『他们是错的』。既然说漏了嘴,那就说完好了。
在模块分明的项目中,每个人都独立地负责一个或几个模块。要证明『问题不是出在我的模块』,是很简单的,而要证明一定是别人负责的某个模块的问题,却比较难。如果能做到这一步,那么问题基本已经定位清楚了,要解决也不是难事,而时间的开销却不小。
在现在常见的处理模型中,更多的是让先遇到bug的模块负责分析。如果不是他的问题,让他就找到出问题的模块,并且转过去。在正常情况下,这样也是比较高效的。然而,不正常情况虽然数量少,却会占用大多数时间。让我们在工作中花费大量时间的,往往不是最擅长的本职工作,而是一些不熟悉不擅长的状况。
想想Java的异常系统,会发现这是更加简单有效的。在Java的每层调用栈中,遇到下面抛上来的异常,只有两个选择:该处理就catch住,不该处理就往上层抛。所以,只要证明『与我无关』,就够了。
而现实问题是,当代的issue处理系统,比如JIRA,其模块分工表是毫无联系的。既没有规定谁才能转问题给我们,也没有规定我们只能把问题转给谁。N个模块之间,是N×(N-1)/2
的关系。所以,如果只证明『与我无关』,就相当于把问题推给了其它N-1
个模块,而它们大多是与问题完全无关的。而且,更常见的是模块划分不够细,眉毛胡子一把抓的情况。
于是,『与我无关』成了禁语。我们不得不承担那些不属于我们的责任,只因为没有一个清晰的责任链。
错误的架构
在解决『把大象放到冰箱里』这个问题时,我们本来只是想细化一下放进去的操作,结果却完全摒弃了预定的三步法,还向调用方抛出了不止一个异常。
在实际工作中,我们就没那么好运了。我们面临的情况是,架构不能改,异常不能抛,一切问题自行解决。美其名曰:执行力。
这个小品里的这一段,之所以惹人发笑,不是因为『把大象放到冰箱里』这么复杂的问题,被白云大妈(宋丹丹饰)简单解决;而是因为这位见识浅薄、好大喜功的白云大妈,抖了抖机灵,给了个看似玄妙的办法,自以为解决了问题,其实完全不可行。
小品虽然可乐,而现实却很可悲。作为一线的执行者,我们有时只能为这种农妇式的高屋建瓴,加班加点地添砖加瓦。
代码与代码的关系,还是比较简单的。而人与人、团队与团队的关系,就复杂多了。有一些组织架构,注定低效,却无法可改。
执行公司的既定战略时,如果有一个底层员工发现了一个不可执行的关键异常,能否跨越七八层传递到CEO那里,最终改变原定计划?代码是可以的,人却往往不行。
结语
最终,我们还是不能把大象放到冰箱里,因为市面上还买不到能干掉大象的可编程机器人。
Yes, I am kidding. _