Разделение объектов (Object decoupling)


Оба варианта Proxy и State предоставляют суррогатный класс, который вы используете в вашем коде; реальные классы, которые делают работы, спрятаны позади суррогатных классов. Когда вы вызываете метод суррогатного класса, он просто поворачивается назад и вызывает метод в реализуемом классе. Эти два шаблона очень похожи, так что Proxy - это просто особый случай State. Каждый испытывает желание объединить эти два шаблона вместе в шаблон, называемы Surrogate, но термин "proxy" имеет давнее и специализированное значение, которое, вероятно, объясняет причину появления двух шаблонов.

Основная идея проста: из базового класса суррогатный класс наследуется наряду с классом или классами, которые предоставляют реальную реализацию:

Object decoupling

Когда создается суррогатный объект, ему передают реализацию, которой он будет посылать все вызовы методов.

Структурное различие между Proxy и State очень просто: Proxy имеет только одну реализацию, в то время как State имеет более одной реализации. Приложения, использующие шаблоны (в Design Patterns), различаются: Proxy используется для доступа к своей реализации, в то время как State позволяет вам менять реализацию динамически. Однако, если вы расширите свое понимание "контроль за доступом к реализации", то эти два шаблона четко совпадут.

Proxy: выдвижение вперед другого объекта

Если мы реализуем Proxy, как показано на приведенной выше диаграмме, это будет выглядеть так:

//: proxy:ProxyDemo.java
// Простейшая демонстрация шаблона Proxy.
package proxy;

import junit.framework.*;

interface ProxyBase {
  
void f();
  
  
void g();
  
  
void h();
}

class Proxy implements ProxyBase {
  
private ProxyBase implementation;
  
  
public Proxy() {
     
implementation = new Implementation();
  
}
  
  
// Передаем вызовы методов в реализацию (implementation):
  
public void f() {
     
implementation.f();
  
}
  
  
public void g() {
     
implementation.g();
  
}
  
  
public void h() {
     
implementation.h();
  
}
}

class Implementation implements ProxyBase {
  
public void f() {
     
System.out.println("Implementation.f()");
  
}
  
  
public void g() {
     
System.out.println("Implementation.g()");
  
}
  
  
public void h() {
     
System.out.println("Implementation.h()");
  
}
}

public class ProxyDemo extends TestCase {
  
Proxy p = new Proxy();
  
  
public void test() {
     
// Здесь просто убеждаемся, что это работает
      // без выбрасывания исключений.
     
p.f();
      p.g
();
      p.h
();
  
}
  
  
public static void main(String args[]) {
     
junit.textui.TestRunner.run(ProxyDemo.class);
  
}
}
// /:~

Конечно, необязательно, чтобы реализация Implementation имела такой же интерфейс, как и Proxy; до тех пор, пока Proxy - это нечто, "говорящее за" класс, которому перенаправляет вызовы, но основная идея отражена верно (обратите внимание, что это утверждение конфликтует с определением Proxy в GoF). Однако, это достаточно последовательно, иметь общий интерфейс, так что класс Implementation вынужден реализовать все те же методы, которые вызывает Proxy.

PoolManager с использованием Proxy

//: proxy:PoolManager.java
package proxy;

import java.util.*;

public class PoolManager {
  
private static class PoolItem {
     
boolean inUse = false;
      Object item;
     
