Android:优雅的处理首页弹框逻辑:责任链模式

背景

随着业务的发展,首页的弹窗越来越多,隐私政策弹窗,广告弹窗,好评弹窗,应用内更新弹窗等等。 并且弹框显示还有要求,比如:

  • 用户本次使用app,只能显示一个弹框,毕竟谁都不愿意打开app就看到一堆弹框
  • 这些弹框有优先级:如隐私政策弹窗优先级肯定比好评弹窗高,所以希望优先级高的优先显示
  • 广告弹框只展示一次
  • 等等

如何优雅的处理这个逻辑呢?请出我们的主角:责任链模式。

责任链模式

举个栗子🌰

一位男性在结婚之前有事要和父母请示,结婚之后要请示妻子,老了之后就要和孩子们商量。作为决策者的父母、妻子或孩子,只有两种选择:要不承担起责任来,允许或不允许相应的请求; 要不就让他请示下一个人,下面来看如何通过程序来实现整个流程。

先看一下类图:

类图非常简单,IHandler上三个决策对象的接口。

java 复制代码
//决策对象的接口
public interface IHandler {
    //处理请求
    void HandleMessage(IMan man);
}
java 复制代码
//决策对象:父母
public class Parent implements IHandler {
    @Override
    public void HandleMessage(IMan man) {
        System.out.println("孩子向父母的请求是:" + man.getRequest());
        System.out.println("父母的回答是:同意");
    }
}
java 复制代码
//决策对象:妻子
public class Wife implements IHandler {
    @Override
    public void HandleMessage(IMan man) {
        System.out.println("丈夫向妻子的请求是:" + man.getRequest());
        System.out.println("妻子的回答是:同意");
    }
}
java 复制代码
//决策对象:孩子
public class Children implements IHandler{
    @Override
    public void HandleMessage(IMan man) {
        System.out.println("父亲向孩子的请求是:" + man.getRequest());
        System.out.println("孩子的回答是:同意");
    }
}

IMan上男性的接口:

java 复制代码
public interface IMan {
    int getType(); //获取个人状况
    String getRequest(); //获取个人请示(这里就简单的用String)
}
java 复制代码
//具体男性对象
public class Man implements IMan {
    /**
     * 通过一个int类型去描述男性的个人状况
     * 0--幼年
     * 1--成年
     * 2--年迈
     */
    private int mType = 0;
    //请求
    private String mRequest = "";

    public Man(int type, String request) {
        this.mType = type;
        this.mRequest = request;
    }

    @Override
    public int getType() {
        return mType;
    }

    @Override
    public String getRequest() {
        return mRequest;
    }
}

最后我们看下一下场景类:

java 复制代码
public class Client {
    public static void main(String[] args) {
        //随机生成几个man
        Random random = new Random();
        ArrayList<IMan> manList = new ArrayList<>();
        for (int i = 0; i < 5; i++) {
            manList.add(new Man(random.nextInt(3), "5块零花钱"));
        }
        //定义三个请示对象
        IHandler parent = new Parent();
        IHandler wife = new Wife();
        IHandler children = new Children();
        //处理请求
        for (IMan man: manList) {
            switch (man.getType()) {
                case 0:
                    System.out.println("--------孩子向父母发起请求-------");
                    parent.HandleMessage(man);
                    break;
                case 1:
                    System.out.println("--------丈夫向妻子发起请求-------");
                    wife.HandleMessage(man);
                    break;
                case 2:
                    System.out.println("--------父亲向孩子发起请求-------");
                    children.HandleMessage(man);
                    break;
                default:
                    break;
            }
        }
    }
}

首先是通过随机方法产生了5个男性的对象,然后看他们是如何就要5块零花钱这件事去请示的,运行结果如下所示:

java 复制代码
--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意
--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意
--------父亲向孩子发起请求-------
父亲向孩子的请求是:5块零花钱
孩子的回答是:同意
--------孩子向父母发起请求-------
孩子向父母的请求是:5块零花钱
父母的回答是:同意
--------丈夫向妻子发起请求-------
丈夫向妻子的请求是:5块零花钱
妻子的回答是:同意

