设计模式学习之1—代理模式

不诗意的女程序猿不是好厨师~
转载请注明出处【From 李诗雨---https://blog.csdn.net/cjm2484836553/article/details/99753509

文中源码见github--https://github.com/junmei520/DesignPattern

0.👉先看这里 --- 诗雨的学习说明

本章主要分6个部分来进行学习整理。
1,2,3比较老套主要是讲解了一些基本概念。
4,5直接给出了代理模式的使用模板,并紧接着举了一个简单的使用实例。到这里你基本可以浅析的了解代理模式和它如何使用的。
6则是实战了,在项目中真实的使用场景举例并附源代码。

关于代理模式的使用原理,我想放在下篇进行,因为这篇已经够长的了。。。
希望你可以有耐心看完,也希望能对你有所帮助。

1.从生活中初识代理

①作为一名苦逼的程序猿今天中午又要赶进度,前端新来了一个气的小哥哥,不如趁此机会让他帮我买个饭吧;
②家里房产太空着多浪费,还是请个中介帮我把房子租出去,挣个零花钱;
③离职的时候老板不给我发工资,那必须得请个律师帮忙打官司啊...
虽然说以上情况纯属虚构(huanxiang),但是找帅小哥哥帮忙买饭,找中介帮忙租房,找律师帮忙打官司,这些可都是代理啊。

所以说生活中代理的影子还是很多的,那代码中呢?我们可是“农以码为天”。所以,下面我们就开始代理模式的正式学习吧。

2.代理模式的类图和角色

首先我们要来看下什么是代理模式呢?
其实它就是为其他对象提供一种代理以控制对这个对象的访问

代理模式所涉及到的四个角色:
Subject:抽象类 --- 声明代理对象和被代理对象的共同接口。
RealSubject : 真实主题类(/代理类) --- 该类定义了代理所表示的真是对象。
ProxySubject : 代理类 --- 该类持有一个对真实主题类的引用,从而可以操作真实对象。
Clent :客户类 --- 即使用代理类的客户端。

四者的关系用类图表示如下:

在这里插入图片描述

对类图做一下简单的说明:
为了实现客户端可通过代理对象去间接访问真实对象方法的目的。
①首先要保证真实类和代理类都实现相同的接口,这样真实对象有的方法,代理对象也会有。②其次还要保证代理对象中持有真实对象的引用,这样便可以通过代理对象来操作真实对象。

3.静态代理和动态代理

代理模式分为: 动态代理静态代理 2种。

  • 静态代理 --- 代理类的代码由程序员自己或通过一些自动化工具生成固定的代码再进行百编译,也就是说我们的代码运行前代理类的class编译文件就已存在了。
  • 而动态代理 --- 则与静态代理相反,通过反射机制动态地生成代理者的对象,也就是说code阶段压根就不需要知道代理谁,代理谁我们将再执行阶段决定。Java给我们提供了一个便捷的动态代理接口 InvocationHandler ,实现该接口需要重写其调用方法 invoke.

注意:动态代理的抽象角色只能是接口类,而不能是抽象类。
否则会抛异常:
Caused by: java.lang.IllegalArgumentException: com.kotlinstudy.proxypatterndemo.dynamicproxy.Subject2 is not an interface


在这里插入图片描述

对此的解释是:所谓的动态代理,实际上就是在运行期间根据被代理接口动态生成一个代理类,这个代理类必须是继承自Proxy类。同时,由于Java是单继承的,那么我们只能通过与被代理类实现共同的接口从而实现代理,自然而然也就意味着:被代理类必须实现某个接口,所以抽象角色也必须是接口而不能是抽象类。

4.静态代理 和 动态代理 的 使用模板

4.1 静态代理使用模板

①声明抽象主题角色:

public abstract class Subject {
    /**
     * 一个普通的业务方法
     */
    public abstract void visit();
}

②定义真实角色

public class RealSubject extends Subject{
    @Override
    public void visit() {
        //真实角色中的具体逻辑和实现
        System.out.println("我是静态代理中的 真实角色 的具体方法");
    }
}

③定义代理类

public class ProxySubject extends Subject{
    private RealSubject mSubject;//持有真实对象的引用

    public ProxySubject(RealSubject mSubject){
        this.mSubject=mSubject;
    }

    @Override
    public void visit(){
        //在真实对象方法前 添加一些操作
        System.out.println("添加一些 之前操作");

        //通过真实角色对象的引用 调用真实角色中的逻辑方法
        mSubject.visit();

        //在真实对象方法后 添加一些操作
        System.out.println("添加一些 之前操作");

    }
}