      PoolItem
(Object item) {
        
this.item = item;
     
}
   }
  
  
public class ReleasableReference { // Используется для построения прокси
     
private PoolItem reference;
     
private boolean released = false;
     
     
public ReleasableReference(PoolItem reference) {
        
this.reference = reference;
     
}
     
     
public Object getReference() {
        
if (released)
           
throw new RuntimeException(
                 
"Tried to use reference after it was released");
        
return reference.item;
     
}
     
     
public void release() {
        
released = true;
         reference.inUse =
false;
     
}
   }
  
  
private ArrayList items = new ArrayList();
  
  
public void add(Object item) {
     
items.add(new PoolItem(item));
  
}
  
  
// Отличие (улучшение?) подхода в запуске вне элементов:
  
public static class EmptyPoolItem {
   }
  
  
public ReleasableReference get() {
     
for (int i = 0; i < items.size(); i++) {
        
PoolItem pitem = (PoolItem) items.get(i);
        
if (pitem.inUse == false) {
           
pitem.inUse = true;
           
return new ReleasableReference(pitem);
        
}
      }
     
// Получи ошибку, как только попробуем сделать преобразование:
      // return new EmptyPoolItem();
     
return null; // временно
  
}
}
// /:~
//: proxy:ConnectionPoolProxyDemo.java
package proxy;

import junit.framework.*;

interface Connection {
  
Object get();
  
  
void set(Object x);
  
  
void release();
}

class ConnectionImplementation implements Connection {
  
public Object get() {
     
return null;
  
}
  
  
public void set(Object s) {
   }
  
  
public void release() {
   }
// никогда не вызывается напрямую
}

class ConnectionPool { // синглетон
  
private static PoolManager pool = new PoolManager();
  
  
private ConnectionPool() {
   }
// Предотвращаем синтез конструктора
  
  
public static void addConnections(int number) {
     
for (int i = 0; i < number; i++)
        
pool.add(new ConnectionImplementation());
  
}
  
  
public static Connection getConnection() {
     
PoolManager.ReleasableReference rr = (PoolManager.ReleasableReference) pool
            .get
();
     
if (rr == null)
        
return null;
     
return new ConnectionProxy(rr);
  
}
  
  
// Proxy, как вложенный класс:
  
private static class ConnectionProxy implements Connection {
     
private PoolManager.ReleasableReference implementation;
     
     
public ConnectionProxy(PoolManager.ReleasableReference rr) {
        
implementation = rr;
     
}
     
     
public Object get() {
        
return ((Connection) implementation.getReference()).get();
     
}
     
     
public void set(Object x) {
         ((
Connection) implementation.getReference()).set(x);
     
}
     
     
public void release() {
        
implementation.release();
     
}
   }
}

public class ConnectionPoolProxyDemo extends TestCase {
  
static {
     
ConnectionPool.addConnections(5);
  
}
  
  
public void test() {
     
Connection c = ConnectionPool.getConnection();
      c.set
(new Object());
      c.get
();
      c.release
();
  
}
  
  
public void testDisable() {
     
Connection c = ConnectionPool.getConnection();
      String s =
null;
      c.set
(new Object());
      c.get
();
      c.release
();
     
try {
        
c.get();
     
}
     
catch (Exception e) {
        
s = e.getMessage();
         System.out.println
(s);
     
}
     
assertEquals(s, "Tried to use reference after it was released");
  
}
  
  
public static void main(String args[]) {
     
junit.textui.TestRunner.run(ConnectionPoolProxyDemo.class);
  
}
}
// /:~

Динамический прокси (Dynamic Proxy)

В JDK 1.3, бал введен Динамический Прокси. Хотя сначала это немного сложновато, это интригующий инструмент.

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

// proxy:DynamicProxyDemo.java
// not work in JDK 1.4.1_01
package proxy;

import java.lang.reflect.*;

interface Foo {
  
void f(String s);
  
  
void g(int i);
  
   String h
(int i, String s);
}

public class DynamicProxyDemo {
  
public static void main(String[] clargs) {
     
Foo prox = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
           
new Class[] { Foo.class }, new InvocationHandler() {
              
public Object invoke(Object proxy, Method method, Object[] args) {
                 
System.out.println("InvocationHandler called:" + "ntMethod = " + method);
                 
if (args != null) {
                    
System.out.println("targs = ");
                    
for (int i = 0; i < args.length; i++)
                       
System.out.println("tt" + args[i]);
                 
}
                 
return null;
              
}
            })
;
      prox.f
("hello");
      prox.g
(47);
      prox.h
(47, "hello");
  
}
}
// /:~
Упражнение: Используя динамический Java-прокси создайте объект, который взаимодействует с простым файлом конфигурации. Например, в файле good_stuff.txt получите следующие записи:
a=1
b=2
c="Hello World"

Чтобы клиентский программист такого NeatPropertyBundle мог потом написать:

NeatPropertyBundle p = new NeatPropertyBundle("good_stuff");
System.out.println
(p.a);
System.out.println
(p.b);
System.out.println
(p.c);

Конфигурационный файл может содержать что угодно, с любыми именами переменных. Динамический прокси либо берет название из карты, либо говорит так или иначе, что это элемент не существует (вероятно, вернув null). Если вы установите параметр, который еще не существует, динамический прокси создаст новый элемент. Метод toString() должен отображать все существующие в настоящий момент элементы.

Упражнение: аналогично предыдущему, используя динамический Java-прокси создайте подключение в файлу DOS Autoexec.bat. Упражнение: Получите SQL запрос, который возвращает данные, затем прочтите метаданные базы. Теперь, для каждой записи предоставьте объект, который имеет атрибуты, соответствующие именам колонок и имеют соответствующий тип данных. Упражнение: Создайте простейший сервер и клиента, который использует XML-RPC. Каждый объект, возвращаемый клиентом, должен использовать концепцию динамического прокси для использования удаленных методов.

Andrea пишет:

Я не совсем уверен насчет упражнений, которые вы предлагаете, за исключением последнего. Дело в том, что я привык думать о привлечении обработчика, как о чем-то, обеспечивающем способность, ортогональную к тем, которые обеспечивает объект позади "прокси".

Другими словами: реализация вызова обработчика полностью не зависит от интерфейса(сов) объекта, который представлен динамически сгенерированным прокси. Это значит, что как только вы реализуете обработчик, вы можете использовать для любого класса этот публичный интерфейс, даже для классов и интерфейсов, которых не было во время реализации обработчика.

Таким образом, обработчик предоставляет службу, которая ортогональна службе, предоставляемой проксируемым объектом. Ричард (Richard) создал несколько обработчиков в своем примере SmartWord, и они, я думаю, являются лучшими обработчиками повторного вызова. В основном они передают вызов в реальный объект, и если вызов генерирует исключение, он ждет некоторое время, а замет делает это же вызов еще раз, и так три раза. Если все три вызова заканчиваются неудачей, то возвращается исключение. И вы можете использовать такой обработчик _любого_ класса.

Implementation - слишком сложный способ, который вы хотите продемонстрировать. Я использую этот пример только для объяснения, что я понимаю под ортогональными службами.

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

Состояние (State): изменение поведения объектов

Объект, который проявляет себя в изменении своего класса.

Указание: условный код почти для всех методов.

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

//: state:KissingPrincess.java
package state;

import junit.framework.*;

class Creature {
  
private boolean isFrog = true;
  
  
public void greet() {
     
if (isFrog)
        
System.out.println("Ribbet!");
     
else
        
System.out.println("Darling!");
  
}
  
  
public void kiss() {
     
isFrog = false;
  
}
}

public class KissingPrincess extends TestCase {
  
Creature creature = new Creature();
  
  
public void test() {
     
creature.greet();
      creature.kiss
();
      creature.greet
();
  
}
  
  
public static void main(String args[]) {
     
junit.textui.TestRunner.run(KissingPrincess.class);
  
}
}
// /:~

Однако, метод greet() и любой другой метод, который должен проверять переменную isFrog прежде, чем выполнит свою операцию, будет заключать в себе неуклюжий код. При делегировании операции объекту State, объект может быть изменен, что упростит код.

//: state:KissingPrincess2.java
package state;

