21

「补课」进行时:设计模式(5)——从 LOL 中学习代理模式

 3 years ago
source link: http://www.cnblogs.com/babycomeon/p/13905303.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

RFnmQv7.jpg!mobile

1. 前文汇总

「补课」进行时:设计模式系列

2. 从 LOL 中学习代理模式

我是一个很喜欢玩游戏的人,虽然平时玩游戏的时间并不多,但我也是一个忠实的 LOL 的爱好者,就是段位有点惨不忍睹,常年倔强的黑铁,今年 S10 的总决赛在上海举行,这个事儿我从 S9 就开始期待,结果门票今年没卖,直接是抽签拼人品。

360w+ 人抽 3600+ 人,这个概率属实有点低,只能找个地方和我的小伙伴一起看了。

myURzmR.jpg!mobile

打 LOL 最开心的事情莫过于拿到 PentaKill 和 victory ,把这件事情使用代码表现出来,首先定义一个玩游戏的人的接口:

public interface ILOLPlayer {
    // 登录使用用户名和密码
    void login(String name, String password);
    // 拿到五杀
    void pentaKill();
    // 游戏胜利
    void victory();
}

第二步对上面的接口做一个实现:

public class LOLPlayer implements ILOLPlayer {

    private String name = "";

    public LOLPlayer(String name) {
        this.name = name;
    }

    @Override
    public void login(String name, String password) {
        System.out.println("登录游戏:name:" + name + ", password:" + password);
    }

    @Override
    public void pentaKill() {
        System.out.println(this.name + " 拿到五杀啦!!!");
    }

    @Override
    public void victory() {
        System.out.println(this.name + " 游戏胜利啦!!!");
    }
}

最后我们写一个最简单的测试类:

public class Test {
    public static void main(String[] args) {
        LOLPlayer lolPlayer = new LOLPlayer("geekdigging");
        lolPlayer.login("geekdigging", "password");
        lolPlayer.pentaKill();
        lolPlayer.victory();
    }
}

运行结果:

登录游戏:name:geekdigging, password:password
geekdigging 拿到五杀啦!!!
geekdigging 游戏胜利啦!!!

在打游戏的过程中,大家都知道有一个类型叫做排位赛,排位赛能到多少段位,一个是看时间,一个是看天赋,基本上打到一定的段位就很难再往上走了,如果说这时候还想升段位,那就只能取找代练帮忙做代打了。

我们找一位代练帮我们继续打游戏:

public class LOLPlayerProxy implements ILOLPlayer {

    private ILOLPlayer ilolPlayer;

    public LOLPlayerProxy(LOLPlayer playerLayer) {
        this.ilolPlayer = playerLayer;
    }

    @Override
    public void login(String name, String password) {
        this.ilolPlayer.login(name, password);
    }

    @Override
    public void pentaKill() {
        this.ilolPlayer.pentaKill();
    }

    @Override
    public void victory() {
        this.ilolPlayer.victory();
    }
}

我们稍微修改一下测试类:

public class Test {
    public static void main(String[] args) {
        LOLPlayer lolPlayer = new LOLPlayer("geekdigging");
        LOLPlayerProxy proxy = new LOLPlayerProxy(lolPlayer);
        proxy.login("geekdigging", "password");
        proxy.pentaKill();
        proxy.victory();
    }
}

这个测试类里面,我们没有自己打游戏,而是使用代练 proxy 来帮我们打游戏,最后的结果是:

登录游戏:name:geekdigging, password:password
geekdigging 拿到五杀啦!!!
geekdigging 游戏胜利啦!!!

这就是代理模式,本来需要自己做事情,使用代理以后,就可以由代理帮我们做事情了。

3. 代理模式定义

代理模式(Proxy Pattern)是一个使用率非常高的模式,其定义如下:

Provide a surrogate or placeholder for another object to control access toit.(为其他对象提供一种代理以控制对这个对象的访问。)

r2ima27.png!mobile

  • Subject: 抽象主题角色。
  • RealSubject: 具体主题角色。
  • Proxy: 代理主题角色。

通用示例代码如下:

// 抽象主题类,定义一个方法
public interface Subject {
    void request();
}

// 具体主题类,在这里写具体的处理逻辑
public class RealSubject implements Subject {
    @Override
    public void request() {
        // 逻辑处理
    }
}

// 代理类
public class Proxy implements Subject {

    private Subject subject;

    public Proxy() {
        this.subject = new Proxy();
    }

    public Proxy(RealSubject subject) {
        this.subject = subject;
    }

