在编程中,正确规划应用程序架构非常重要。为此,一个不可或缺的工具是设计模式。今天我们要讲的是Proxy,或者说代理。
为什么需要副手?
此模式有助于解决与对象的受控访问相关的问题。您可能有一个问题:“为什么我们需要这种受控访问?” 让我们看一下几种情况,它们将帮助您弄清楚是什么。实施例1
假设我们有一个包含一堆旧代码的大型项目,其中有一个类负责从数据库下载报告。该类同步工作,即在数据库处理请求时整个系统处于空闲状态。平均而言,报告会在 30 分钟内生成。由于此功能,其上传从 00:30 开始,管理层在早上收到此报告。经过分析发现,报告生成后必须立即收到,即一天之内。无法重新安排启动时间,因为系统将等待数据库的响应。解决方案是通过在单独的线程中启动上传和报告生成来改变操作原理。该解决方案将允许系统照常运行,并且管理层将收到新的报告。然而,有一个问题:当前的代码无法重写,因为它的功能被系统的其他部分使用。在这种情况下,您可以使用代理模式引入中间代理类,它将接收上传报告的请求,记录开始时间并启动单独的线程。当报告生成后,线程将完成其工作,每个人都会高兴。实施例2
开发团队创建了一个海报网站。为了获取有关新事件的数据,他们求助于第三方服务,并通过特殊的封闭库实现与第三方服务的交互。开发过程中出现了一个问题:第三方系统每天更新一次数据,用户每次刷新页面都会产生一次请求。这会创建大量请求并且服务停止响应。解决方案是缓存服务响应并在每次重新启动时向访问者提供保存的结果,并根据需要更新此缓存。在这种情况下,使用代理模式是一个很好的解决方案,无需更改已完成的功能。该模式如何运作
要实现此模式,您需要创建一个代理类。它实现一个服务类接口,为客户端代码模拟其行为。因此,客户端与其代理进行交互,而不是真实的对象。通常,所有请求都会传递到服务类,但在调用之前或之后会执行其他操作。简单地说,这个代理对象是客户端代码和目标对象之间的一层。让我们看一个缓存来自非常慢的旧磁盘的请求的示例。让它成为某个古老应用中的电动火车时刻表,其运行原理无法改变。每天在固定时间插入具有更新时间表的磁盘。所以我们有:- 界面
TimetableTrains
。 TimetableElectricTrains
实现该接口的类。- 客户端代码正是通过这个类与磁盘文件系统进行交互。
- 客户类
DisplayTimetable
。它的方法printTimetable()
使用类方法TimetableElectricTrains
。
printTimetable()
该类TimetableElectricTrains
都会访问磁盘、卸载数据并将其提供给客户端。该系统运行良好,但速度非常慢。因此,决定通过添加缓存机制来提高系统性能。这可以使用代理模式来完成: 这样,类DisplayTimetable
甚至不会注意到它正在与该类交互TimetableElectricTrainsProxy
,而不是与前一个类交互。新的实现每天加载一次计划,并根据重复的请求,从内存中返回已加载的对象。
对于哪些任务最好使用 Proxy?
在以下几种情况下,这种模式肯定会派上用场:- 缓存。
- 惰性实现也称为惰性实现。当您可以根据需要加载一个对象时,为什么要一次加载它呢?
- 记录请求。
- 临时数据和访问检查。
- 启动并行处理线程。
- 记录或统计通话历史记录。
的优点和缺点
- + 您可以根据需要控制对服务对象的访问;
- + 用于管理服务对象生命周期的附加功能;
- + 无需服务对象即可工作;
- + 提高代码性能和安全性。
- - 由于额外的治疗而存在性能恶化的风险;
- - 使程序类的结构复杂化。
实践中的替代模式
让我们与您一起实现一个从磁盘读取火车时刻表的系统:public interface TimetableTrains {
String[] getTimetable();
String getTrainDepartureTime();
}
实现主接口的类:
public class TimetableElectricTrains implements TimetableTrains {
@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 DisplayTimetable {
private TimetableTrains timetableTrains = new TimetableElectricTrains();
public void printTimetable() {
String[] timetable = timetableTrains.getTimetable();
String[] tmpArr;
System.out.println("Поезд\tОткуда\tКуда\t\tВремя отправления\tВремя прибытия\tВремя в пути");
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;Лондон;Прага;13:43;21:15;07:32
BA-1404;Париж;Грац;14:25;21:25;07:00
9B-8710;Прага;Вена;04:48;08:49;04:01;
9B-8122;Прага;Грац;04:48;08:49;04:01
我们来测试一下:
public static void main(String[] args) {
DisplayTimetable displayTimetable = new DisplayTimetable();
displayTimetable.printTimetable();
}
结论:
Поезд Откуда Куда Время отправления Время прибытия Время в пути
9B-6854 Лондон Прага 13:43 21:15 07:32
BA-1404 Париж Грац 14:25 21:25 07:00
9B-8710 Прага Вена 04:48 08:49 04:01
9B-8122 Прага Грац 04:48 08:49 04:01
现在让我们来看看实现我们的模式的步骤:
-
定义一个接口,允许您使用新的代理而不是原始对象。在我们的示例中是
TimetableTrains
。 -
创建一个代理类。它必须包含对服务对象的引用(在类中创建或传入构造函数);
这是我们的代理类:
public class TimetableElectricTrainsProxy implements TimetableTrains { // Ссылка на оригинальный an object private TimetableTrains timetableTrains = new TimetableElectricTrains(); private String[] timetableCache = null @Override public String[] getTimetable() { return timetableTrains.getTimetable(); } @Override public String getTrainDepartureTime(String trainId) { return timetableTrains.getTrainDepartureTime(trainId); } public void clearCache() { timetableTrains = null; } }
在此阶段,我们只需创建一个带有对原始对象的引用的类,并将所有调用传递给它。
-
我们实现代理类的逻辑。基本上,调用总是重定向到原始对象。
public class TimetableElectricTrainsProxy implements TimetableTrains { // Ссылка на оригинальный an object private TimetableTrains timetableTrains = new TimetableElectricTrains(); private String[] timetableCache = null @Override public String[] getTimetable() { if(timetableCache == null) { timetableCache = timetableTrains.getTimetable(); } return timetableCache; } @Override public String getTrainDepartureTime(String trainId) { if(timetableCache == null) { timetableCache = timetableTrains.getTimetable(); } for(int i = 0; i < timetableCache.length; i++) { if(timetableCache[i].startsWith(trainId+";")) return timetableCache[i]; } return ""; } public void clearCache() { timetableTrains = null; } }
该方法
getTimetable()
检查调度数组是否缓存在内存中。如果没有,它会发出从磁盘加载数据的请求,并存储结果。如果请求已经在运行,它将快速从内存中返回一个对象。由于其简单的功能, getTrainDepartireTime() 方法不必重定向到原始对象。我们只是将其功能复制到一个新方法中。
你不能那样做。如果您必须重复代码或执行类似的操作,则意味着出现了问题,您需要从不同的角度看待问题。在我们的简单示例中没有其他方法,但在实际项目中,很可能代码会写得更正确。
-
将客户端代码中原始对象的创建替换为替换对象:
public class DisplayTimetable { // Измененная link private TimetableTrains timetableTrains = new TimetableElectricTrainsProxy(); public void printTimetable() { String[] timetable = timetableTrains.getTimetable(); String[] tmpArr; System.out.println("Поезд\tОткуда\tКуда\t\tВремя отправления\tВремя прибытия\tВремя в пути"); 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 Лондон Прага 13:43 21:15 07:32 BA-1404 Париж Грац 14:25 21:25 07:00 9B-8710 Прага Вена 04:48 08:49 04:01 9B-8122 Прага Грац 04:48 08:49 04:01
太好了,它工作正常。
您还可以考虑一个工厂,它将根据某些条件创建原始对象和替换对象。
GO TO FULL VERSION