CodeGym /Java 博客 /随机的 /代理设计模式
John Squirrels
第 41 级
San Francisco

代理设计模式

已在 随机的 群组中发布
在编程中,正确规划应用程序的体系结构非常重要。设计模式是实现这一目标不可或缺的方法。今天我们来谈谈代理。

为什么需要代理?

此模式有助于解决与对象的受控访问相关的问题。您可能会问,“为什么我们需要受控访问?” 让我们看一下可以帮助您弄清楚什么是什么的几种情况。

示例 1

想象一下,我们有一个包含一堆旧代码的大型项目,其中有一个类负责从数据库中导出报告。该类同步工作。也就是说,当数据库处理请求时,整个系统处于空闲状态。生成报告平均需要 30 分钟。因此,导出过程从凌晨 12:30 开始,管理层在早上收到报告。审计显示,如果能够在正常工作时间内立即收到报告会更好。开始时间不能推迟,系统在等待数据库响应时不能阻塞。解决方案是改变系统的工作方式,在单独的线程上生成和导出报告。该解决方案将使系统照常工作,管理层将收到新的报告。然而,存在一个问题:无法重写当前代码,因为系统的其他部分使用了它的功能。在这种情况下,我们可以使用代理模式引入一个中间代理类,该类将接收导出报告的请求,记录开始时间,并启动一个单独的线程。生成报告后,线程终止,大家都很高兴。

示例 2

一个开发团队正在创建一个活动网站。为了获取新事件的数据,该团队查询第三方服务。一个特殊的私人图书馆促进与服务的互动。在开发过程中,发现一个问题:第三方系统每天更新一次数据,但是用户每次刷新页面都会向其发送一个请求。这会创建大量请求,并且服务会停止响应。解决方案是缓存服务的响应,并在重新加载页面时将缓存的结果返回给访问者,并根据需要更新缓存。在这种情况下,代理设计模式是一种不改变现有功能的优秀解决方案。

设计模式背后的原理

要实现此模式,您需要创建一个代理类。它实现服务类的接口,模仿其对客户端代码的行为。以这种方式,客户端与代理而不是真实对象进行交互。通常,所有请求都传递给服务类,但之前或之后有其他操作。简而言之,代理是客户端代码和目标对象之间的一层。考虑从一个旧的和非常慢的硬盘缓存查询结果的例子。假设我们正在谈论某个逻辑无法更改的古老应用程序中的电动火车时刻表。每天在固定时间插入带有更新时间表的磁盘。所以,我们有:
  1. TrainTimetable界面。
  2. ElectricTrainTimetable,它实现了这个接口。
  3. 客户端代码通过这个类与文件系统进行交互。
  4. TimetableDisplay客户端类。它的printTimetable()方法使用类的方法ElectricTrainTimetable
该图很简单: 代理设计模式:- 2目前,每次调用printTimetable()方法时,ElectricTrainTimetable类都会访问磁盘、加载数据并将其呈现给客户端。系统运行正常,但速度很慢。因此,决定通过添加缓存机制来提高系统性能。这可以使用代理模式来完成: 代理设计模式:- 3因此,TimetableDisplay类甚至不会注意到它正在与ElectricTrainTimetableProxy类而不是旧类进行交互。新的实现每天加载一次时间表。对于重复请求,它从内存中返回先前加载的对象。

什么任务最适合代理?

以下是这种模式肯定会派上用场的几种情况:
  1. 缓存
  2. 延迟或惰性初始化 如果可以根据需要加载对象,为什么要立即加载它?
  3. 记录请求
  4. 数据和访问的中间验证
  5. 启动工作线程
  6. 记录对对象的访问
还有其他用例。了解此模式背后的原理,您可以确定可以成功应用它的情况。乍一看,代理与门面做同样的事情,但事实并非如此。代理具有与服务对象相同的接口。另外,不要将此模式与装饰器适配器模式混淆。装饰提供扩展接口,适配器提供替代接口。

的优点和缺点

  • + 您可以根据需要控制对服务对象的访问
  • + 与管理服务对象生命周期相关的附加能力
  • + 它在没有服务对象的情况下工作
  • + 它提高了性能和代码安全性。
  • - 由于额外的请求,性能可能会变差
  • - 它使类层次结构更加复杂

实践中的代理模式

让我们实现一个从硬盘读取火车时刻表的系统:

public interface TrainTimetable {
   String[] getTimetable();
   String getTrainDepartureTime();
}
这是实现主接口的类:

public class ElectricTrainTimetable implements TrainTimetable {

   @Override
   public String[] getTimetable() {
       ArrayList<String> list = new ArrayList<>();
       try {
           Scanner scanner = new Scanner(new FileReader(new File("/tmp/electric_trains.csv")));
           while (scanner.hasNextLine()) {
               String line = scanner.nextLine();
               list.add(line);
           }
       } catch (IOException e) {
           System.err.println("Error:  " + e);
       }
       return list.toArray(new String[list.size()]);
   }

   @Override
   public String getTrainDepartureTime(String trainId) {
       String[] timetable = getTimetable();
       for (int i = 0; i < timetable.length; i++) {
           if (timetable[i].startsWith(trainId+";")) return timetable[i];
       }
       return "";
   }
}
每次获得火车时刻表时,程序都会从​​磁盘读取一个文件。但这只是我们麻烦的开始。每次您获得哪怕是一列火车的时刻表时,都会读取整个文件!很好的是,这样的代码只存在于不做什么的例子中:)客户端类:

