Паттерн проектирования «Заместитель» / «Proxy»

    Почитать описание других паттернов.

    Проблема


    Необходимо контролировать доступ к объекту, не изменяя при этом поведение клиента.

    Описание


    При проектировании сложных систем, достаточно часто возникает необходимость обеспечить контролируемый доступ к определенным объектам системы. Мотивацией для этого служит ряд приобретаемых преимуществ. Таких как, ленивая инициализация по требованию для «громоздких» объектов, подсчет количества ссылок на объект и т.д. и т.п. Однако, не всегда потребность в контролируемом доступе к объекту базируется только на преимуществах. Как правило, сложность процессов реального мира, ограничения вычислительных ресурсов просто не оставляют проектировщику выбора, нежели как воспользоваться паттерном «Заместитель» («Сурогат»).

    Идея паттерна «Заместитель» заключается в предоставлении клиенту другого объекта (заместителя), взамен объекту с контролируемым доступом. При этом, объект-заместитель, реализует тот-же интерфейс, что и оригинальный объект, в результате чего, поведение клиента не требует изменений. Иными словами, клиент взаимодействует с заместителем ровно как с оригинальным объектом посредством единого интерфейса. Клиент, так же, не делает предположений о том работает ли он с реальным объектом или его заместителем. Контролирование доступа к объекту, при этом, достигается за счет использования ссылки на него в заместителе, благодаря которой заместитель переадресовывает внешние вызовы контролируемому объекту, возможно сопровождая их дополнительными операциями.

    Такой подход позволяет неявным для клиента образом контролировать доступ к объекту.

    Практическая задача


    Рассмотрим задачу реализации игры «Сапер». Будем полагать, что для ячеек (Cell), которые бывают заминированными (Mine) и пустыми (Empty), существуют некоторые громоздкие графические изображения. Для заминированной ячейки — мина, для пустой ячейки изображение с количеством мин в соседних клетках. При этом, само изображение хранится в каждой ячейке и инстанциируется в момент ее создания. Игрок же, видит изображение ячейки только после ее открытия (операция open()). Поэтому, было бы разумным инстанциировать ячейки в тот момент, когда игрок пытается их открыть, чтобы сократить расходы общей памяти для хранения изображений. Однако, такой подход тут применить нельзя. Дело в том, что до операции open() у каждой ячейки вызываются операции getTop(), getLeft() для получения координат ячейки. Но если ячейка еще не будет создана, о каких ее координатах может идти речь?

    Использование паттерна прокси решает данную проблему. Вместо оригинальных объектов клиент будет использовать их земестители (MineProxy, EmptyProxy). При этом становится возможной ленивая инициализация ячеек, ввиду того, что оригинальный объект создается лишь при вызове операции open() у прокси а на запросы о получении координат (getTop(), getLeft()) прокси отвечает самостоятельно, по крайней мере до момента создания оригинального объекта.

    Диаграмма классов




    Реализация на Java


    /**
    * Абстрактный класс ячейки минного поля
    */
    public abstract class Cell {
      public static final int OPENED = 0;
      public static final int CLOSED = 1;
      
      protected int status;

      protected int left, top;

      public Cell(int left, int top) {
        super();
        
        this.left = left;
        this.top = top;
        this.status = Cell.CLOSED;
      }
      
      /**
       * Открыть данную ячейку. Будем считать, что в этой операции происходит некоторая
       * ресурсоемкая операция. Например, загрузка изображения, для отображения содержимого ячейки.
       */
      public void open() {  
        this.status = Cell.OPENED;
      }

      public int getLeft() {
        return left;
      }
      
      public int getTop() {
        return top;
      }
      
      public int getStatus() {
        return status;
      }

      /**
       * Единственная абстрактная операция, возвращаяет количество очков за открытие данной ячейки.
       */
      public abstract int getPoints();  
    }

    /**
    * Уточнение ячейки минного поля, в качестве пустой ячейки
    */
    public class Empty extends Cell {
      
      public Empty(int left, int top) {
        super(left, top);
        
        // загружаем тяжелое изображение пустой ячейки.
      }

      @Override
      public int getPoints() {
        return 10;  // 10 очков за открытую пустую ячейку
      }
    }

    /**
    * Уточнение ячейки, как ячейки с миной.
    */
    public class Mine extends Cell {
      
      public Mine(int left, int top) {
        super(left, top);
        
        // загружаем тяжелое изображение ячейки c миной
      }

      @Override
      public int getPoints() {
        return 100;   // 100 очков за открытую мину
      }
    }

    /**
    * Прокси для пустой ячейки
    */
    public class EmptyProxy extends Cell {
      private Empty proxy; // ссылка на пустую ячейку

      public EmptyProxy(int left, int top) {
        super(left, top);
        this.proxy = null;
      }

      /**
       * Ленивая инициализация пустой ячейки
       */
      @Override
      public void open() {
        if (proxy == null) {
          proxy = new Empty(left, top);
        }
        
        proxy.open();
      }

      @Override
      public int getLeft() {
        if (proxy == null) {
          return left;
        } else {
          return proxy.getLeft();
        }
      
      }
      
      @Override
      public int getTop() {
        if (proxy == null) {
          return top;
        } else {
          return proxy.getTop();
        }
      }

      @Override
      public int getStatus() {
        if (proxy == null) {
          return status;
        } else {
          return proxy.getStatus();
        }
      }

      @Override
      public int getPoints() {
        if (proxy == null) {
          return 10;
        } else {
          return proxy.getPoints();
        }
      }
    }

    /**
    * Прокси для ячейки с миной
    */
    public class MineProxy extends Cell {
      private Mine proxy;

      public MineProxy(int left, int top) {
        super(left, top);
        
        this.proxy = null;
      }
      
      /**
       * Ленивая инициализация ячейки с миной
       */
      @Override
      public void open() {
        if (proxy == null) {
          proxy = new Mine(left, top);
        }
        
        proxy.open();
      }

      @Override
      public int getLeft() {
        if (proxy == null) {
          return left;
        } else {
          return proxy.getLeft();
        }
      
      }
      
      @Override
      public int getTop() {
        if (proxy == null) {
          return top;
        } else {
          return proxy.getTop();
        }
      }

      @Override
      public int getStatus() {
        if (proxy == null) {
          return status;
        } else {
          return proxy.getStatus();
        }
      }

      @Override
      public int getPoints() {
        if (proxy == null) {
          return 100;
        } else {
          return proxy.getPoints();
        }
      }
    }

    /**
    * Использование
    */
    public class Main {
      public static void main(String[] args) {
        Cell cells[][] = new Cell[10][10];
        
        for (int i=0; i<10; i++) {
          for (int j=0; j<10; j++) {
            
            if (i+j % 2 == 0) {
              cells[i][j] = new MineProxy(i, j);
            } else {
              cells[i][j] = new EmptyProxy(i, j);
            }
          }
        }
        
        for (int i=0; i<10; i++) {
          for (int j=0; j<10; j++) {
            cells[i][j].open();
          }
        }
      }
    }

    * This source code was highlighted with Source Code Highlighter.
    Поделиться публикацией

    Похожие публикации

    Комментарии 11

      0
      спасибо за материал. небольшое предложение — обращаться для получения proxy не напрямую, а через метод «ленивой инициализации» — getProxy(), тогда не надо будет везде вставлять проверку if (proxy == null)
        +2
        хотя нет, не внимательно посмотрел код, так что мое предложение неверно
        +4
        Мне кажется, очень мало раскрыто по использованию данного шаблона. А ведь это один из самых популярных в EJB-технологии. По сути все EJB сделаны в той или иной мере на основе данного подхода.

        1) Proxy также (и, наверно, в основном) используются для оборачивания основного объекта некоторой оболочкой, контролирующей его работу. Например, оборачиванием каждого вызова в проверку и открытие транзакции, включение профайлинга и т.д., проверку наличия security credentials и прав доступа на объект. При этом метод исходного объекта, обычно, вызывается «без купюр» — то есть не происходит модификации аргументов вызова или результатов (кроме уже их оборачивания в прокси).

        Кстати, использование proxy == null / getProxy() является примером т.н. LazyProxy, являющимся ещё одним шаблоном проектирования. Самый известный (с моей точки зрения) пример использования — Lazy-инициализация бинов в Hibernate.

        2) Создание Proxy в простейшем случае делается с помощью классов Proxy / InvokationHandler, которые позволяют создать прокси для любого набора интерфейсов. Однако часто приходится оборачивать не интерфейсы (EJB 2.x — это были бы Remote/Local интерфейсы), а классы (EJB 3.x). В этом случае используются фреймворки вроде CGLIB / ASM / Javaassist для реализации шаблона Proxy через наследование, т.е. через создание дочернего класса и оборачивания всех super-вызовов нужным кодом.

        Это всё, конечно, в случае необходимости оборачивания Proxy в runtime, когда класс на момент компиляции не знает, что придётся оборачивать. И хотя при этом страдает производительность, данный подход имеет плюсы и в случае статической компиляции — если мы добавим новый метод в исходный объект, в «динамических» прокси нам менять ничего не нужно, а вот «статические» придётся дописывать.

        3) Proxy бывают и нелокальные, например, предназначенные для вызова методов объекта в удалённой JVM. Обычно реализуются через Proxy / InvokationHandler, так как у объекта есть RemoteInterface.
          –4
          так вот почему проги на яве так тормозят…
          –2
          Частенько юзаю в MVC эту концепцию, давая view'у вместо исходной модели proxy модель.
            0
            > Необходимо контролировать доступ к объекту, не изменяя при этом, поведение клиента.

            Вторая запятая явно лишняя
              –2
              Да и вообще в тексте куча лишних запятых. Кошмар!!!
                0
                Да где же? Можно конкретнее?
                0
                Вторую запятую убрал :)
                –3
                Мотивацией для этого служит ряд приобретаемых преимуществ.


                Перевод очень «топорный».

                Идея паттерна «Заместитель» заключается в предоставлении клиенту другого объекта (заместителя), взамен объекту с контролируемым доступом


                Да и покрытие темы поверхностно.

                Вообще мне как-то приходила в голову идея, похожая на прокси шаблон для сокрытия некоторых методов класса и частичного ограничения на параметры других методов (админ может менять редактировать сообщение, пользователь не может). Сейчас узнал, что это называется защищающий прокси =)

                П.С.: Интереса ради глянул как написано в русской вике:
                Поскольку интерфейс «Реального Субъекта» идентичен интерфейсу «Субъекта», так, что «Заместителя» можно подставить вместо «Реального Субъекта», контролирует доступ к «Реальному Субъекту», может отвечать за создание или удаление «Реального Субъекта».


                тоже жесть =)
                  0
                  Перевод очень «топорный».

                  Это не перевод :) Это написано от себя лично, естественно после прочтения GoF, поэтому общие нотки прослеживаются.

                  Да и покрытие темы поверхностно.

                  Мне кажется, что моя интерпретация более понятна, чем на википедии. Хотя судить Вам.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое