13.代理模式Proxy

1.初识代理模式

为其它对象提供一种代理以控制对这个对象的访问。

  • Proxy:代理对象,通常具有如下功能: 1:实现与具体的目标对象一样的接口,这样就可以使用代理来代替具体的目标对象 2:保存一个指向具体目标对象的引用,可以在需要的时候调用具体的目标对象 3:可以控制对具体目标对象的访问,并可能负责创建和删除它
    Subject:目标接口,定义代理和具体目标对象的接口,这样就可以在任何使用具体目标对象的地方使用代理对象
    RealSubject:具体的目标对象,真正实现目标接口要求的功能。

2.体会代理模式

2.1 场景问题——访问多条数据

考虑这样一个实际应用:要一次性访问多条数据。

这个功能的背景是这样的;在一个HR(人力资源)应用项目中客户提出,当选择一个部门或是分公司的时候,要把这个部门或者分公司下的所有员工都显示出来,而且不要翻页,好方便他们进行业务处理。在显示全部员工的时候,只需要显示名称即可,但是也需要提供如下的功能:在必要的时候可以选择并查看某位员工的详细信息。

客户方是一个集团公司,有些部门或者分公司可能有好几百人,不让翻页,也就是要求一次性的获取这多条数据并展示出来。
该怎么样实现呢?

2.2 不使用模式的解决方案

不就是要获取某个部门或者某个分公司下的所有员工的信息吗?直接使用 sql语句从数据库中查询就可以得到,示意性的SQL大致如下:
String sql = "select * from 用户表,部门表 where 用户表.depId = 部门表.depId and 部门表.depId like '"+用户选择查看的depId+"%'";

1.建表的语句如下:

CREATE TABLE TBL_DEP ( 
  DEPID VARCHAR2(20) PRIMARY KEY, 
  NAME VARCHAR2(20) 
); 
CREATE TABLE TBL_USER ( 
  USERID VARCHAR2(20) PRIMARY KEY, 
  NAME VARCHAR2(20) , 
  DEPID VARCHAR2(20) , 
  SEX VARCHAR2(10) , 
  CONSTRAINT TBL_USER_FK FOREIGN KEY(DEPID)   REFERENCES TBL_DEP(DEPID) 
);

2.增加点测试数据

INSERT INTO TBL_DEP VALUES('01','总公司'); 
INSERT INTO TBL_DEP VALUES('0101','一分公司'); 
INSERT INTO TBL_DEP VALUES('0102','二分公司'); 
INSERT INTO TBL_DEP VALUES('010101','开发部'); 
INSERT INTO TBL_DEP VALUES('010102','测试部'); 
INSERT INTO TBL_DEP VALUES('010201','开发部'); 
INSERT INTO TBL_DEP VALUES('010202','客服部'); 
INSERT INTO TBL_USER VALUES('user0001','张三1','010101','男'); 
INSERT INTO TBL_USER VALUES('user0002','张三2','010101','男'); 
INSERT INTO TBL_USER VALUES('user0003','张三3','010102','男'); 
INSERT INTO TBL_USER VALUES('user0004','张三4','010201','男'); 
INSERT INTO TBL_USER VALUES('user0005','张三5','010201','男'); 
INSERT INTO TBL_USER VALUES('user0006','张三6','010202','男'); 
COMMIT; 

存在的问题:
上面的实现看起来很简单,功能也正确,但是蕴含一个较大的问题,那就是:当一次性访问的数据条数过多,而且每条描述的数据量又很大的话,那会消耗较多的内存。

对于用户表,事实上是有很多字段的,不仅仅是示例的那么几个,再加上不使用翻页,一次性访问的数据就可能会有很多条。如果一次性需要访问的数据较多的话,内存开销会比较大。

而且从客户使用角度来说,有很大的随机性,客户既可能访问每一条数据,也可能一条都不访问。也就是说,一次性访问很多条数据,消耗了大量内存,但是很可能是浪费掉了,客户根本就不会去访问那么多数据,对于每条数据,客户只需要看看姓名而已。

