JAVA设计模式之模板方法模式

一 概述

今天我们要谈的模板方法模式非常好理解,因为它特别贴近于我们的日常生活,顾名思义就是给出一个模板,大家按照模板各自去发挥。常常听一些“牛人”吹嘘:我的成功可以被复制!然后就告诉你第一步怎么干,第二步怎么干,第三步怎么干。。。但是10个人去按照他说的模板去做,却会有10种不同的结果,就像此次要谈论的模板方法模式一样。

1.1 定义

在一个方法中定义一个算法的骨架,而将一些步骤的实现延迟到子类中,使得子类可以在不改变一个算法的结构前提下即可重定义该算法的某些特定步骤。

  • 模板方法模式属于行为型模式
  • 模板方法模式主要是用来定义一套流程下来的固定步骤,而具体的步骤实现则可以是不固定的

即使是这么简单的概念,新手单纯的看它的定义还是不知道在说啥?这又一次印证了理论需要结合实践的那个真理。那接下来我们就让我们在实践中去理解理论。

1.2 使用场景

这个设计模式一般在最初写代码的时候基本上是不会预先想到的,都是在后期不断重构的过程中被提炼出来的。简单来说就是:当你发现你写了两个或者多个几乎一毛一样的类,只是有些方法的实现方式不一致的时候,你就要停下来想想了,如果你又发现在使用这些类的地方调用方法顺序都一样,不要犹豫请使用模板方法模式重构吧。

1.3 UML 类图

在这里插入图片描述
角色说明:

  • AbstractClass(抽象类):定义了一整套算法框架
  • ConcreteClass(具体实现类):具体实现类,根据需要去实现抽象类中的方法

二 实现

直播行业是2017年互联网行业的风口,有熊猫直播、映客直播、陌陌直播、虎牙直播。。。说是全民直播也不为过。

王二狗他们老板也想当风口的猪,就是说它想借着风上天,于是也加入了直播行业大军。小公司不可能自己开发一套直播的基础架构系统,实力不允许,于是就想到了接入第三方的直播 SDK,刚开始接入了腾讯的直播 SDK,后来由于不稳定又要同时接入金山的 SDK,而且要做到可以互相切换。

当王二狗带着小弟林蛋大日夜赶工,一个月后把这个功能实现后发现,你妈这重复代码太多了,需要重构啊,不然老板哪天又要接其他家的 SDK 可咋办,总不能把几乎一毛一样的代码再写一遍吧。

因为用户看直播是有一套固定的模板流程的:登录—进入房间—获取音视频流—观看—停止音视频流—退出房间 虽然两套 SDK 每一个步骤的实现方式不同,但是基本都是遵循同一套流程。王二狗猛然发现,这不就是给模板方法模式准备的场景嘛,于是乎撸起袖子就干了!

模板方法模式 UML 图:
在这里插入图片描述
如上图所示,模板方法模式关键点如下:一个模板类 LivePlay,里面有一个定义为 final 的模板方法 seeLivePlay(),多个模板实现类。

2.1 定义一个模板类 LivePlay

如下代码片段所示,其中的 seeLivePlay() 就是所谓的模板方法,为了不被子类 overwrite,它被设置为 final ,其定义了一个算法骨架。

其中的 login() 是一个实体方法,里面是通用逻辑,所有的子类都是一样的。四个被 abstract 修饰的是抽象方法,这些方法是需要子类去根据自己的实际算法实现的,而 pushVideoStream() 方法有一个默认的空实现,这个一般称为钩子方法,设计用来被其中部分需要的子类 overwrite。

例如腾讯直播 SDK 提供了旁路推流的功能,而金山的没有提供,那么腾讯直播类就可以选择 overwrite 这个钩子方法,提供自己的实现。

public abstract class LivePlay {
    //模板方法
    public final void seeLivePlay() {
        login();
        openRoom();
        startAudioAndVideoStream();
        pushVideoStream();
        stopAudioAndVideoStream();
        closeRoom();
    }

    //实体方法,这个方法实现通用的业务逻辑
    private void login() {
        System.out.println("用户登录");
    }
    
    /*抽象方法*/
    //打开房间
    public abstract void openRoom();
    //打开音视频流
    public abstract void startAudioAndVideoStream();
    //关闭音视频流
    public abstract void stopAudioAndVideoStream();
    //关闭房间
    public abstract void closeRoom();

    /*钩子方法,可以被需要的子类overwrite*/
    //旁路推流,可以通过视频链接在浏览器中查看视频
    public void pushVideoStream() {
    }
}

2.2 创建具体实现类

定义具体的实体类,根据情况 overwrite 相应的抽象方法和钩子方法。

//腾讯直播类
public class TencentLivePlay extends LivePlay  {
    @Override
    public void openRoom() {
        System.out.println("腾讯打开房间");
    }

    @Override
    public void startAudioAndVideoStream() {
        System.out.println("腾讯打开音视频流");
    }

    @Override
    public void stopAudioAndVideoStream() {
        System.out.println("腾讯关闭音视频流");
    }

    @Override
    public void closeRoom() {
        System.out.println("腾讯关闭房间");
    }
    // 覆写钩子方法,提供旁路推流功能
    @Override
    public void pushVideoStream() {
        super.pushVideoStream();
        System.out.println("腾讯进行旁路推流");
    }
}

值得注意的是,由于腾讯 SDK 提供了旁路推流的功能,所以它 overwrite 了 pushVideoStream() 这个钩子方法。

//金山直播类
public class JinShanLivePlay extends LivePlay  {
    @Override
    public void openRoom() {
        System.out.println("金山打开房间");
    }

    @Override
    public void startAudioAndVideoStream() {
        System.out.println("金山打开音视频流");
    }

    @Override
    public void stopAudioAndVideoStream() {
        System.out.println("金山关闭音视频流");
    }

    @Override
    public void closeRoom() {
        System.out.println("金山关闭房间");
    }
}

由于金山 SDK 没有提供了旁路推流的功能,所以它不用覆写 pushVideoStream() 这个钩子方法,而只需要 overwrite 抽象方法即可。

2.3 客户端测试

我们根据后端返回的结果来决定使用哪家的 SDK。

    public static void main(String[] args) {        
        //此处省略若干代码
         ...
        LivePlay tencentLive=new TencentLivePlay();
        tencentLive.seeLivePlay();
        
        System.out.println("");
        
        LivePlay jinShanLive=new JinShanLivePlay();
        jinShanLive.seeLivePlay();
    }

输出结果:

用户登录
腾讯打开房间
腾讯打开音视频流
腾讯进行旁路推流
腾讯关闭音视频流
腾讯关闭房间

用户登录
金山打开房间
金山打开音视频流
金山关闭音视频流
金山关闭房间

从输出可以清楚的看到,要使用哪家的服务就实例化哪家的对象,然后调用开始观看直播即可。

三 总结

3.1 应用场景

  • 一次性实现算法的执行顺序和固定不变部分,可变部分则交由子类来实现
  • 多个子类中拥有相同的行为时,可以将其抽取出来放在父类中,避免重复的代码
  • 使用钩子方法来让子类决定父类的某个步骤是否执行,实现子类对父类的反向控制
  • 控制子类扩展。模板方法只在特定点调用钩子方法,这样就只允许在这些点进行扩展

3.2 优点

  • 提高代码复用性,去除子类中的重复代码
  • 提高扩展性,不同实现细节放到不同子类中,易于增加新行为

3.3 缺点

  • 每个不同的实现都需要定义一个子类,这会导致类的个数的增加,设计更加抽象
  • 调用控制反转:一般情况下,程序的执行流是子类调用父类的方法,模板方法模式使得程序流程变成了父类调用子类方法,这个使得程序比较难以理解和跟踪

四 Android中的源码实例分析

Android 中 View 的 draw 方法就是使用了模板方法模式:

4.1 View 的 draw 方法

public class View {
    // 钩子方法,空实现
    protected void onDraw(Canvas canvas) {
    }
    // 钩子方法,空实现
    protected void dispatchDraw(Canvas canvas) {
    }
    // 绘制方法,定义绘制流程
    public void draw(Canvas canvas) {
       //其他代码略

        /*
         *  绘制流程如下:
         *
         *      1. 绘制view背景
         *      2. 如果有需要,就保存图层
         *      3. 绘制view内容
         *      4. 绘制子View
         *      5. 如果有必要,绘制渐变框和恢复图层
         *      6. 绘制装饰(滑动条等)
         */

        if (!dirtyOpaque) {
            drawBackground(canvas); //步骤1. 绘制 view 背景
        }

        // 如果可能的话跳过第2步和第5步(常见情况)
        final int viewFlags = mViewFlags;
        boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
        boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
        if (!verticalEdges && !horizontalEdges) {
            if (!dirtyOpaque) onDraw(canvas); // 步骤 3. 绘制 view 内容

            dispatchDraw(canvas);// 步骤4. 绘制子 View

            // 覆盖一部分内容,绘制前景
            if (mOverlay != null && !mOverlay.isEmpty()) {
                mOverlay.getOverlayView().dispatchDraw(canvas);
            }

            onDrawForeground(canvas); // 步骤6. 绘制装饰(滑动条等)

            return;
        }
}

4.2 说明

View 的 draw() 方法中定义了一整套的绘制流程,这个流程是固定的,所有的 Android 中的 View 都是按照这个流程来绘制的。

其中 drawBackground() 这个方法在 View 类中是实现了具体过程的,而 onDraw() 方法和 dispatchDraw() 方法在 View 中都是空实现,即都是钩子方法。不同的子类通过重写这些空实现来实现自身不同的绘制效果。

具体的 View,像 TextView 这些单一的 View,就会重写 onDraw() 方法,由于 TextView 没有子 View,所以 dispatchDraw() 还是空实现;而 ViewGroup 类含有子 View,需要遍历子 View 并绘制,因此需要重写 onDraw() 和 dispatchDraw()。

所以,我们自定义 View 时必须且只需重写 onDraw();自定义 ViewGroup 时则需要重写 onDraw() 和 dispatchDraw()。

4.3 其他

另外,像 Activity 的生命周期,AsyncTask 等等也是用到了模板方法模式,也兴趣的可以研究一下。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值