用Java把大象放到冰箱里

作者: 匿蟒 | 来源:发表于2017-01-03 21:51 被阅读577次

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

    如果我们用编程语言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把大象放到冰箱里

      本文链接:https://www.haomeiwen.com/subject/zsivvttx.html