【设计模式】结构型模式其七: 代理模式(上) -> 静态代理

97 阅读9分钟

代理模式(上)

什么是代理模式

image.png

代购商品:顾客 -> 代购网站 -> 商品
软件开发:客户端 -> 代理对象 -> 真实对象

定义

代理模式:
给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问
引入一个新的代理对象
代理对象在客户端对象和目标对象之间起到中介的作用
去掉客户不能看到的内容和服务或者增添客户需要的额外的新服务

模式结构

image.png

代理模式包含以下3个角色:

  • Subject(抽象主题角色)
  • Proxy(代理主题角色)
  • RealSubject(真实主题角色)

了解概念之后,来个案例加深理解

案例学习

某软件公司承接了某信息咨询公司的收费商务信息查询系统的开发任务,该系统的基本需求如下:

(1) 在进行商务信息查询之前用户需要通过身份验证,只有合法用户才能够使用该查询系统;

(2) 在进行商务信息查询时系统需要记录查询日志,以便根据查询次数收取查询费用。

该软件公司开发人员已完成了商务信息查询模块的开发任务。

现希望能够以一种松耦合的方式向原有系统增加身份验证和日志记录功能,客户端代码可以无区别地对待原始的商务信息查询模块和增加新功能之后的商务信息查询模块,而且可能在将来还要在该信息查询模块中增加一些新的功能。

现使用代理模式设计并实现该收费商务信息查询系统。

image.png

业务类

这部分是一个独立模块,为了业务而存在,用于添加真实主题操作的额外功能

//身份验证类:业务类
public class AccessValidator {
   //模拟实现登录验证
   public boolean validate(String userId) {
      System.out.println("在数据库中验证用户'" + userId + "'是否是合法用户?");
      if (userId.equalsIgnoreCase("杨过")) {
         System.out.println("'" + userId + "'登录成功!");
         return true;
      }
      else {
         System.out.println("'" + userId + "'登录失败!");
         return false;
      }
   }
}

//日志记录类:业务类

public class Logger {
   //模拟实现日志记录
   public void log(String userId) {
      System.out.println("更新数据库,用户'" + userId + "'查询次数加1!");
   }
}

抽象主题

//抽象查询类:抽象主题类

//为一个接口,需要进行的操作的抽象

public interface Searcher {
   public String doSearch(String userId,String keyword);
}    

真实主题类

// 真实进行的操作

//具体查询类:真实主题类
public class RealSearcher implements Searcher {
   //模拟查询商务信息
   public String doSearch(String userId, String keyword) {
      System.out.println("用户'" + userId + "'使用关键词'" + keyword + "'查询商务信息!");
      return "返回具体内容";
   }
}

代理类

// 将具体的操作在代理类中进行,比如真实业务的实行,额外业务的实行,而且这里面可以自定义执行顺序。

// 因此,我们需要维持对真实主题的引用与业务代码的引用,不过业务代码可以在调用时才新建。

//代理查询类:代理主题类
public class ProxySearcher implements Searcher {
   private RealSearcher searcher = new RealSearcher(); //维持一个对真实主题的引用
   private AccessValidator validator;
   private Logger logger;
   
   public String doSearch(String userId,String keyword) {
      //如果身份验证成功,则执行查询
      if (this.validate(userId)) {
         String result = searcher.doSearch(userId,keyword); //调用真实主题对象的查询方法
         this.log(userId); //记录查询日志
         return result; //返回查询结果
      }
      else {
         return null;
      }
   } 
   
   //创建访问验证对象并调用其validate()方法实现身份验证
   public boolean validate(String userId) {
      validator = new AccessValidator();
      return validator.validate(userId);
   }
   
   //创建日志记录对象并调用其log()方法实现日志记录
   public void log(String userId) {
      logger = new Logger();
      logger.log(userId);
   }
}

我们目前的代理类是一个继承了抽象主题类的类,我们为了可以动态的替换代理类,我们可以使用配置文件来动态替换。

XML文件类

<?xml version="1.0"?>
<config>
   <className>designpatterns.proxy.ProxySearcher</className>
</config>

*不是我这里怎么写配置文件你就怎么写,你需要写上完整类名(在你电脑上的位置),这样反射才能正常启用。

XML文件读取类

public class XMLUtil {
   //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象
   public static Object getBean() {
      try {
         //创建DOM文档对象
         DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();
         DocumentBuilder builder = dFactory.newDocumentBuilder();
         Document doc;                    
         doc =builder.parse(new File("./config.xml")); 
      
         //获取包含类名的文本节点
         NodeList nl = doc.getElementsByTagName("className");
         Node classNode=nl.item(0).getFirstChild();
         String cName=classNode.getNodeValue();
        
         //通过类名生成实例对象并将其返回
         Class c=Class.forName(cName);
         Object obj=c.getConstructor().newInstance();
         return obj;
      }   
      catch(Exception e) {
         e.printStackTrace();
         return null;
      }
   }
}

客户端调用