④MainActivity作为client类进行测试

//测试静态代理的使用
        //构造一个真实角色
        RealSubject real=new RealSubject();
        //通过真实对象构造一个代理对象
        ProxySubject proxy=new ProxySubject(real);
        //调用代理的相关方法
        proxy.visit();

4.2 动态代理使用模板

动态代理的使用和静态代理十分类似,不同的就在于动态代理的代理类,需要实现 InvocationHandler这个接口,并且需要重写invoke()方法。我们也来走一遍。为了区分我们把相应的类后面加个2.

①创建抽象角色(只能是接口类)


public interface Subject2 {
    /**
     * 一个普通的业务方法
     */
     void visit();
}

②定义真实角色

public class RealSubject2 implements Subject2 {
    @Override
    public void visit() {
        //真实角色中的具体逻辑和实现
        System.out.println("我是动态代理中的 真实角色中的方法");
    }
}

③实现 InvocationHandler这个接口

public class MyInvocationHandler implements InvocationHandler {
    private Object obj;//真实角色的引用

    public MyInvocationHandler(Object obj){
        this.obj=obj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //调用被代理对象的方法
        Object result=method.invoke(obj,args);
        return result;
    }
}

④MainActivity作为client类进行测试

        //测试动态代理的使用
        RealSubject2 real2 = new RealSubject2();
        InvocationHandler invocationHandler = new MyInvocationHandler(real2);
        Subject2 dynamicProxy = (Subject2) Proxy.newProxyInstance(RealSubject2.class.getClassLoader(), new Class[]{Subject2.class}, invocationHandler);
        dynamicProxy.visit();

5.先从一个人尽皆知的栗子讲起

虽说有些例子,在项目中根本用不到,但是很多书中却又不得不提,我想主要还是它有助于我们去理解一个概念吧,虽然不实用,但是可以抛砖引玉,或许在以后的某个时候我们可以把它变变,它就价值翻倍了。

那下面,我就以《设计模式》中的一个人尽皆知的栗子讲起吧。这里我们先结合实景理解一下代理模式,之后我们再讲项目中的实际例子,这样循序渐进或许会好点。

事情是这样的:小民遇到了被老板拖欠工资的情况,他决定通过法律途径来解决问题,于是他请了一个律师来作为自己的诉讼代理人。

下面我们先使用代理模式来模拟这一事件。

①首先将诉讼的流程抽象再一个接口类中,创建一个诉讼接口类

/**
 * Created by ChenJunMei on 2019/8/23.
 * 诉讼接口类
 */

public interface ILawsuit {
    //提交申请
    void submit();

    //进行举证
    void burden();

    //开始辩护
    void defend();

    //诉讼完成
    void finish();
}

②创建具体诉讼人---小民 实现ILawsuit接口,并给出具体的实现逻辑

/**
 * Created by ChenJunMei on 2019/8/23.
 * 具体诉讼人---小民
 */

public class XiaoMin implements ILawsuit {
    @Override
    public void submit() {
        //老板欠小民工资,小民只好申请仲裁
        System.out.println("老板拖欠工资,特此申请仲裁!");
    }

    @Override
    public void burden() {
        //小民证据充足,不怕告不赢
        System.out.println("这是合同书和过去一年的银行工资流水!");
    }

    @Override
    public void defend() {
        //铁证如山,没什么好说的
        System.out.println("证据确凿,我没必要多说什么了!");
    }

    @Override
    public void finish() {
        //结果肯定是必赢
        System.out.println("诉讼成功!判决老板七天内结算工资");
    }
}

③创建代理律师类,该类持有一个被代理者(小民)的引用,律师所执行的方法就是简单的调用被代理者中的方法。

/**
 * Created by ChenJunMei on 2019/8/23.
 * 代理律师类
 */

public class Lawyer implements ILawsuit{
    //持有一个具体被代理者的引用
    private ILawsuit mLawsuit;
    
    public Lawyer(ILawsuit lawsuit){
        mLawsuit=lawsuit;
    }
    
    @Override
    public void submit() {
        mLawsuit.submit();
    }

    @Override
    public void burden() {
        mLawsuit.burden();
    }

    @Override
    public void defend() {
        mLawsuit.defend();
    }

    @Override
    public void finish() {
        mLawsuit.finish();
    }
}

④在MainActivity中进行测试

 //测试小民诉讼
        ILawsuit xiaomin=new XiaoMin();//构造一个小民
        ILawsuit lawyer=new Lawyer(xiaomin);//构造一个代理律师并将小民作为构造参数传递进去
        lawyer.submit();//律师提交诉讼
        lawyer.burden();//律师进行举证
        lawyer.defend();//律师代替小民进行辩护
        lawyer.finish();//完成诉讼

当然,一个律师可以代理多个人,所以除了小民,还可以小辉。我们只需要再定义一个XiaoHui类实现ILawsuit即可,再在客户端类中修改高层模块调用逻辑就OK了。

好,到这里,一个模拟诉讼的栗子就完成了。

我们再使用动态代理来实现一下:
要使用动态代理,诉讼接口类和被代理类XiaoMin都不需要变,我们只需要对代理律师类做下改动就好了。我们需要创建一个类实现InvocationHandler类并重写invoke()方法,我们主要通过invoke()方法来调用具体的被代理方法。

/**
 * Created by ChenJunMei on 2019/8/23.
 * 动态代理 创建LawyerInvocationHandler 实现 InvocationHander接口
 */

public class LawyerInvocationHandler implements InvocationHandler {
    private Object realObj;

    public LawyerInvocationHandler(Object realObj){
        this.realObj=realObj;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //调用被代理对象的方法
        Object retObj=method.invoke(realObj,args);
        return retObj;
    }
}

在MainActivity中测试一下动态实现代理诉讼:

        //测试动态代理方式 小民诉讼
        ILawsuit xiaomin2=new XiaoMin();//构造一个小民
        LawyerInvocationHandler lawyerInvocationHandler=new LawyerInvocationHandler(xiaomin2);//创建一个InvocationHandler
        ClassLoader loader=xiaomin2.getClass().getClassLoader();//获取被代理类xiaomin2的ClassLoader
        //动态构造一个代理律师
        ILawsuit dynamicLawyer= (ILawsuit) Proxy.newProxyInstance(loader,new Class[]{ILawsuit.class},lawyerInvocationHandler);
        dynamicLawyer.submit();
        dynamicLawyer.burden();
        dynamicLawyer.defend();
        dynamicLawyer.finish();

好了经过这个栗子是不是对代理模式有了一点感觉。下面就让我们来看看这几模式在真实项目中使用的栗子。

6.实际项目中代理模式的使用举例

实例①场景描述:

购物车中的商品选中就删除的功能。
如果只是一个干干净净的删除那怎么写都可以,但是在新版本的时候老板要求在删除前要先做一些其他操作,删除成功后又要做其他操作你怎么决解。

这样的话我们就不得不为以后做考虑了,如果后期还要对删除前后增加其他要求怎么办。每次都直接改删除中的代码的话就有点low了,要是每次我们只调用xxx.delete()方法,至于删除前和删除后的操作都在里面封装好就太好了。
要达到这种目的,代理模式是我们的一个很好的选择。

下面我先用静态代理来实现,再用动态代理。之后我再拿其他两种不妥当的方法来做下对比,相信优劣立马就可以体现出来。

使用静态代理的代码实现:
①创建商品接口 抽象类

public interface IGoods {
    void delete();
}

②创建商品真实类

public class RealGoods implements IGoods{

    @Override
    public void delete() {
        System.out.println("请求后台删除操作...");
    }
}

③创建代理类

public class GoodsProxy implements IGoods {
    private IGoods iGoods;

    public GoodsProxy(IGoods iGoods) {
        this.iGoods = iGoods;
    }

    @Override
    public void delete() {
        //在这增加做删除前的一些操作
        System.out.println("删除前的一些操作...");
        iGoods.delete();
        //在这增加删除后的一些操作
        System.out.println("删除后的一些操作...");
    }
}

④在客户类(MainActivity)中进行测试使用
只需要一句delete()调用, 既不影响之前的商品删除,也增加了可扩展性。

GoodsProxy goodsProxy = new GoodsProxy(new RealGoods());
        goodsProxy.delete();

静态代理模式需要为每一个需要代理的类写一个代理类,如果需要代理的类有N个,那就要写N个代理类,这就有点让人无法忍受了。所以我们考虑用动态代理来实现,这样后期的灵活性更大些

使用动态代理的代码实现:
①创建商品接口 抽象类
②创建商品真实类
1和2两步都和静态代理的一模一样,这里主要改变的是第三步

③静态代理实现的是我们自己定义的接口,而这里动态代理实现的是JDK提供的接口InvocationHandler,并需要重写invoke()方法

public class GoodsInvocationHandler implements InvocationHandler {
    private IGoods  mIGoods;

    public GoodsInvocationHandler(IGoods mIGoods) {
        this.mIGoods = mIGoods;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //删除前的一些操作
        System.out.println("动态代理方式:删除前的操作...");
        Object invoke = method.invoke(mIGoods, args);
        //删除后的一些操作
        System.out.println("动态代理方式:删除后的操作...");
        return invoke;
    }
}

④使用与测试

        RealGoods goods = new RealGoods();//构造一个商品真实类
        GoodsInvocationHandler goodsInvocationHandler = new GoodsInvocationHandler(goods);//构造一个InvocationHandler
        IGoods goodsDynacmicProxy = (IGoods) Proxy.newProxyInstance(goods.getClass().getClassLoader(), goods.getClass().getInterfaces(), goodsInvocationHandler);//动态生成一个代理类
        goodsDynacmicProxy.delete();//通过动态代理对象调用删除方法

两种不好的方法做优劣对比(不使用代理模式的情况下):
不好的方法一:直接修改删除逻辑代码,在其内部直接添加删除前和删除后的操作

public class RealGoods implements IGoods{

    @Override
    public void delete() {
        //直接添加删除前的操作
        System.out.println("直接添加删除前操作");
        System.out.println("请求后台删除操作...");
        //直接添加删除后的操作
        System.out.println("直接添加删除后操作");
    }
}

违反了开闭原则,直接修改内部代码,代码的可扩展性也极差。

不好的方法二
再增加一个接口,接口中添加了删除前操作和删除后操作。
在使用的

xxx.delete();

地方增加两个行代码:

xxx.beforeDelete();   //删除前的操作
xxx.delele();
xxx.afterDelete();    //删除后的操作

这在一定程度上增加了可扩展性,但是如果项目中使用到删除的地方很多,那每个地方都要增加两个方法,也实在是令人无法忍受啊。

所以,通过不好的进行对比。我们才能更加真切的体会到使用代理默模式来解决这种问题的好处。

实例场景②描述

自己的项目中嵌入了aar包,发现自己的工程和aar中都引用了系统的同一个静态成员变量,不管任何一方从新设置这个变量,另一方的该变量都会被覆盖 的问题。

举个具体的栗子:
开发一个aar包嵌入到客户的项目中。采用Thread的UncaughtExceptionHandler来记录崩溃日志,但是客户那边也是用的这个方法。而Thread中的defaultUncaughtExceptionHandler是一个静态的成员变量,如果有人已经设置了对象进去,我们再设置,就会覆盖了人家的对象。反之,我们自己的就被覆盖了。如果按照这种思路,那肯定就只有最后一个调用Thread.setUncaughtExceptionHandler的人才能拿到错误的日志信息了。
而要解决这个问题,使用代理是一个很好的解决方法。

思路是:
先取出Thread类中已有的异常处理对象defaultHandler ,拿到被代理的对象。
然后用拿到的被代理对象创建一个实现InvocationHandler 接口的实现InVocationHandler接口 ,在invoke(Object proxy, Method method, Object[] args)方法中,加上自己的操作,并调用 method.invoke(//被代理的对象, args)。
最后利用defaultHandler 和实现的InVocation 类对象,创建代理类对象并替换原有对象(被代理对象)。

具体代码实现:

Thread.UncaughtExceptionHandler defaultHandler = Thread.getDefaultUncaughtExceptionHandler();
 class InVocation implements InvocationHandler {

        Thread.UncaughtExceptionHandler defaultHandler;

        public InVocation(Thread.UncaughtExceptionHandler defaultHandler) {
            this.defaultHandler = defaultHandler;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Throwable e;
            Thread t;
            if (args != null && args.length > 1) {
                t = (Thread) args[0];
                e = (Throwable) args[1];
            }
            /**
             * 根据 t和e将错误信息写到文件中
             * ...
             */

            //将信息传递给原来的
            Object o = method.invoke(this.defaultHandler, args);
            return o;
        }
    }
Thread.UncaughtExceptionHandler uncaughtExceptionHandler = (Thread.UncaughtExceptionHandler) Proxy.newProxyInstance(defaultHandler.getClass().getClassLoader(), defaultHandler.getClass().getInterfaces(), new InVocation(defaultHandler));
Thread.setDefaultUncaughtExceptionHandler(uncaughtExceptionHandler);

积累点滴,做好自己~

参考资料

《Android源码设计模式解析与实战》第18章
《Android插件化开发指南》第四章
知乎问答:https://zhuanlan.zhihu.com/p/41110998

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容