发没发现上述的代码是不是有点不舒服,有点别扭,有点想重构它的感觉?那就对了!这段代码有以下几个问题:

  • 职责界定不清晰

对孩子提出的请示,应该在父母类中做出决定,父母有责任、有义务处理孩子的请示,

因此Parent类应该是知道孩子的请求自己处理,而不是在Client类中进行组装出来, 也就是说 原本应该是父亲这个类做的事情抛给了其他类进行处理,不应该是这样的。

  • 代码臃肿

我们在Client类中写了if...else的判断条件,而且能随着能处理该类型的请示人员越多,

if...else的判断就越多,想想看,臃肿的条件判断还怎么有可读性?!

  • 耦合过重

这是什么意思呢,我们要根据Man的type来决定使用IHandler的那个实现类来处理请

求。有一个问题是:如果IHandler的实现类继续扩展怎么办?修改Client类? 与开闭原则违背了!【开闭原则:软件实体如类,模块和函数应该对扩展开放,对修改关闭】
www.jianshu.com/p/05196fac1...

  • 异常情况欠考虑

丈夫只能向妻子请示吗?丈夫向自己的父母请示了,父母应该做何处理?

我们的程序上可没有体现出来,逻辑失败了!

既然有这么多的问题,那我们要想办法来解决这些问题,我们先来分析一下需求,男性提出一个请示,必然要获得一个答复,甭管是同意还是不同意,总之是要一个答复的,而且这个答复是唯一的,不能说是父母作出一个决断,而妻子也作出了一个决断,也即是请示传递出去,必然有一个唯一的处理人给出唯一的答复,OK,分析完毕,收工,重新设计,我们可以抽象成这样一个结构,男性的请求先发送到父亲,父母一看是自己要处理的,就作出回应处理,如果男性已经结婚了,那就要把这个请求转发到妻子来处理,如果男性已经年迈,那就由孩子来处理这个请求,类似于如图所示的顺序处理图。

父母、妻子、孩子每个节点有两个选择:要么承担责任,做出回应;要么把请求转发到后序环节。结构分析得已经很清楚了,那我们看怎么来实现这个功能,类图重新修正,如图 :

从类图上看,三个实现类Parent、Wife、Children只要实现构造函数和父类中的抽象方法 response就可以了,具体由谁处理男性提出的请求,都已经转移到了Handler抽象类中,我们 来看Handler怎么实现,

java 复制代码
public abstract class Handler {
    //处理级别
    public static final int PARENT_LEVEL_REQUEST = 0; //父母级别
    public static final int WIFE_LEVEL_REQUEST = 1;	//妻子级别
    public static final int CHILDREN_LEVEL_REQUEST = 2;//孩子级别

    private Handler mNextHandler;//下一个责任人

    protected abstract int getHandleLevel();//具体责任人的处理级别

    protected abstract void response(IMan man);//具体责任人给出的回应

    public final void HandleMessage(IMan man) {
        if (man.getType() == getHandleLevel()) {
            response(man);//当前责任人可以处理
        } else {
            //当前责任人不能处理,如果有后续处理人,将请求往后传递
            if (mNextHandler != null) {
                mNextHandler.HandleMessage(man);
            } else {
                System.out.println("-----没有人可以请示了,不同意该请求-----");
            }
        }
    }

    public void setNext(Handler next) {
        this.mNextHandler = next;
    }
}

再看一下具体责任人的实现:Parent、Wife、Children

java 复制代码
public class Parent extends Handler{

    @Override
    protected int getHandleLevel() {
        return Handler.PARENT_LEVEL_REQUEST;
    }

    @Override
    protected void response(IMan man) {
        System.out.println("----------孩子向父母提出请示----------");
        System.out.println(man.getRequest());
        System.out.println("父母的回答是:同意");
    }
}
java 复制代码
public class Wife extends Handler{
    @Override
    protected int getHandleLevel() {
        return Handler.WIFE_LEVEL_REQUEST;
    }

    @Override
    protected void response(IMan man) {
        System.out.println("----------丈夫向妻子提出请示----------");
        System.out.println(man.getRequest());
        System.out.println("妻子的回答是:同意");
    }
}
java 复制代码
public class Children extends Handler{
    @Override
    protected int getHandleLevel() {
        return Handler.CHILDREN_LEVEL_REQUEST;
    }

    @Override
    protected void response(IMan man) {
        System.out.println("----------父亲向孩子提出请示----------");
        System.out.println(man.getRequest());
        System.out.println("孩子的回答是:同意");
    }
}

那么再看一下场景复现:

在Client中设置请求的传递顺序,先向父母请示,不是父母应该解决的问题,则由父母传递到妻子类解决,若不是妻子类解决的问题则传递到孩子类解决,最终的结果必然有一个返回,其运行结果如下所示。

java 复制代码
----------孩子向父母提出请示----------
15块零花钱
父母的回答是:同意
----------丈夫向妻子提出请示----------
15块零花钱
妻子的回答是:同意
----------父亲向孩子提出请示----------
15块零花钱
孩子的回答是:同意
----------丈夫向妻子提出请示----------
15块零花钱
妻子的回答是:同意
----------父亲向孩子提出请示----------
15块零花钱
孩子的回答是:同意

结果也正确,业务调用类Client也不用去做判断到底是需要谁去处理,而且Handler抽象类的子类可以继续增加下去,只需要扩展传递链而已,调用类可以不用了解变化过程,甚至是谁在处理这个请求都不用知道。在这种模式就是责任链模式

定义

Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request.Chain the receiving objects and pass the request along the chain until an object handles it.

(使多个对象都有机会处理请求,从而避免了请求的发送者和接受者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有对象处理它为止。) 责任链模式的重点是在"链"上,由一条链去处理相似的请求在链中决定谁来处理这个请 求,并返回相应的结果,其通用类图如图所示

最后总结一下,责任链的模版:

包含四个对象,Handler,Request,Level,Response:

java 复制代码
public class Request {
    //请求的等级
    public Level getRequestLevel(){
        return null;
    }
}
java 复制代码
public class Level {
    //请求级别
}
java 复制代码
public class Response {
   //处理者返回的数据
}
java 复制代码
//抽象处理者
public abstract class Handler {
    private Handler mNextHandler;

    //每个处理者都必须对请求做出处理
    public final Response handleMessage(Request request) {
        Response response = null;
        if (getHandlerLevel().equals(request.getRequestLevel())) {
            //是自己处理的级别,自己处理
            response = echo(request);
        } else {
            //不是自己处理的级别,交给下一个处理者
            if (mNextHandler != null) {
                response = mNextHandler.echo(request);
            } else {
                //没有处理者能处理,业务自行处理
            }
        }
        return response;
    }

    public void setNext(Handler next) {
        this.mNextHandler = next;
    }

    @NotNull
    protected abstract Level getHandlerLevel();

    protected abstract Response echo(Request request);
}

实际应用

我们回到开篇的问题:如何设计弹框的责任链?

kt 复制代码
//抽象处理者
abstract class AbsDialog(private val context: Context) {
    private var nextDialog: AbsDialog? = null
    
    //优先级
    abstract fun getPriority(): Int

    //是否需要展示
    abstract fun needShownDialog(): Boolean

    fun setNextDialog(dialog: AbsDialog?) {
        nextDialog = dialog
    }

    open fun showDialog() {
        //这里的逻辑,我们就简单点,具体逻辑根据业务而定
        if (needShownDialog()) {
            show()
        } else {
            nextDialog?.showDialog()
        }
    }

    protected abstract fun show()
    
    // Sp存储, 记录是否已经展示过
    open fun needShow(key: String): Boolean {
        val sp: SharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
        return sp.getBoolean(key, true)
    }

    open fun setShown(key: String, show: Boolean) {
        val sp: SharedPreferences = context.getSharedPreferences(SP_NAME, Context.MODE_PRIVATE)
        sp.edit().putBoolean(key, !show).apply()
    }

    companion object {
        const val LOG_TAG = "Dialog"
        const val SP_NAME = "dialog"
        const val POLICY_DIALOG_KEY = "policy_dialog"
        const val AD_DIALOG_KEY = "ad_dialog"
        const val PRAISE_DIALOG_KEY = "praise_dialog"
    }
}
kt 复制代码
/**
 * 模拟 隐私政策弹窗
 * */
class PolicyDialog(context: Context) : AbsDialog(context) {
    override fun getPriority(): Int = 0

    override fun needShownDialog(): Boolean {
        // 这里可以根据业务逻辑判断是否需要显示弹窗,如接口控制等等
        // 这里通过Sp存储来模拟
        return needShow(POLICY_DIALOG_KEY)
    }

    override fun show() {
        Log.d(LOG_TAG, "显示隐私政策弹窗")
        setShown(POLICY_DIALOG_KEY, true) //记录已经显示过
    }
}
kt 复制代码
/**
 * 模拟 广告弹窗
 * */
class AdDialog(private val context: Context) : AbsDialog(context) {
    private val ad = DialogData(1, "XX广告弹窗") // 模拟广告数据

    override fun getPriority(): Int = 1

    override fun needShownDialog(): Boolean {
        // 广告数据通过接口获取,广告id应该是唯一的,所以根据id保持sp
        return needShow(AD_DIALOG_KEY + ad.id)
    }

    override fun show() {
        Log.d(LOG_TAG, "显示广告弹窗:${ad.name}")
        setShown(AD_DIALOG_KEY + ad.id, true)
    }
}
kt 复制代码
/**
 * 模拟 好评弹窗
 * */
class PraiseDialog(context: Context) : AbsDialog(context) {
    override fun getPriority(): Int = 2

    override fun needShownDialog(): Boolean {
        // 这里可以根据业务逻辑判断是否需要显示弹窗,如用户使用7天等
        // 这里通过Sp存储来模拟
        return needShow(PRAISE_DIALOG_KEY)
    }

    override fun show() {
        Log.d(LOG_TAG, "显示好评弹窗")
        setShown(PRAISE_DIALOG_KEY, true)
    }
}
kt 复制代码
//模拟打开app
val dialogs = mutableListOf<AbsDialog>()
dialogs.add(PolicyDialog(this))
dialogs.add(PraiseDialog(this))
dialogs.add(AdDialog(this))
//根据优先级排序
dialogs.sortBy { it.getPriority() }
//创建链条
for (i in 0 until dialogs.size - 1) {
    dialogs[i].setNextDialog(dialogs[i + 1])
}
dialogs[0].showDialog()

第一次打开

第二次打开

第三次打开

总结:

  • 优点

责任链模式非常显著的优点是将请求和处理分开。请求者可以不用知道是谁处理的,处理者可以不用知道请求的全貌,两者解耦,提高系统的灵活性。

  • 缺点

责任链有两个非常显著的缺点:一是性能问题,每个请求都是从链头遍历到链尾,特别是在链比较长的时候,性能是一个非常大的问题。二是调试不很方便,特别是链条比较长, 环节比较多的时候,由于采用了类似递归的方式,调试的时候逻辑可能比较复杂。

  • 注意事项

链中节点数量需要控制,避免出现超长链的情况,一般的做法是在Handler中设置一个最大节点数量,在setNext方法中判断是否已经是超过其阈值,超过则不允许该链建立,避免无意识地破坏系统性能。

相关推荐
青草地溪水旁43 分钟前
设计模式(C++)详解——建造者模式(1)
c++·设计模式·建造者模式
念何架构之路7 小时前
Go语言设计模式(七)组合模式
设计模式·组合模式
sun0077009 小时前
android ndk编译valgrind
android
AI视觉网奇10 小时前
android studio 断点无效
android·ide·android studio
jiaxi的天空10 小时前
android studio gradle 访问不了
android·ide·android studio
No Silver Bullet11 小时前
android组包时会把从maven私服获取的包下载到本地吗
android
catchadmin11 小时前
PHP serialize 序列化完全指南
android·开发语言·php
tangweiguo0305198712 小时前
Kable使用指南:Android BLE开发的现代化解决方案
android·kotlin
00后程序员张15 小时前
iOS App 混淆与资源保护:iOS配置文件加密、ipa文件安全、代码与多媒体资源防护全流程指南
android·安全·ios·小程序·uni-app·cocoa·iphone
易元15 小时前
模式组合应用-外观模式
后端·设计模式