客户端调用时不需要知道代不代理,它只用调用想要调用的方法就行。

根据配置文件来生成具体的代理类。

public class Client {
   public static void main(String args[]) {
      Searcher searcher;  //针对抽象编程,客户端无须分辨真实主题类和代理类
      searcher = (Searcher)XMLUtil.getBean();
      String result = searcher.doSearch("杨过","玉女心经");
   }
}

输出

在数据库中验证用户'杨过'是否是合法用户?
'杨过'登录成功!
用户'杨过'使用关键词'玉女心经'查询商务信息!
更新数据库,用户'杨过'查询次数加1!

实例代码
(1) AccessValidator:身份验证类,业务类
(2) Logger:日志记录类,业务类
(3) Searcher:抽象查询类,充当抽象主题角色
(4) RealSearcher:具体查询类,充当真实主题角色
(5) ProxySearcher:代理查询类,充当代理主题角色
(6) Client:客户端测试类

代理模式分析

一般有如下常见代理模式:

  1. 远程代理(Remote Proxy):为一个位于不同的地址空间的对象提供一个本地的代理对象,这个不同的地址空间可以在同一台主机中,也可以在另一台主机中,远程代理又称为大使(Ambassador)

  2. 虚拟代理(Virtual Proxy):如果需要创建一个资源消耗较大的对象,先创建一个消耗相对较小的对象来表示,真实对象只在需要时才会被真正创建

  3. 保护代理(Protect Proxy):控制对一个对象的访问,可以给不同的用户提供不同级别的使用权限

  4. 缓冲代理(Cache Proxy):为某一个目标操作的结果提供临时的存储空间,以便多个客户端可以共享这些结果

  5. 智能引用代理(Smart Reference Proxy):当一个对象被引用时,提供一些额外的操作,例如将对象被调用的次数记录下来等

我们的案例中使用了保护代理智能引用代理: 在代理类ProxySearcher中实现对真实主题类的权限控制和引用计数

远程代理进一步解释

像我们的云服务器,就是通过IP与TCP端口号与远程服务器进行连接,然后获得一个客户端来进行操作,远程代理对象承担了大部分的网络通信工作,并负责对远程业务方法的调用。

虚拟代理进一步解释

  • 对于一些占用系统资源较多或者加载时间较长的对象,可以给这些对象提供一个虚拟代理
  • 在真实对象创建成功之前虚拟代理扮演真实对象的替身,而当真实对象创建之后,虚拟代理将用户的请求转发给真实对象
  • 使用一个“虚假”的代理对象来代表真实对象,通过代理对象来间接引用真实对象,可以在一定程度上提高系统的性能
具体例子
interface Subject {
    void run();
}

class RealSubject implements Subject {
    @Override
    public void run() {
        // 真正的程序逻辑
    }
}

class VirtualProxy implements Subject {
    private RealSubject realSubject;

    @Override
    public void run() {
        if (realSubject == null) {
            realSubject = new RealSubject();
        }
        realSubject.run();
    }
}

public class Client{
    public static void main(String[] args) {
        // 虚拟对象
        VirtualProxy virtualProxy = new VirtualProxy();
        // 执行实际逻辑
        virtualProxy.run();
    }
}

模式优缺点

模式优点:

共有优点

  • 能够协调调用者和被调用者,在一定程度上降低了系统的耦合度
  • 客户端可以针对抽象主题角色进行编程,增加和更换代理类无须修改源代码,符合开闭原则,系统具有较好的灵活性和可扩展性

特有优点

  • 远程代理:可以将一些消耗资源较多的对象和操作移至性能更好的计算机上,提高了系统的整体运行效率
  • 虚拟代理:通过一个消耗资源较少的对象来代表一个消耗资源较多的对象,可以在一定程度上节省系统的运行开销
  • 缓冲代理:为某一个操作的结果提供临时的缓存存储空间,以便在后续使用中能够共享这些结果,优化系统性能,缩短执行时间
  • 保护代理:可以控制对一个对象的访问权限,为不同用户提供不同级别的使用权限

模式缺点

  • 由于在客户端和真实主题之间增加了代理对象,因此有些类型的代理模式可能会造成请求的处理速度变慢(例如保护代理)
  • 实现代理模式需要额外的工作,而且有些代理模式的实现过程较为复杂(例如远程代理)

动态代理也算代理模式,但是这篇文章够长了,所以分到下一篇。

适用环境

  • 当客户端对象需要访问远程主机中的对象时可以使用远程代理
  • 当需要用一个消耗资源较少的对象来代表一个消耗资源较多的对象,从而降低系统开销、缩短运行时间时可以使用虚拟代理
  • 当需要为某一个被频繁访问的操作结果提供一个临时存储空间,以供多个客户端共享访问这些结果时可以使用缓冲代理
  • 当需要控制对一个对象的访问,为不同用户提供不同级别的访问权限时可以使用保护代理
  • 当需要为一个对象的访问(引用)提供一些额外的操作时可以使用智能引用代理