那么该怎么实现,才能既把多条用户数据的姓名显示出来,而又能节省内存空间,当然还要实现在客户想要看到更多数据的时候,能正确访问到数据呢?

2.3 使用模式的解决方案

3.理解代理模式

3.1 认识代理模式

3.1.1 代理模式的功能

代理模式是通过创建一个代理对象,用这个代理对象去代表真实的对象,客户端得到这个代理对象过后,对客户端没有什么影响,就跟得到了真实对象一样来使用。

当客户端操作这个代理对象时,实际上功能最终还是会由真实的对象来完成,只不过是通过代理操作的,也就是客户端操作代理,代理操作真正的对象。

正是因为有代理对象夹在客户端和被代理的真实对象中间,相当于一个中转,那么在中转的时候就有很多花招可以玩,比如:判断一下权限,如果没有足够的权限那就不给你中转了,等等。

3.1.2:代理的分类

(1)虚代理:根据需要来创建开销很大的对象,该对象只有在需要的时候才会被真正创建
(2)远程代理:用来在不同的地址空间上代表同一个对象,这个不同的地址空间可以是在本机,也可以在其它机器上,在Java里面最典型的就是RMI技术
(3)copy-on-write代理:在客户端操作的时候,只有对象确实改变了,才会真的拷贝(或克隆)一个目标对象,算是虚代理的一个分支
(4)保护代理:控制对原始对象的访问,如果有需要,可以给不同的用户提供不同的访问权限,以控制他们对原始对象的访问
(5)Cache代理:为那些昂贵的操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果
(6)防火墙代理:保护对象不被恶意用户访问和操作
(7)同步代理:使多个用户能够同时访问目标对象而没有冲突
(8)智能指引:在访问对象时执行一些附加操作,比如:对指向实际对象的引用计数、第一次引用一个持久对象时,将它装入内存等

在这些代理类型中,最常见的是:虚代理、保护代理、远程代理和智能指引这几种。我们主要来学习虚代理和保护代理,这是实际开发中使用频率最高的。

3.1.3:虚代理的示例

前面的例子就是一个典型的虚代理的实现。

3.1.4:copy-on-write

拷贝一个大的对象是很消耗资源的,如果这个被拷贝的对象从上次操作以来,根本就没有被修改过,那么再拷贝这个对象是没有必要的,白白消耗资源而已。那么就可以使用代理来延迟拷贝的过程,可以等到对象被修改的时候才真的对它进行拷贝。

copy-on-write可以大大降低拷贝大对象的开销,因此它算是一种优化方式,可以根据需要来拷贝或者克隆对象。

3.1.5:具体目标和代理的关系

从代理模式的结构图来看,好像是有一个具体目标类就有一个代理类,其实不是这样的。如果代理类能完全通过接口来操作它所代理的目标对象,那么代理对象就不需要知道具体的目标对象,这样就无须为每一个具体目标类都创建一个代理类了。

但是,如果代理类必须要实例化它代理的目标对象,那么代理类就必须知道具体被代理的对象,这种情况下,一个具体目标类通常会有一个代理类。这种情况多出现在虚代理的实现里面。

3.1.6:代理模式的调用顺序示意图

3.2 保护代理

保护代理是一种控制对原始对象访问的代理,多用于对象应该有不同的访问权限的时候。保护代理会检查调用者是否具有请求所必需的访问权限,如果没有相应的权限,那么就不会调用目标对象,从而实现对目标对象的保护。

1.示例需求
现在有一个订单系统,要求是:一旦订单被创建,只有订单的创建人才可以修改订单中的数据,其他人不能修改。

相当于现在如果有了一个订单对象实例,那么就需要控制外部对它的访问,满足条件的可以访问,而不满足条件的就不能访问了。