import junit.framework.*;

class Creature {
  
private interface State {
     
String response();
  
}
  
  
private class Frog implements State {
     
public String response() {
        
return "Ribbet!";
     
}
   }
  
  
private class Prince implements State {
     
public String response() {
        
return "Darling!";
     
}
   }
  
  
private State state = new Frog();
  
  
public void greet() {
     
System.out.println(state.response());
  
}
  
  
public void kiss() {
     
state = new Prince();
  
}
}

public class KissingPrincess2 extends TestCase {
  
Creature creature = new Creature();
  
  
public void test() {
     
creature.greet();
      creature.kiss
();
      creature.greet
();
  
}
  
  
public static void main(String args[]) {
     
junit.textui.TestRunner.run(KissingPrincess2.class);
  
}
}
// /:~

Кроме того, изменения в State везде происходят автоматически, и не требуют редактирования во всех методах класса, чтобы изменения вступили в силу.

Вот основная структура State:

//: state:StateDemo.java
// Simple demonstration of the State pattern.
package state;

import junit.framework.*;

interface State {
  
void operation1();
  
  
void operation2();
  
  
void operation3();
}

class ServiceProvider {
  
private State state;
  
  
public ServiceProvider(State state) {
     
this.state = state;
  
}
  
  
public void changeState(State newState) {
     
state = newState;
  
}
  
  
// Передача вызовов методов в реализацию:
  
public void service1() {
     
// ...
     
state.operation1();
     
// ...
     
state.operation3();
  
}
  
  
public void service2() {
     
// ...
     
state.operation1();
     
// ...
     
state.operation2();
  
}
  
  
public void service3() {
     
// ...
     
state.operation3();
     
// ...
     
state.operation2();
  
}
}

class Implementation1 implements State {
  
public void operation1() {
     
System.out.println("Implementation1.operation1()");
  
}
  
  
public void operation2() {
     
System.out.println("Implementation1.operation2()");
  
}
  
  
public void operation3() {
     
System.out.println("Implementation1.operation3()");
  
}
}

class Implementation2 implements State {
  
public void operation1() {
     
System.out.println("Implementation2.operation1()");
  
}
  
  
public void operation2() {
     
System.out.println("Implementation2.operation2()");
  
}
  
  
public void operation3() {
     
System.out.println("Implementation2.operation3()");
  
}
}

public class StateDemo extends TestCase {
  
static void run(ServiceProvider sp) {
     
sp.service1();
      sp.service2
();
      sp.service3
();
  
}
  
  
ServiceProvider sp = new ServiceProvider(new Implementation1());
  
  
public void test() {
     
run(sp);
      sp.changeState
(new Implementation2());
      run
(sp);
  
}
  
  
public static void main(String args[]) {
     
junit.textui.TestRunner.run(StateDemo.class);
  
}
}
// /:~

В функции main( ) вы можете увидеть, что сначала используется первая реализация, замет происходит переключение на вторую реализацию, которая тоже используется.

Существует несколько деталей, из которых вы должны выбрать в соответствии с требованиями вашей собственной реализации, такие как: что фактически вы предоставляете клиенту, использующему Состояние, и как будут сделаны переключения состояния. Иногда (как в свинговом менеджере компоновки) клиент может передать объект напрямую, но в KissingPrincess2.java фактически Состояние использовалось так, что клиент его не видел. Кроме того, механизм изменения состояния может быть простым или сложным - в Машине Состояний, описанной далее в этой книге, есть большое число состояний и рассмотрены различные механизмы переключения.

Свинговый менеджер компоновки, упомянутый выше в качестве примера, - это интересный пример, потому что он показывает поведения и Стратегии (Strategy) и Состояния (State).