    @Override
    public void request() {
        this.before();
        this.subject.request();
        this.after();
    }

    private void before() {
        // 逻辑预处理
    }

    private void after() {
        // 逻辑善后处理
    }
}

在最后的这个代理类中,通过构造函数来进行代理角色的传递,同时还可以在具体的处理逻辑上构造一个切面,定义预处理逻辑以及善后处理逻辑。

4. 代理模式的优点

  1. 职责清晰:真实的角色是用来实现具体业务逻辑的,无需关心其他工作,可以后期通过代理的方式来完成其他的工作。
  2. 高扩展性:
  3. 智能化:

5. 普通代理

首先说普通代理,它的要求就是客户端只能访问代理角色,而不能访问真实角色,这是比较简单的。

使用上面最开始的打 LOL 进行改造,我自己作为一个游戏玩家,我肯定自己不练级了,也就是场景类不能再直接 new 一个 LOLPlayer 对象了,它必须由 LOLPlayerProxy 来进行模拟场景。

首先是对 LOLPlayer 类进行改造,把 LOLPlayer 这个类的构造方法修改,使他不能直接 new 一个对象出来。

public class LOLPlayer implements ILOLPlayer {

    private String name;

    public LOLPlayer(ILOLPlayer ilolPlayer, String name) throws Exception {
        if (ilolPlayer == null) {
            throw new Exception("不能创建真实的角色");
        } else {
            this.name = name;
        }
    }
    // 省略剩余的代码
}

接下来是代理类:

public class LOLPlayerProxy implements ILOLPlayer {

    private ILOLPlayer iloLPlayer;

