JavaRush /Java блог /Random UA /Паттерн проектування Proxy

Паттерн проектування Proxy

Стаття з групи Random UA
У програмуванні важливо правильно спланувати архітектуру програми. Незамінний засіб для цього – шаблони проектування. Сьогодні поговоримо про Proxy, або по-іншому — Заступника.

Навіщо потрібен Заступник

Цей патерн допомагає вирішити проблеми, пов'язані з контрольованим доступом до об'єкта. У тебе може виникнути питання: "Для чого потрібен такий контрольований доступ?" Давай розглянемо кілька ситуацій, які допоможуть тобі розібратися, що до чого.

Приклад 1

Припустимо, що у нас є великий проект з купою старого коду, де є клас, який відповідає за вивантаження звітів із бази даних. Клас працює синхронно, тобто вся система простоює, доки база обробляє запит. У середньому, звіт генерується за 30 хвабон. Через цю особливість його вивантаження запускається о 00:30 і керівництво отримує цей звіт вранці. При аналізі з'ясувалося, що слід отримувати звіт відразу після його генерації, тобто протягом дня. Перенести час запуску не можна, оскільки система чекатиме відповідь від бази. Вихід — змінити принцип роботи, запустивши розвантаження та генерацію звіту в окремому потоці. Таке рішення дозволить системі працювати у звичайному режимі, а керівництво отримуватиме свіжі звіти. Проте є проблема: поточний код переписувати не можна, оскільки його функції використовують інші частини системи. У цьому випадку можна ввести проміжний проксі-клас за допомогою патерна Заступник, який отримуватиме запит на вивантаження звіту, логуватиме час початку та запускатиме окремий потік. Коли звіт згенерується, потік завершить роботу і всі будуть щасливі.

Приклад 2

Команда розробників створює сайт-афішу. Щоб отримати дані щодо нових заходів, вони звертаються до стороннього сервісу, взаємодія з яким реалізована через спеціальну закриту бібліотеку. Під час розробки виникла проблема: стороння система оновлює дані щодня, а запит до неї відбувається щоразу, коли користувач оновлює сторінку. Це створює багато запитів, і сервіс перестає відповідати. Рішення – кешувати відповідь сервісу та надавати відвідувачам збережений результат при кожному перезавантаженні, оновлюючи цей кеш за потребою. У цьому випадку використання патерну Заступник – відмінне рішення без зміни готового функціоналу.

Принцип роботи патерну

Щоб упровадити цей патерн, потрібно створити клас-проксі. Він реалізує інтерфейс сервісного класу, імітуючи його поведінку клієнтського коду. Таким чином, замість реального об'єкта клієнт взаємодіє з його заступником. Як правило, всі запити передаються далі до сервісного класу, але з додатковими діями до або після його виклику. Простіше кажучи, цей проксі-об'єкт - прошарок між клієнтським кодом і цільовим об'єктом. Розглянемо приклад із кешуванням запиту з дуже повільного старого диска. Нехай це буде розклад електропоїздів у якомусь стародавньому додатку, чий принцип дії не можна змінювати. Диск із оновленим розкладом вставляють щодня у фіксований час. Отже, у нас є:
  1. Інтерфейс TimetableTrains.
  2. Клас TimetableElectricTrains, який реалізує цей інтерфейс.
  3. Саме через цей клас клієнтський код взаємодіє із файловою системою диска.
  4. Клас-клієнт DisplayTimetable. Його метод printTimetable()використовує методи класу TimetableElectricTrains.
Схема проста: Паттерн проектування Proxy - 2В даний момент при кожному виклик методу printTimetable()клас TimetableElectricTrainsзвертається на диск, вивантажує дані і надає їх клієнту. Ця система працює добре, але дуже повільно. Тому вирішабо збільшити продуктивність системи, додавши механізм кешування. Це можна зробити з використанням патерну Proxy: Паттерн проектування Proxy - 3Таким чином, клас DisplayTimetableнавіть не помітить, що взаємодіє з класом TimetableElectricTrainsProxy, а не з попереднім. Нова реалізація завантажує розклад один раз на день, а за повторних запитів повертає вже завантажений об'єкт з пам'яті.

Для яких завдань краще використовувати Proxy

Ось кілька ситуацій, у яких тобі точно стане в нагоді цей патерн:
  1. Кешування.
  2. Відкладена реалізація, також відома як лінива. Навіщо завантажувати об'єкт відразу, якщо можна завантажити його за необхідності?
  3. Логування запитів.
  4. Проміжні перевірки даних та доступу.
  5. Запуск паралельних потоків обробки.
  6. Запис чи підрахунок історії звернення.
Є й інші сценарії використання. Розуміючи принцип роботи цього патерну, ти сам зможеш знайти для нього вдале застосування. На перший погляд, заступник робить те ж, що і Фасад , але це не так. Заступник має той самий інтерфейс , що й сервісний об'єкт. Також не потрібно плутати патерн із Декоратором або Адаптером . Декоратор пропонує розширений інтерфейс, а Адаптер — альтернативний.

Переваги і недоліки

  • + Можна як завгодно контролювати доступ до сервісного об'єкта;
  • + Додаткові можливості управління життєвим циклом сервісного об'єкта;
  • + працює без сервісного об'єкта;
  • + Підвищує швидкодію та безпеку коду.
  • - є ризик погіршення продуктивності через додаткові обробки;
  • - ускладнює структуру класів програми.

Паттерн Заступник на практиці

Давай реалізуємо з тобою систему, яка читає розклад поїздів із диска:
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
Тепер підемо кроками впровадження нашого патерну:
  1. Визначити інтерфейс, який дає змогу використовувати замість оригінального об'єкта новий заступник. У нашому прикладі це TimetableTrains.

  2. Створити клас заступника. У ньому має бути посилання на сервісний об'єкт (створити у класі або передати у конструкторі);

    Ось наш клас-заступник:

    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;
       }
    }

    На цьому етапі просто створюємо клас із посиланням на оригінальний об'єкт та передаємо всі виклики йому.

  3. Реалізуємо логіку класу-заступника. В основному виклик завжди перенаправляється оригінальному об'єкту.

    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() не довелося перенаправляти в оригінальний об'єкт. Ми просто дублювали його функціонал у новий метод.

    Так робити не можна. Якщо довелося дублювати код або робити подібні маніпуляції, то щось пішло не так, і потрібно подивитися на проблему під іншим кутом. У нашому простому прикладі іншого шляху немає, але в реальних проектах, швидше за все, код буде написано коректніше.

  4. Замінити в клієнтському коді створення оригінального об'єкта на об'єкт-заступник:

    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

    Чудово, працює коректно.

    Можна також розглянути варіант із фабрикою, яка створюватиме як оригінальний об'єкт, так і об'єкт-заступник залежно від певних умов.

Корисне посилання замість точки

  1. Відмінна стаття про патерни і трохи про “Заступника”

На сьогодні все! Непогано б повернутися до навчання та перевірити нові знання на практиці :)
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