public class TimetableDisplay {
   private TrainTimetable trainTimetable = new ElectricTrainTimetable();

   public void printTimetable() {
       String[] timetable = trainTimetable.getTimetable();
       String[] tmpArr;
       System.out.println("Train\\tFrom\\tTo\\t\\tDeparture time\\tArrival time\\tTravel time");
       for (int i = 0; i < timetable.length; i++) {
           tmpArr = timetable[i].split(";");
           System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]);
       }
   }
}
示例文件:

9B-6854;London;Prague;13:43;21:15;07:32
BA-1404;Paris;Graz;14:25;21:25;07:00
9B-8710;Prague;Vienna;04:48;08:49;04:01;
9B-8122;Prague;Graz;04:48;08:49;04:01
让我们测试一下:

public static void main(String[] args) {
   TimetableDisplay timetableDisplay = new timetableDisplay();
   timetableDisplay.printTimetable();
}
输出:

Train  From  To  Departure time  Arrival time  Travel time
9B-6854  London  Prague  13:43  21:15  07:32
BA-1404  Paris  Graz  14:25  21:25  07:00
9B-8710  Prague  Vienna  04:48  08:49  04:01
9B-8122  Prague  Graz  04:48  08:49  04:01
现在让我们来看看介绍我们的模式所需的步骤:
  1. 定义一个接口,允许使用代理而不是原始对象。在我们的示例中,这是TrainTimetable.

  2. 创建代理类。它应该有对服务对象的引用(在类中创建它或传递给构造函数)。

    这是我们的代理类:

    
    public class ElectricTrainTimetableProxy implements TrainTimetable {
       // Reference to the original object
       private TrainTimetable trainTimetable = new ElectricTrainTimetable();
      
       private String[] timetableCache = null
    
       @Override
       public String[] getTimetable() {
           return trainTimetable.getTimetable();
       }
    
       @Override
       public String getTrainDepartureTime(String trainId) {
           return trainTimetable.getTrainDepartureTime(trainId);
       }
      
       public void clearCache() {
           trainTimetable = null;
       }
    }
    

    在此阶段,我们只是创建一个引用原始对象的类,并将所有调用转发给它。

  3. 让我们来实现代理类的逻辑。基本上,调用总是重定向到原始对象。

    
    public class ElectricTrainTimetableProxy implements TrainTimetable {
       // Reference to the original object
       private TrainTimetable trainTimetable = new ElectricTrainTimetable();
    
       private String[] timetableCache = null
    
       @Override
       public String[] getTimetable() {
           if (timetableCache == null) {
               timetableCache = trainTimetable.getTimetable();
           }
           return timetableCache;
       }
    
       @Override
       public String getTrainDepartureTime(String trainId) {
           if (timetableCache == null) {
               timetableCache = trainTimetable.getTimetable();
           }
           for (int i = 0; i < timetableCache.length; i++) {
               if (timetableCache[i].startsWith(trainId+";")) return timetableCache[i];
           }
           return "";
       }
    
       public void clearCache() {
           trainTimetable = null;
       }
    }
    

    检查getTimetable()时间表数组是否已缓存在内存中。如果没有,它会发送请求从磁盘加载数据并保存结果。如果已经请求了时间表,它会迅速从内存中返回对象。

    由于其简单的功能,getTrainDepartureTime() 方法不必重定向到原始对象。我们只是用一种新方法复制了它的功能。

    不要这样做。如果你必须复制代码或做类似的事情,那就是出了问题,你需要从不同的角度重新审视问题。在我们的简单示例中,我们别无选择。但在实际项目中,代码很可能会写得更正确。

  4. 在客户端代码中,创建一个代理对象而不是原始对象:

    
    public class TimetableDisplay {
       // Changed reference
       private TrainTimetable trainTimetable = new ElectricTrainTimetableProxy();
    
       public void printTimetable() {
           String[] timetable = trainTimetable.getTimetable();
           String[] tmpArr;
           System.out.println("Train\\tFrom\\tTo\\t\\tDeparture time\\tArrival time\\tTravel time");
           for (int i = 0; i < timetable.length; i++) {
               tmpArr = timetable[i].split(";");
               System.out.printf("%s\t%s\t%s\t\t%s\t\t\t\t%s\t\t\t%s\n", tmpArr[0], tmpArr[1], tmpArr[2], tmpArr[3], tmpArr[4], tmpArr[5]);
           }
       }
    }
    

    查看

    
    Train  From  To  Departure time  Arrival time  Travel time
    9B-6854  London  Prague  13:43  21:15  07:32
    BA-1404  Paris  Graz  14:25  21:25  07:00
    9B-8710  Prague  Vienna  04:48  08:49  04:01
    9B-8122  Prague  Graz  04:48  08:49  04:01
    

    太好了,它工作正常。

    您还可以考虑根据特定条件创建原始对象和代理对象的工厂选项。

在我们说再见之前,这里有一个有用的链接

今天就到这里!返回课程并在实践中尝试您的新知识并不是一个坏主意:)
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION