用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. _

你可能感兴趣的:(用Java把大象放到冰箱里)