3.3 Java中的代理

3.3.1 Java的静态代理

通常把前面自己实现的代理模式,称为Java的静态代理。这种实现方式有一个较大的缺点,就是如果Subject接口发生变化,那么代理类和具体的目标实现都要变化,不是很灵活

3.3.2 Java的动态代理

通常把使用Java内建的对代理模式支持的功能来实现的代理称为Java的动态代理。动态代理跟静态代理相比,明显的变化是:静态代理实现的时候,在 Subject接口上定义很多的方法,代理类里面自然也要实现很多方法;而动态代理实现的时候,虽然Subject接口上定义了很多方法,但是动态代理类始终只有一个invoke方法。这样当Subject接口发生变化的时候,动态代理的接口就不需要跟着变化了。

JDK 为我们提供了一种动态代理的实现,通过实现 InvocationHandler 接口来实现动态代理。

控制库存的动态代理类:

public class StockHandler implements InvocationHandler {
    /**
     * 被代理类
     */
    private Object target;

    /**
     * 库存
     */
    private static Integer stock = 1;

    public StockHandler(Object target){
        super();
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        if(stock > 0){
            method.invoke(target,null);
            stock--;
        }else{
            throw new RuntimeException("库存不足");
        }

        return null;
    }
}

仔细观察上面的动态代理类,发现它的被代理类 使用了 Object 类型,而不是具体指定某一个类,在调用时再指定:

Station station = new Station();
InvocationHandler handler = new StockHandler(station);

Class cls = station.getClass();
ClassLoader loader = cls.getClassLoader();
TicketSell sell = (TicketSell)Proxy.newProxyInstance(loader,cls.getInterfaces(),handler);

sell.buyTicket();
sell.buyTicket();

JDK 动态代理也有不足之处,它要求被代理类一定要实现某个接口,比如上面的 Station 类实现了 TicketSell 接口。如果我们的类原本是没有实现接口的,总不能为了用代理而特意去给他加一个接口吧?

为了解决这个问题,可以使用 cglib 动态代理,它是基于类做的代理,而不是基于接口。

3.4 代理模式的优缺点

代理模式在客户和被客户访问的对象之间,引入了一定程度的间接性,客户是直接使用代理,让代理来与被访问的对象进行交互。不同的代理类型,这种附加的间接性有不同的用途,也就是有不同的特点:

  • 1)远程代理:隐藏了一个对象存在于不同的地址空间的事实,也即是客户通过远程代理去访问一个对象,根本就不关心这个对象在哪里,也不关心如何通过网络去访问到这个对象,从客户的角度来讲,它只是在使用代理对象而已。
  • 2)虚代理:可以根据需要来创建 “大”对象,只有到必须创建对象的时候,虚代理才会创建对象,从而大大加快程序运行速度,并节省资源。通过虚代理可以对系统进行优化。
  • 3)保护代理:可以在访问一个对象的前后,执行很多附加的操作,除了进行权限控制之外,还可以进行很多跟业务相关的处理,而不需要修改被代理的对象。也就是说,可以通过代理来给目标对象增加功能。
  • 4)智能指引:跟保护代理类似,也是允许在访问一个对象的前后,执行很多附加的操作,这样一来就可以做很多额外的事情,比如:引用计数等。

4.思考代理模式

4.1 代理模式的本质

代理模式的本质是:控制对象访问

4.2 何时选用

  • 1)需要为一个对象在不同的地址空间提供局部代表的时候,可以使用远程代理
  • 2)需要按照需要创建开销很大的对象的时候,可以使用虚代理
  • 3)需要控制对原始对象的访问的时候,可以使用保护代理
  • 4)需要在访问对象的时候执行一些附加操作的时候,可以使用智能指引代理

参考

  • 1)周君 经典设计模式实战演练
  • 2)研磨设计模式——跟着cc学设计系列视频教程

你可能感兴趣的:(13.代理模式Proxy)