欢迎来到 陈本布衣 的博客小园

关于搓澡的技术演进

  夏天到了,天气炎热,每天都是臭汗直冒;自己再怎么不爱干净,还是要坚持每天洗澡,不然婆娘肯定是不让上床的,毕竟,谁也不愿意啃一坨臭烘烘的肉。

  所以,洗澡事小,关系犹大。作为程序员,尤其是 Java 程序员,博主还煞有介事的,把每日的洗澡的步骤给记录了下来——

1 public class Bath {
2     public void bathing(){
3         System.out.println("入浴:更衣解带,进入浴房");
4         System.out.println("洗头:一盆顶水湿全身,先抹飘油洗秃顶。。。");
5         System.out.println("洗身:浴球在手,全身都有——左刷刷,右刷刷,前刷刷,后刷刷,上刷刷,下刷刷。。。");
6         System.out.println("出浴:洗罢冲净,盛装出浴");
7     }
8 }

  当然,以上只是最基本的操作步骤,其实每一个步骤都还有很多不可描述的细节没有具化——毕竟,洗澡还是一个颇为复杂的系统性操作。后来,有好事者觉得步骤太过简略,向博主求问洗澡的各种实现细节,博主不堪骚扰,决定将原来的步骤进行细化。但是,如果直接在原方法步骤里面再去添加长篇大论的描述(脑补一下我在每个打印中再扩展1万字说开去),会严重影响原方法代码中洗澡步骤的连贯性和可读性,代码会显很冗余,不太好看。作为一个惯于封装的 Java 程序员,怎么可能容忍那么 low 的代码呢?所以,基于代码整洁的考虑,博主首先想到了方法抽取,将洗澡的各个步骤抽取出来成为单独的业务方法,然后再给原方法按步骤调用就行了,于是,原来的代码就变成了下面这样子——

public class Bath {
    public void bathing(){
        this.enterTheBathroom();
        this.washThehair();
        this.washTheBody();
        this.outTheBathroom();
    }


    private void enterTheBathroom(){
        System.out.println("其实,对于我等屌丝来说,宽衣解带?不存在的。经常是一条裤子一挎,就已经是裸体状态了。。。(此处有1万字)");
    }

    private void washThehair(){
        System.out.println("我不经意间摸到了自己不再茂盛的秀发,不觉有些感伤,想到自己撸码不过几年光景,头发就如秋风一扫,簌簌直掉。。。(此处有1万字)");
    }


    private void washTheBody(){
        System.out.println("其实自己喜欢舒肤佳多一点,喜欢香皂在自己肌肤上滑过的细腻感。不过因为太滑,香皂总掉旁边的茅坑里,即使侥幸没掉坑里,也总怕谁冷不丁的在背后替我将肥皂捡起。。。(此处有1万字)");
    }

    private void outTheBathroom(){
        System.out.println("洗完澡我才猛然发现,根本就没带衣服进来。。。(此处有1万字)");
    }
}

   代码改完,搓澡细节展示更加生动了,博主颇感满意。某天夫人洗澡时脚滑踩蹲坑里去了,出来又对我破口大骂,无非是哪个闺蜜嫁得好人家咯,洗澡都用淋浴房而不是站在蹲坑上;哪个闺蜜家里又是专用浴缸可以泡牛奶浴咯云云,都是些陈词滥调的抱怨,自己早就听的不耐烦了,由她去。不过,这一骂倒是让自己对上面的搓澡步骤又有了新的想法。所谓人分三六九等,阶层不同,洗澡的场所、方式也就大不一样。单说洗澡的场所,有钱人进的是专用浴缸或者专门的淋浴房,而自己进的是多功能的搓澡茅房。不过也还好啦,好歹是个单间,比上不足比下有余,像我工地搬砖的表弟,就只能进可以欣赏彼此 pp 的人民澡堂。

  所以,回看自己上面的代码,就发现一个很严重的问题,那就是没有普适性,可移植性太差,因为只有自己是这样长篇大论的洗澡的,别人都不是。所以,为了通用性,很有必要将每个步骤进一步抽象。怎么抽象呢,第一直觉是,将几个搓澡方法用接口定义,然后不管是有钱人还是我这样的屌丝抑或搬砖的表弟,要洗澡,实现接口细化操作即可,似乎没什么难度。但是这里有一个问题,就是实现接口可以,但是上述搓澡的操作顺序必须要有保证,你总不能先穿衣出来然后又脱衣进去吧?所以,必须要对搓澡步骤有一个明确的定义,任何要洗澡的人,对接口进行实现的时候,都只能按照搓澡步骤来执行实现的方法,不能乱套!

  基于这种思想,博主很爽利的将上述代码改成了下面的形式——

public interface BathStep {

    void enterTheBathroom();

    void washThehair();

    void washTheBody();

    void outTheBathroom();
}


public class MyBath implements BathStep{

    public void bathing(){
        this.enterTheBathroom();
        this.washThehair();
        this.washTheBody();
        this.outTheBathroom();
    }

    @Override
    public void enterTheBathroom(){
        System.out.println("其实,对于我等屌丝来说,宽衣解带?不存在的。经常是一条裤子一挎,就已经是裸体状态了。。。(此处有1万字)");
    }

    @Override
    public void washThehair(){
        System.out.println("我不经意间摸到了自己不再茂盛的秀发,不觉有些感伤,想到自己撸码不过几年光景,头发就如秋风一扫,簌簌直掉。。。(此处有1万字)");
    }

    @Override
    public void washTheBody(){
        System.out.println("其实自己喜欢舒肤佳多一点,喜欢香皂在自己肌肤上滑过的细腻感。不过因为太滑,香皂总掉旁边的茅坑里,即使侥幸没掉坑里,也总怕谁冷不丁的在背后替我将肥皂捡起。。。(此处有1万字)");
    }
    @Override
    public void outTheBathroom(){
        System.out.println("洗完澡我才猛然发现,根本就没带衣服进来。。。(此处有1万字)");
    }
}

  这代码稍微有那么点意思了,但是细品一下还是有很大问题。第一,规范搓澡步骤的方法没有抽离出来,而是让接口的实现者去定义,这看起来很荒谬;第二,虽然将搓澡步骤都提成了接口,满足了自定义的需求,但问题恰恰在于,有些步骤并不需要自定义实现,比如washTheBody,管你谁谁谁,肯定也是差不多的要搓上搓下吧;比如出浴,也都是穿好衣服再出来吧。所以,全部定义成接口并不是最好的方式,很多时候你会为了满足一些共性操作,不得不单独定义一个适配器类来对接口中需要的共性方法做一些默认的实现,反倒是增加了复杂度(虽然Java8之后的接口已经可以默认实现了,但为了满足更大的版本兼容性,博主这里依然还采取传统一些的方式吧)。

  思来想去,既然接口不能很好的满足,那就只能抽象类了。稍以琢磨,定义抽象类倒有不少好处——

    一,对于不需要搓澡人自定义的步骤,直接在抽象类中功能实现即可,不用绕圈子;哪怕谁对步骤有自定义需求,子类覆盖父类方法即可(也看个人设计是否允许覆盖);

    二,对于需要搓澡人自定义的步骤,定义为抽象方法,强制子类对步骤做具体的自定义实现;

    三,对于不可变的搓澡纲领问题,在该抽象类中可很方便的以单独的方法进行明确约束定义,同时,为防止具体的搓澡人随意更改步骤,很有必要将该约束搓澡步骤的方法定义为 final 的;

  思想进一步明确之后,博主又很爽利的将搓澡的代码改成了下面这个样子——

public abstract class Bath {
    
    public final void bathing(){
        this.enterTheBathroom();
        this.washThehair();
        this.washTheBody();
        this.outTheBathroom();
    }

    // 需要子类具体实现的步骤
    abstract void enterTheBathroom();
    
    // 需要子类具体实现的步骤
    abstract void washThehair();

    public void washTheBody(){
        System.out.println("其实自己喜欢舒肤佳多一点,喜欢香皂在自己肌肤上滑过的细腻感。不过因为太滑,香皂总掉旁边的茅坑里,即使侥幸没掉坑里,也总怕谁冷不丁的在背后替我将肥皂捡起。。。(此处有1万字)");
    }
    
    public void outTheBathroom(){
        System.out.println("洗完澡我才猛然发现,根本就没带衣服进来。。。(此处有1万字)");
    }
}

  所以,你悟出什么了吗,这就是模板方法模式啊!上面代码中的 bathing 方法,即是所谓的模板方法——

    在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中去;模板方法可以在子类不改变算法结构的情况下,重新定义算法中的某些步骤。

  自此,你再带着定义再回头看看博主关于搓澡的技术演进,你应该就比较明了了——所谓的模板方法模式,也就那么回事。

  当然,上面的代码场景还是有不灵活的地方,比如步骤中搓澡才是核心功能,洗头与否,是完全看个人心情的,你这强行要求别人洗澡必洗头,怕是不太好吧?如何将决定权交给搓澡人自己呢,这也是模板方法这种模式中经常运用到的,通过添加钩子判断函数,让搓澡人根据心情去覆写改变判断逻辑,从而达到间接干预模板方法执行流的目的。模板方法都通透了,添加钩子就是易如反掌的事情了。博主就不贴代码了,让搓澡的你,自己去实现为妙!

模式生态

  对于模板方法模式的运用,不论JDK自己的类库中还是我们常用框架的源码中,你都能很轻松的找到很多示例供你参考学习。博主试举几例,做个引子。

  比如,在JDK的源码中IO流中,你可以看到所有输入字节流的超类 InputStream是下面这样的——

  

   输出字节流、以及字符流等形同此例,都是基于抽象类先提供基本的算法骨架,然后子类在进行具体的方法实现。

   再比如,Java 程序员早已离不开的 Spring,在其帝国源码版图中,你也随处能找到形似的或神似的模板方法代码。比如下面这段在 AbstractApplicationContext 抽象类中刷新容器的代码——

public void refresh() throws BeansException, IllegalStateException {
        synchronized (this.startupShutdownMonitor) {
            // Prepare this context for refreshing.
            prepareRefresh();

            // Tell the subclass to refresh the internal bean factory.
            ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

            // Prepare the bean factory for use in this context.
            prepareBeanFactory(beanFactory);

            try {
                // Allows post-processing of the bean factory in context subclasses.
                postProcessBeanFactory(beanFactory);

                // Invoke factory processors registered as beans in the context.
                invokeBeanFactoryPostProcessors(beanFactory);

                // Register bean processors that intercept bean creation.
                registerBeanPostProcessors(beanFactory);

                // Initialize message source for this context.
                initMessageSource();

                // Initialize event multicaster for this context.
                initApplicationEventMulticaster();

                // Initialize other special beans in specific context subclasses.
                onRefresh();

                // Check for listener beans and register them.
                registerListeners();

                // Instantiate all remaining (non-lazy-init) singletons.
                finishBeanFactoryInitialization(beanFactory);

                // Last step: publish corresponding event.
                finishRefresh();
            }

            catch (BeansException ex) {
                if (logger.isWarnEnabled()) {
                    logger.warn("Exception encountered during context initialization - " +
                            "cancelling refresh attempt: " + ex);
                }

                // Destroy already created singletons to avoid dangling resources.
                destroyBeans();

                // Reset 'active' flag.
                cancelRefresh(ex);

                // Propagate exception to caller.
                throw ex;
            }

            finally {
                // Reset common introspection caches in Spring's core, since we
                // might not ever need metadata for singleton beans anymore...
                resetCommonCaches();
            }
        }
    }

  或许看不懂方法执行过程中每一步的意思,但其一步两步的代码走位风格,和博主搓澡步骤是何其的形似啊。还有一些常见功能如操作数据库,事务管理等,其原始操作本身就有明显的步骤先后的要求——也即模式术语所谓的算法骨架(如数据库操作要加载驱动、建立连接、预编译、执行sql、释放资源等),所以采用模板方法也是非常应景的操作手法。只不过,Spring对原始操作进行的封装和转换等处理,让你可能不是那么容易的能从繁杂的代码中观其大略的,但至少,有了这一层思想之后,你在看源码的时候,或许更能触类旁通一些东西!

布衣说法  

  看到这里,可能有些小伙伴会觉得博主所讲的或所举示例不是那么标准,有些形似又有些神似,完全没有搓澡的时候那么爽利啊。

  那么,关于设计模式的学习,你觉得你应该形似还是神似呢?学其形,势必要刻板的遵循定义,依葫芦画瓢,那只是没有灵魂的模仿;悟其神,参透模式背后的设计思想,才是你能灵活运用的基本前提。悟道之后,你可以发散很多撸码的思想。譬如,博主上面提到的干预模板方法执行流的钩子方法,实际运用中,我一定要定义一个钩子判断才行吗,我可以不以给方法一个空实现,不也能达到只搓澡不洗头的目的吗?譬如 Spring那个刷新容器的 refresh 方法,你会发现其方法步骤的每一步其实都在本地完成了实现,并不没有定义成抽象方法交给子类去实现。是的,人家只是要基于代码整洁的考虑,将复杂的业务通通封装成一个个单独的方法,逻辑清晰明了,代码整洁不冗余,这样的模板方法可不完全符合模板方法的定义的,但看起来爽啊!所以,你的模式学习,是不是还要那么循规蹈矩,呆板教条呢?

     自此,布衣博主版的模板方法模式,拉拉杂杂的,也算阐述完毕了。啥玩意儿?还要模式的 UML 类图,应用场景、优缺点总结?卧槽,真当写教材呢,心法口诀的教给你了,整那些学究的玩意儿干啥呢!

   不说了,收工!

 

posted on 2020-07-08 08:46  陈本布衣  阅读(650)  评论(2编辑  收藏  举报
****************************************** 页脚Html代码 ******************************************