Для чего нужен Заместитель
Этот паттерн помогает решить проблемы, связанные с контролируемым доступом к объекту. У тебя может возникнуть вопрос: “Для чего нужен такой контролируемый доступ?”. Давай рассмотрим пару ситуаций, которые помогут тебе разобраться, что к чему.Пример 1
Представим, что у нас есть большой проект с кучей старого кода, где есть класс, отвечающий за выгрузку отчетов из базы данных. Класс работает синхронно, то есть вся система простаивает, пока база обрабатывает запрос. В среднем отчет генерируется за 30 минут. Из-за этой особенности его выгрузка запускается в 00:30, и руководство получает этот отчет утром. При анализе выяснилось, что необходимо получать отчет сразу после его генерации, то есть в течение дня. Перенести время запуска нельзя, так как система будет ждать ответ от базы. Выход — изменить принцип работы, запустив выгрузку и генерацию отчета в отдельном потоке. Такое решение позволит системе работать в обычном режиме, а руководство будет получать свежие отчеты. Однако есть проблема: текущий код переписывать нельзя, так как его функции используют другие части системы. В этом случае можно ввести промежуточный прокси-класс с помощью паттерна Заместитель, который будет получать запрос на выгрузку отчета, логировать время начала и запускать отдельный поток. Когда отчет сгенерируется, поток завершит свою работу и все будут счастливы.Пример 2
Команда разработчиков создает сайт-афишу. Чтобы получить данные о новых мероприятиях, они обращаются к стороннему сервису, взаимодействие с которым реализовано через специальную закрытую библиотеку. При разработке появилась проблема: сторонняя система обновляет данные раз в сутки, а запрос к ней происходит каждый раз, когда пользователь обновляет страницу. Это создает большое количество запросов, и сервис перестает отвечать. Решение — кэшировать ответ сервиса и предоставлять посетителям сохраненный результат при каждой перезагрузке, обновляя этот кэш по необходимости. В этом случае использование паттерна Заместитель — отличное решение без изменения готового функционала.Принцип работы паттерна
Чтобы внедрить этот паттерн, нужно создать класс-прокси. Он реализует интерфейс сервисного класса, имитируя его поведение для клиентского кода. Таким образом вместо реального объекта клиент взаимодействует с его заместителем. Как правило, все запросы передаются далее сервисному классу, но с дополнительными действиями до или после его вызова. Проще говоря, этот прокси-объект — прослойка между клиентским кодом и целевым объектом. Рассмотрим пример с кэшированием запроса из очень медленного старого диска. Пусть это будет расписание электропоездов в каком-нибудь древнем приложении, чей принцип действия нельзя изменять. Диск с обновленным расписанием вставляют каждый день в фиксированное время. Итак, у нас есть:- Интерфейс
TimetableTrains
. - Класс
TimetableElectricTrains
, который реализует этот интерфейс. - Именно через этот класс клиентский код взаимодействует с файловой системой диска.
- Класс-клиент
DisplayTimetable
. Его методprintTimetable()
использует методы классаTimetableElectricTrains
.

printTimetable()
класс TimetableElectricTrains
обращается на диск, выгружает данные и предоставляет их клиенту. Эта система функционирует хорошо, но очень медленно. Поэтому было решено увеличить производительность системы, добавив механизм кэширования.
Это можно сделать с использованием паттерна Proxy:

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 { // Ссылка на оригинальный объект 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 { // Ссылка на оригинальный объект 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 { // Измененная ссылка 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
Отлично, работает корректно.
Можно также рассмотреть вариант с фабрикой, которая будет создавать как оригинальный объект, так и объект-заместитель в зависимости от определенных условий.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