    public LOLPlayerProxy(String name) {
        try {
            iloLPlayer = new LOLPlayer(this, name);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 省略剩余的代码
}

代理类也是仅修改了构造函数,通过传进来的一个代理者的名称,就能进行代理,在这种改造下,系统更加简洁了,调用者只知道代理存在就可以,不用知道代理了谁。

最后的测试类也需要进行修改:

public class Test {
    public static void main(String[] args) {
        ILOLPlayer proxy = new LOLPlayerProxy("geekdigging");
        proxy.login("geekdigging", "password");
        proxy.pentaKill();
        proxy.victory();
    }
}

在这个代理类上,我没有再去 new 一个 LOLPlayer 的对象,即可对 LOLPlayer 进行代理。

7. 强制代理

强制代理实际上一个普通代理模式的变种,普通代理是通过代理找到真实的角色,但是强制代理却是要「强制」,必须通过真实角色查找到代理角色,否则将不能访问。

首先是对接口类加一个 getProxy() 方法,指定要访问自己必须通过哪个代理。

public interface ILOLPlayer {
    // 登录使用用户名和密码
    void login(String name, String password);
    // 拿到五杀
    void pentaKill();
    // 游戏胜利
    void victory();
    // 获取自己的代理类
    ILOLPlayer getProxy();
}

然后再是对具体实现类的改造:

public class LOLPlayer implements ILOLPlayer {

    private String name;

    private ILOLPlayer proxy;

    public LOLPlayer(String name) {
        this.name = name;
    }

    @Override
    public void login(String name, String password) {
        if (this.isProxy()) {
            System.out.println("登录游戏:name:" + name + ", password:" + password);
        } else {
            System.out.println("请使用指定的代理");
        }

    }

    @Override
    public void pentaKill() {
        if (this.isProxy()) {
            System.out.println(this.name + " 拿到五杀啦!!!");
        } else {
            System.out.println("请使用指定的代理");
        }
    }

    @Override
    public void victory() {
        if (this.isProxy()) {
            System.out.println(this.name + " 游戏胜利啦!!!");
        } else {
            System.out.println("请使用指定的代理");
        }
    }

    @Override
    public ILOLPlayer getProxy() {
        this.proxy = new LOLPlayerProxy(this);
        return this.proxy;
    }

    private boolean isProxy() {
        if (this.proxy == null) {
            return false;
        } else {
            return true;
        }
    }
}

这里增加了一个私有方法,检查是否是自己指定的代理,是指定的代理则允许访问,否则不允许访问。

接下来是强制代理类的改进:

public class LOLPlayerProxy implements ILOLPlayer {

    private ILOLPlayer iloLPlayer;

    public LOLPlayerProxy(ILOLPlayer iloLPlayer) {
        this.iloLPlayer = iloLPlayer;
    }

    @Override
    public void login(String name, String password) {
        this.iloLPlayer.login(name, password);
    }

    @Override
    public void pentaKill() {
        this.iloLPlayer.pentaKill();
    }

    @Override
    public void victory() {
        this.iloLPlayer.victory();
    }

    @Override
    public ILOLPlayer getProxy() {
        return this;
    }
}

最后一个是测试类:

public class Test {
    public static void main(String[] args) {
        test1();
        test2();
        test3();
    }

    public static void test1() {
        ILOLPlayer iloLPlayer = new LOLPlayer("geekdigging");
        iloLPlayer.login("geekdigging", "password");
        iloLPlayer.pentaKill();
        iloLPlayer.victory();
    }

    public static void test2() {
        ILOLPlayer iloLPlayer = new LOLPlayer("geekdigging");
        ILOLPlayer proxy = new LOLPlayerProxy(iloLPlayer);
        proxy.login("geekdigging", "password");
        proxy.pentaKill();
        proxy.victory();
    }

    public static void test3() {
        ILOLPlayer iloLPlayer = new LOLPlayer("geekdigging");
        ILOLPlayer proxy = iloLPlayer.getProxy();
        proxy.login("geekdigging", "password");
        proxy.pentaKill();
        proxy.victory();
    }
}

这里我写了三个测试方法,分别是 test1 、 test2 和 test3 ,执行一下这个测试类,结果如下:

请使用指定的代理
请使用指定的代理
请使用指定的代理
请使用指定的代理
请使用指定的代理
请使用指定的代理
登录游戏:name:geekdigging, password:password
geekdigging 拿到五杀啦!!!
geekdigging 游戏胜利啦!!!

可以发现,前两个方法都没有正常产生访问, test1 是直接 new 了一个对象,无法成功访问,而 test2 虽然是使用了代理,但是结果还是失败了,因为它指定的并不是真实的对象,这个对象是我们自己手动 new 出来的,当然不行,只有最后一个 test3 是可以正常代理对象的。

强制代理的概念就是要从真实角色查找到代理角色,不允许直接访问真实角色。高层模块只要调用 getProxy 就可以访问真实角色的所有方法,它根本就不需要产生一个代理出来,代理的管理已经由真实角色自己完成。

6. 动态代理

动态代理是在实现阶段不用关心代理谁,而在运行阶段才指定代理哪一个对象。相对来说,自己写代理类的方式就是静态代理。

实现动态代理,主要有两种方式,一种是通过 JDK 为我们提供的 InvocationHandler 接口,另一种是使用 cglib 。

把上面的案例接着改成动态代理的方式:

增加一个 LOLPlayIH 动态代理类,来实现 InvocationHandler 接口。

public class LOLPlayIH implements InvocationHandler {

    Object object;

    public LOLPlayIH(Object object) {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = method.invoke(this.object, args);
        return result;
    }
}

这里的 invoke 方法是接口 InvocationHandler 定义必须实现的,它完成对真实方法的调用。

接下来是测试类:

public class Test {
    public static void main(String[] args) {
        ILOLPlayer ilolPlayer = new LOLPlayer("geekdigging");
        InvocationHandler handler = new LOLPlayIH(ilolPlayer);
        ClassLoader loader = ilolPlayer.getClass().getClassLoader();
        ILOLPlayer proxy = (ILOLPlayer) Proxy.newProxyInstance(loader, new Class[] {ILOLPlayer.class}, handler);
        proxy.login("geekdigging", "password");
        proxy.pentaKill();
        proxy.victory();
    }
}

这里我们没有创建代理类,也没有实现 ILOLPlayer 接口,但我们还是让代练在帮我们上分,这就是动态代理。

接下来看下 CGLIB 代理的方式,修改前面的代理类:

public class CglibProxy implements MethodInterceptor {

    private Object target;
    public Object getInstance(final Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(this.target.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {

        Object result = methodProxy.invoke(this.target, objects);

        return result;
    }
}

编写新的测试类:

public class Test {
    public static void main(String[] args) {
        ILOLPlayer ilolPlayer = new LOLPlayer("geekdigging");
        CglibProxy proxy = new CglibProxy();
        LOLPlayer lolPlayer = (LOLPlayer) proxy.getInstance(ilolPlayer);
        lolPlayer.login("geekdigging", "password");
        lolPlayer.pentaKill();
        lolPlayer.victory();
    }
}

这里有一点需要注意, CGLIB 动态代理需要具体对象拥有无参构造,需要我们手动在 LOLPlayer 中添加一个无参构造函数。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK