用Java把大象放到冰箱里

『把大象放到冰箱里,需要哪三步?』——这是源于春晚小品的一个段子。

如果我们用编程语言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. _

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容