Различие между Proxy и Состоянием (State) состоит в проблемах, которые они решают. Proxy чаще всего решает следующие проблемы, как указано в Design Patterns:

  1. Удаленный прокси. Эти прокси предназначены для объектов, расположенных в разном адресном пространстве. Удаленный прокси создается для вас автоматически при использовании RMI компилятором rmic, когда он создает заглушки и скелеты.

  2. Виртуальный прокси. Этот прокси обслуживает "ленивую инициализацию" для создания дорогостоящих объектов по требованию.

  3. Защитный прокси. Используется, когда вы не хотите, чтобы клиентский программист имел полный доступ к проксируемому объекту.

  4. Умная ссылка. Для добавления дополнительных операций при доступе к проксируемому объекту. Например, для отслеживания и хранения количества ссылок, которые удерживает определенный объект, чтобы реализовать идиому копирование при записи и предотвратить наложение объектов. Более простым примером является отслеживание количества вызовов определенного метода.

Вы можете рассматривать ссылку, как некоторого рода защитный прокси, так как она управляет доступом к реальному объекту в куче (и проверяет, например, что вы не используете null-ссылку).

[Переписать это: В Design Patterns Proxy и State не выглядят связанными друг с другом, потому что два этих шаблона (как я полагаю) даны с различными структурами. State, в частности, использует раздельную иерархию реализаций, а мне кажется, что в этом нет необходимости, до тех пор, пока реализация не выйдет из под вашего контроля (конечно это возможно, но если этот код полностью принадлежит вам, то я не вижу причин не получить выгоду из элегантности и полезности единственного базового класса). Кроме того, Прокси должен использовать тот же базовый класс для своей реализации, что и проксируемый объект, доступ к которому вы контролируете. Независимо от специфики, и Пркси и Состояние являются суррогатами, передающими вызов методов реализуемому объекту.]

Состояние может быть найдено повсеместно, поскольку это достаточно фундаментальная идея. For example, in Builder, the "Director" uses a backend Builder object to produce different behaviors.

Итераторы (Iterator): отделение алгоритмов от контейнеров

Александр Степанов (Alexander Stepanov) размышлял многие годы о проблеме общих техник программирования прежде чем создал STL (вместе с Dave Musser). Он пришел к заключению, что все алгоритмы определяются через алгебраические структуры - которые мы называем контейнерами.

В процессе создания он реализовал итераторы, являющиеся центральным моментом алгоритмов использования, поскольку они отделяли алгоритмы от контейнеров специфического типа, с которыми алгоритмы могли бы работать. Это означает, что вы можете описать алгоритм, не заботясь о соблюдении определенной последовательности операций. Более того, любой код, который вы пишите с использованием итераторов, не зависит от структуры данных, которой этот код манипулирует и, таким образом, ваш код более общий, и его легче использовать повторно.

Использование итераторов также переносит ваш код в реальность функционального программирования, требованиями которого является описание того, что делает программа на каждом этапе, вместо того, как она это делает. Таким образом, вы говорите "сортировка", а не описываете сортировку. Назначением STL в C++ было обеспечение основных программных подходов для C++ (насколько удачен был этот подход, оставим без внимания).

Если вы используете контейнеры Java (а писать код без их использования достаточно сложно), значит, вы используете итераторы - в форме Enumeration в Java 1.0/1.1 или Iterator в Java 1.2. Поэтому вы уже должны быть хорошо знакомы с основами их использования. Если это не так, обратитесь к Главе 9, Хранение ваших объектов, подраздел Итераторы в книге Thinking in Java, 2-я редакция (ее можно свободно скачать по адресу www.BruceEckel.com).

Поскольку контейнеры Java 2 полностью связаны с итераторами, они становятся великолепными кандидатами на общую/фундаментальную технику программирования. Эта глава исследует эту технику, преобразуя STL алгоритмы в Java для использования с библиотечными контейнерами из Java 2.

Итераторы с проверкой типа (type-safe)

В книге Thinking in Java, 2-я редакция я показал создания контейнера с проверкой типа, который принимал только объекты определенного типа. Читатель, Linda Pazzaglia, спросила, о другом явном компоненте с проверкой типа, об итераторе, который мог бы работать с основными контейнерами из java.util, но навязать ограничение, чтобы тип объекта итерации совпадал с определенным типом.

Хотя Java и включает механизм шаблонов, итератор такого сорта был бы выгоден с точки зрения того, что он мог бы вернуть объект определенного типа, вместо шаблонного итератора, который возвращает общий Object, или требует ручной доводки для каждого типа, который вы хотите итерировать. Я выберу предыдущий подход.

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

И наконец, как мы строим итератор? Мы не можем переписать существующие библиотечные классы Java, которые уже производят Enumeration'ы и Iterator'ы. Однако, мы можем использовать шаблон проектирования Декоратор (Decorator), и создать класс, который просто является оберткой Enumeration или Iterator, производимый контейнером, создать новый объект, который реализует поведение итератора (которое состоит, в данном случае, в выбрасывании RuntimeException, если обнаружен недопустимый тип), но с тем же самым интерфейсом, как и оригинальный Enumeration или Iterator, так что он может быть использован в том же самом месте (на самом деле вы можете сказать, что это шаблон Прокси (Proxy), но мне больше нравиться Декоратор (Decorator) по своему назначению). Ниже приведен код:

//: com:bruceeckel:util:TypedIterator.java
package com.bruceeckel.util;

import java.util.*;

public class TypedIterator implements Iterator {
  
private Iterator imp;
  
private Class type;
  
  
public TypedIterator(Iterator it, Class type) {
     
imp = it;
     
this.type = type;
  
}
  
  
public boolean hasNext() {
     
return imp.hasNext();
  
}
  
  
public void remove() {
     
imp.remove();
  
}
  
  
public Object next() {
     
Object obj = imp.next();
     
if (!type.isInstance(obj))
        
throw new ClassCastException("TypedIterator for type " + type
               +
" encountered type: " + obj.getClass());
     
return obj;
  
}
}
// /:~

Упражнения

  1. Создайте пример "виртуального прокси".
  2. Создайте пример прокси типа "Умной ссылки", в котором вы храните количество вызовов метода определенного объекта.
  3. Создайте программу, схожую с определенной системой DBMS, которая позволяет только определенное число одновременных соединений. Для реализации этого используйте систему, схожую с синглетоном, контролирующую число "соединений" созданного ей объекта. Когда пользователь заканчивает соединение, система должна быть проинформирована, таким образом она может проверять, что соединение может использоваться повторно. Для гарантии этого используйте прокси-объект вместо ссылки на реальное соединение, и разработайте прокси так, чтобы он возвращал соединение системе назад.
  4. Используя шаблон Состояния, создайте класс, называющийся UnpredictablePerson, который меняет варианты ответа при вызове метода hello() в зависимости от Настроения(Mood), в котором он находится. Добавьте дополнительный вид Настроения(Mood), называемое Prozac.
  5. Создайте простую реализацию копирования при записи.
  6. java,util.Map не имеет способов автоматической загрузки двумерного массива объектов в карту(Map), в качестве пар ключ-значение. Создайте класс-адаптер, который делает это.
  7. Создайте Построитель Адаптера (Adapter Factory), который динамически находит и производит адаптер, который вам нужно подсоединить к данному объекту с желаемым интерфейсом.
  8. Выполните предыдущее упражнение с использованием динамического прокси, который является частью стандартной библиотеки Java.
  9. Измените пример Пула Объектов (Object Pool) таким образом, чтобы объекты возвращались в пул автоматически спусте некоторое время.
  10. Измените предыдущее упражнение на использование "аренды" таким образом, чтобы клиент мог возобновить аренду объекта для предотвращения его от автоматического освобождения по таймеру.
  11. Измените систему Пула Объектов (Object Pool), чтобы использовалась система нитей (thread).