AOP@Work: Улучшенные шаблоны проектирования AspectJ, часть 1

АОП делает шаблоны более простыми, гибкими и легкими для повторного использования

Шаблоны проектирования давно стали частью набора инструментов опытных разработчиков. К сожалению, поскольку шаблоны могут воздействовать на несколько классов, они также могут быть навязчивыми и трудными для повторного использования. В этой состоящей из двух частей статье, третьей в серии AOP@Work, Николас Лесицки расскажет, как АОП решает эту проблему путем фундаментального преобразования реализации шаблона. Он исследует три классических шаблона проектирования Gang of Four (GoF) (Adapter, Decorator и Observer) и обсуждает практические преимущества и преимущества проектирования при их реализации с использованием аспектно-ориентированного программирования.

Nicholas Lesiecki, Инструктор по программированию, Google

Николас Лесицки (Nicholas Lesiecki) является известным экспертом по АОП и языку программирования Java. Он является соавтором книги "Освоение AspectJ" (Wiley, 2003) и членом AspectMentor, консорциума экспертов в аспектно-ориентированном программировании. Он выступает на тему использованию AspectJ для тестирования, создания шаблонов проектирования и решения реальных бизнес-проблем на таких встречах как SD West, OOPSLA, AOSD и в серии симпозиумов No Fluff Just Stuff. В настоящее время он работает в Google в качестве инженера-программиста и инструктора по программированию.



17.05.2005

Что такое шаблон проектирования? Согласно "Шаблоны проектирования: Элементы повторно используемого объектно-ориентированного программного обеспечения" (обычно называемого GoF; см. раздел "Ресурсы"):

Шаблон проектирования систематично именует, мотивирует и объясняет общий дизайн, предназначенный для решения повторяющихся проблем в объектно-ориентированных системах. Он описывает проблему, решение, а также то, когда применять решение и каковы его последствия. Он также предоставляет советы по реализации и примеры. Решение представляет собой общую конфигурацию объектов и классов, решающих проблему. Решение настраивается и реализуется для решения проблемы в конкретном контексте.

После многих лет успешного использования шаблонов для решения проблем в ОО-системах я обнаружил, что согласен с этим определением. Шаблоны были отличным способом поговорить с моими напарниками программистами о проекте, и они представляли наилучшие практические решения повторяющихся проблем проектирования. Поэтому я был немного шокирован, когда присутствовал при разговоре со Стюартом Хэловэем (Stuart Halloway), и он предложил альтернативный заголовок для GoF: "Обходные пути для того, что не сделано в С++". С его точки зрения, то что существует как "шаблон" в одном языке, может быть отнесено к категории самого языка в другой парадигме. Он привел пример Factories - полезных в языке программирования Java, но менее полезных в Objective-C, который позволяет возвращать подтипы из конструктора.

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

Так куда идет АОП? Для ООП мы имеем шаблоны GoF, которые дают нам последовательный, хотя иногда и громоздкий, способ работы с общими концепциями, такими как наблюдатели и декораторы. АОП построен на ООП для предоставления прямого способа выражения пересекающихся процессов. Это следует из того, что некоторые из шаблонов GoF реализуют пересекающиеся процессы и могут быть напрямую выражены в АОП. То есть, вы заметите, что некоторые шаблоны, содержащие множество классов, могут быть выражены в виде одного аспекта. Некоторые шаблоны станут проще, поскольку будут содержать меньше кода. Некоторые настолько хорошо поддерживаются, что почти исчезают. Другие строго привязаны к ООП (например, шаблоны, имеющие дело со структурами классов) и остаются не изменными при работе с АОП.

Об этом цикле статей

Цикл статей AOP@Work предназначен для разработчиков, обладающих некоторыми основными знаниями аспектно-ориентированного программирования и желающих расширить или углубить их (материалы по основам АОП можно найти в разделе "Ресурсы"). Как и большинство статей developerWorks, статьи этого цикла очень практичны: из каждой статьи вы получите новые знания, которые можно сразу же использовать.

Каждый из авторов статей этой серии был выбран из-за превосходных знаний или компетентности в АОП. Многие авторы являются участниками рассматриваемых в статьях проектов или инструментальных средств. Каждая статья рецензируется, чтобы гарантировать справедливость и точность выраженной в ней точки зрения.

Связывайтесь, пожалуйста, с авторами лично, чтобы прокомментировать их статьи или задать о них вопрос. Чтобы прокомментировать цикл статей в целом, вы можете связаться с ее ведущим Николасом Лесицки (Nicholas Lesiecki). Дополнительные материалы по АОП можно найти в разделе "Ресурсы".

В этой статье исследуется реализация шаблонов в АОП (в частности AspectJ). Я выбрал инструментарий шаблонов GoF из-за их широкой популярности и общего назначения. В этой части статьи я установлю некоторые критерии для анализа влияния шаблонов и перейду к рассмотрению шаблонов Adapter и Decorator. Adapter демонстрирует преимущества статического пересечения, а Decorator проявляет себя как исчезающий шаблон. Во второй части я предлагаю более глубокое изучение шаблона Observer, который не исчезает, а показывает главные преимущества при реализации в AspectJ. Во второй части показывается, как AspectJ разрешает конвертирование шаблонов в повторно используемые базовые аспекты, позволяя, таким образом, загрузить предкомпилированные библиотеки шаблонов - захватывающая перспектива для энтузиастов шаблонов.

Зачем применять АОП для проектирования шаблонов?

Я уже заявлял, что многие шаблоны являются пересекающимися, но конечно же я был не первый, кто об этом подумал. В недавнем исследовании были проанализированы шаблоны GoF и было определено, что 17 из 23 шаблонов демонстрируют некоторую степень пересечения. (Статья Яна Хэннемана (Jan Hannemann) и Грегора Кицзейлеза (Gregor Kiczales) "Реализация шаблонов проектирования в Java AspectJ", см. раздел "Ресурсы" для получения более детальной информации). Если АОП обещает помочь с пересекающимися процессами, то какую выгоду вы можете ожидать от использования шаблонов проектирования? Я начну отвечать на этот вопрос общими понятиями, а затем настрою среду разработки, в которой я буду рассматривать каждый шаблон проектирования.

Преимущества использования АОП в шаблонах проектирования

Первым ключевым преимуществом АОП является способность локализовать код данного шаблона проектирования. Это означает, что шаблон часто может быть реализован в одном аспекте или паре тесно связанных аспектов (в отличие от реализации языка Java, где приложение с шаблоном может распространиться по нескольким классам). Способность видеть весь код в единственном месте приводит к нескольким практическим выгодам. Во-первых, читатели кода могут более легко понять шаблон, если все его взаимодействия видимы в одном месте. Во вторых, если разработчики должны изменить реализацию шаблона, они могут сделать это изменение в одном месте, вместо того, чтобы искать фрагменты шаблона по всей системе. В-третьих, разработчики могут использовать понятные имена для описания аспекта, содержащего шаблон, обеспечивая таким образом текстовые подсказки относительно предназначения шаблона программистам, которые в дальнейшем будут поддерживать систему. Например, вы могли бы назвать аспект SensorAdapter, указывая, что применили шаблон Adapter к сенсорам.

Другое ключевое преимущество реализации шаблона AspectJ - забывчивость. Другими словами, классы, которые играют роль в шаблоне, не обязательно должны знать об этой роли. Эта выгода является прямым результатом локализации - поскольку шаблон локализован в аспекте, он не затрагивает его участников. Забывчивость делает код шаблона проще для понимания пользователями. Но не только это - забывчивость делает композицию шаблона намного проще. В Java-реализации, если класс участвует в нескольких шаблонах, его основное значение может быстро стать скрытым. Классы, которые не знают об их участии в шаблоне, могут также быть многократно использованы в других контекстах. Вы увидите конкретный пример этого в части 2 статьи.

Что такое пересечение?

Программы часто реализуют поведение, которое не помещается естественно в единственный модуль программы, или даже в несколько тесно связанных модулей программы. Использующее аспекты сообщество описывает этот тип поведения как пересечение, потому что этот процесс идет вразрез типичным разделам ответственности в данной модели программирования. Например, в ОО-программировании естественной единицей модульности является класс, и пересекающийся процесс является процессом, охватывающим несколько классов. Таким образом, если шаблон проектирования вносит поведение в три различных класса, можно сказать, что такой шаблон пересекает эти классы. Пересекающиеся процессы ведут к рассредоточению кода (связанный код не локализуется с другим связанным кодом) и к путанице в коде (связанный код находится рядом с несвязанным кодом). Такое рассредоточение и путаница мешают рассуждать о системе. Для освежения в памяти таких понятий АОП как пересекающиеся процессы обратитесь в раздел "Ресурсы".

Эти преимущества разрешают многократное использование некоторых шаблонов на уровне кода. Концепции и структура шаблонов проектирования всегда были повторно используемыми. Любой, кто хотел реализовать шаблон Observer, мог отложить книгу GoF и вставить шаблон в свой код. Но при использовании аспектно-ориентированного подхода вы можете уменьшить проблемы, загрузив аспект ObserverProtocol (доступен в проекте Design Patterns; см. раздел "Ресурсы"). Кроме облегчения реализации многократное использование на уровне кода также позволяет реализовать более тесную связь кода шаблона и документации. Например, я могу просмотреть javadocs для ObserverProtocol и понять его назначение и структуру без необходимости поиска отдельного учебника.

Среда для анализа

Каждое описание шаблона подчиняется общей структуре. Я начну с примерной проблемы и предоставлю общее описание шаблона. Затем я опишу, как реализовать шаблон сначала на языке Java, а затем на языке AspectJ. После каждой реализации я опишу то, что делает шаблон пересекающимся и какой эффект эта версия шаблона оказывает на понимание, поддержку, многократное использование и создание кода.

Щелкните значок Code вверху или внизу этой статьи (или перейдите в раздел "Загрузка"), чтобы загрузить все исходные тексты следующих примеров.


Шаблон Adapter

Первым шаблоном, который я рассмотрю детально, является Adapter. Adapter полностью посвящен совместимости. Шаблон позволяет классам взаимодействовать между собой, что в противном случае было бы невозможно из-за несовместимости интерфейсов. Для реализации Adapter в Java-коде вы заключаете класс (целевой класс) в специальный класс Adapter, который транслирует API целевого класса в тот, который ожидают пользователи, или в тот, который легче использовать.

Условие: Обеспечение агрегированного считывания данных датчиков

Допустим, вы проектируете космический корабль, и вас попросили обеспечить считывание всех критических датчиков. Поскольку вы расширяете существующую систему, для каждого датчика, к которому вы должны обратиться, уже существуют удобные классы Java. Например, можно обратиться к температурному датчику со следующим классом:

public class TemperatureGauge {

  public int readTemperature(){
    //доступ к внутренностям датчика
  }
}

К датчику радиации вы можете получить доступ следующим образом:

public class RadiationDetector {

  public double getCurrentRadiationLevel(){
    //чтение уровня радиации
  }
}

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

Readout:
Sensor 1 status is OK
Sensor 2 status is OK
Sensor 3 status is BORDERLINE
Sensor 4 status is DANGER!

Вы можете усовершенствовать экран, используя, например, следующий метод:

public static void main(String[] args){
    RadiationDetector radiationDetector = //поиск детектора
    TemperatureGauge gauge = //получение данных
    
    List allSensors = new ArrayList();
    allSensors.add(radiationDetector);
    allSensors.add(gauge);
    
    int count = 1;
    for (Sensor sensor : allSensors) {
      //Как прочитать каждый тип сенсора...?
    }
  }

Чем дальше, тем лучше, но как опросить каждый датчик без обращения к ужасным проверкам оператором if(sensor instanceof XXX)? Одним из вариантов является изменение каждого класса датчика, а именно добавление метода getStatus(), интерпретирующего прочитанные данные датчика и возвращающего String, как показано ниже:

if(this.readTemperature() > 160){
  return "DANGER";
}
return "OK"

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


Adapter языка Java

Традиционная реализация шаблона Adapter окружает каждый целевой класс классом, реализующим удобный API. В этом случае вы создали бы общий интерфейс под названием, скажем, StatusSensor, показанный здесь:

public interface StatusSensor {
  String getStatus();
}

Используя этот общий интерфейс вы могли бы реализовать чтение данных следующим образом:

  for (StatusSensor sensor : allSensors) {
    System.out.print("Sensor " + count++);
    System.out.println(" status is " + sensor.getStatus());  
  }

Единственным остающимся вопросом является соответствие каждого датчика интерфейсу. Классы Adapter достигают этого. Как вы можете увидеть в листинге 1, каждый Adapter хранит датчик, который помещается в переменной-члене и использует этот датчик при реализации метода getStatus:

Листинг 1. Классы Adapter и клиентский код
//Классы Adapter
public class RadiationAdapter implements StatusSensor {
  private final RadiationDetector underlying;

  public RadiationAdapter(RadiationDetector radiationDetector) {
    this.underlying = radiationDetector;
  }

  public String getStatus() {
    if(underlying.getCurrentRadiationLevel() > 1.5){
      return "DANGER";
    }
    return "OK";
  }
}

public class TemperatureAdapter implements StatusSensor {
  //...аналогично
}

//привязка кода для каждого датчика с его адаптером...
allSensors.add(new RadiationAdapter(radiationDetector));
allSensors.add(new TemperatureAdapter(gauge));

В листинге показан также "связующий код", который накладывает на каждый датчик соответствующий Adapter перед считыванием данных. Шаблон не требует, чтобы связующий код появлялся в каком-либо конкретном месте. Возможные месторасположения - "just after creation" и "just before use." В коде примера они помещаются прямо перед добавлением датчика к набору устройств индикации.

Анализ Adapter в языке Java

Шаблон Adapter существует достаточно комфортно в традиционной реализации. Что делает пересечение? Процесс "status" проходит через несколько различных классов датчиков. Если бы вы должны были разместить классы Adapter в пакете, ОО-реализация этого шаблона была бы достаточно модульной. Пакет стал бы "Модулем Adapter". Идиома наложения защищает датчики от знаний о шаблоне, что ведет к ослаблению связей. К сожалению, часть приложения, которое сделало наложение, должна будет знать и об Adapters и о датчиках, к которым они обращались. Таким образом, местоположение связующего кода тоже пересекалось бы шаблоном.

А теперь применим к Adapter на языке Java мои критерии оценки:

  • Понимание: Так называемые SensorAdapters, расположенные в пакете, делают намерения этого шаблона понятными. К сожалению, связующий код может быть расположен далеко от пакета Adapter. Так как область связующего кода не структурирована, при попытке понять шаблон вы можете либо пропустить запутанный код, либо попытаться в нем разобраться.
    Вы должны также побеспокоиться о работе по идентификации объекта. То есть, если в системе существуют версии одного и того же объекта, на которые было или не было применено наложение, вы должны выяснить, будут ли они эквивалентными.
  • Повторное использование: Для повторного использования этого шаблона вы должны реализовать его с нуля.
  • Поддержка: При добавлении новых датчиков к системе вы должны добавить новые классы адаптеров и обновить связующий код, охватывающий их.
  • Композиция: Допустим, что вы желаете привлечь датчики из другого шаблона. Датчики не знают об Adapter, поэтому будут не тронуты. Однако это палка о двух концах. Адаптированную или не адаптированную версию датчика должен рассматривать новый шаблон в качестве своего объекта?

AspectJ Adapter

Как и другие шаблоны проектирования AspectJ-реализация Adapter сохраняет назначение и концепции своих предшественников. Реализация использует объявления intertype - важный тип поддержки пересекающихся процессов, о котором говорят меньше, чем о pointcut и advice. Если вам нужно освежить знания о статических пересекающихся процессах, обратитесь к разделу "Ресурсы".

Как и в чистой ООП-версии, АОП-версия Adapter требует наличия интерфейса StatusSensor. Однако, вместо использования отдельных "накладывающихся" (wrapper) классов, AspectJ-версия использует форму declare parents для прямого внедрения StatusSensor в различные датчики, что показано ниже:

public aspect SensorAdapter {

  declare parents : 
    (TemperatureGauge || RadiationDetector) 
    implements StatusSensor;
}

Теперь датчики должны соответствовать интерфейсу. Но они еще не реализуют интерфейс (о чем радостно сообщит вам компилятор AspectJ). Для завершения реализации шаблона вы должны добавить объявления методов intertype в аспект, для того чтобы реализовать соответствие датчиков. В приведенном ниже коде метод getStatus() добавлен к классу TemperatureGauge:

  public String TemperatureGauge.getStatus(){
    if(this.readTemperature() > 160){
      return "DANGER";
    }
    return "OK";
  }

AspectJ-версия класса чтения данных выглядит так же, как версия, реализованная на Java, за исключением того, что для наложения на датчики не нужен связующий код. Каждый датчик "оборачивает" себя сам.

Изменение кода сторонних разработчиков

Проницательные читатели вероятно вспомнят, что в условиях примера я упоминал, что один из датчиков приобретен у сторонних поставщиков и что этот факт не позволит вам просто добавить метод класса в реализации на языке Java. Напротив, компилятор AspectJ предоставляет возможность воздействовать на компилируемый байткод, для того чтобы разрешить разработчикам применять аспекты к более широкому диапазону классов. Добавление этапа компилирования, в котором аспект SensorAdapter вплетается в jar-файл стороннего поставщика, позволяет эффективно использовать адаптированную версию класса.

Некоторые разработчики могут беспокоиться о порождениях модифицированного байткода без владения соответствующим исходным кодом. В некоторых ситуациях такой вид модификации нарушает условия лицензии (например, хорошо известно, что модификация классов, предоставленных Sun Microsystems, нарушает соответствующую лицензию). В других случаях создание активных изменений в классе без твердого понимания его внутренней организации может привести к трудноуловимым ошибкам. В этом случае, однако, модификации служат только для дополнения функциональных возможностей целевого класса и взаимодействуют только с его открытым (public) интерфейсом. Предполагая, что поставщик шаблона RadiationDetector не запрещает модификацию его классов, не существует причин беспокоиться об АОП-версии Adapter в большей степени, чем об ООП-версии.

Анализ AspectJ Adapter

Ключевым преимуществом реализации Adapter в AspectJ является локализация. Шаблон полностью содержится внутри одного аспекта, а не в двух (или более) отдельных классах адаптеров и неструктурированной области оболочки. Применим к AspectJ–реализации мои критерии оценки:

  • Понимание: Отсутствие "наложения" устраняет необходимость (для понимания шаблона) смотреть на что-либо еще, кроме аспекта Adapter. Устраняется также необходимость иметь дело с вопросами тождественности объектов.
  • Повторное использование: AspectJ-версия является повторно используемой не больше и не меньше, чем другие версии.
  • Поддержка: Поскольку для каждого нового датчика нужно лишь написание метода (а не целого класса), расширение реализации AspectJ должно быть немного более простым. По мере роста числа адаптеров или усложнении логики, требуемой для каждой адаптации, вы можете обнаружить, что один аспект становится чрезмерно большим. В этом случае вы можете разбить аспект на несколько более маленьких аспектов. При делении аспекта теряются некоторые из преимуществ локализации, но сохраняются другие преимущества.
  • Композиция: Вы легко можете создать несколько шаблонов, поскольку нет проблемы координации "наложений".

Итог: и Java, и AspectJ реализации выполняют хорошую работу, для того чтобы оставаться в стороне от классов датчиков. Однако только AspectJ-версия остается в стороне от вашего приложения. Является ли это главным преимуществом? Ответ, вероятно зависит от того, проявляет ли ваше приложение какое-либо усложненное свойство, описанное в исследованиях. Если бы в проекте я использовал AspectJ, то определенно применил бы его для реализации Adapter, хотя и не привлек бы AspectJ для решения лишь только этой одной проблемы. Следующий шаблон, Decorator, предоставляет несколько более значительные преимущества.


Шаблон Decorator

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

Шаблон Decorator предназначен для динамического добавления функциональных возможностей к существующему объекту. Канонический пример в книге GoF рассматривает литеральную декорацию. В их примере на класс GUI-компонента накладывается класс Decorator, добавляющий границу, или, возможно, линии прокрутки, для отображения компонента. Java Class Libraries в некоторой степени обеспечивает Decorator, например, методы в java.util.Collections, которые оформляют Collection так, что она становится unmodifiable или synchronized, а также богатый набор потоков ввода/вывода, которые могут буферизировать, наполнять или контролировать другие потоки.

Условие: Контроль чтений файла

Для придания остроты этому примеру я решил выбрать Decorator из дистрибутива Java и посмотреть, чего будет стоить репликация его в AspectJ. Таким декоратором, который показался мне интригующим, был ProgressMonitorInputStream из javax.swing. Согласно документации ProgressMonitorInputStream контролирует ход процесса чтения в зависимом входном потоке.

Чтобы продемонстрировать декоратор в действии, я написал простой GUI, который читает файл. Вы можете рассмотреть код для чтения входного потока в листинге 2 и копии экрана работающего приложения на рисунке 1. (Вы можете также поработать с полным исходным кодом примера, нажав на пиктограмму Code, расположенную вверху или внизу этой страницы.)

Рисунок 1. ProgressMonitor для потока
ProgressMonitor для потока

Вы, возможно, захотите иметь под рукой javadocs или даже исходный код java.io и ProgressMonitorInputStream, ссылки на которые находятся в разделе "Ресурсы".


Decorator на языке Java

В языке программирования Java вы сталкиваетесь с шаблоном Decorator при первом создании интерфейса (или класса) AbstractComponent, которому могут соответствовать и базовые реализации (называемые иногда ConcreteComponents) и декораторы. В этом примере AbstractComponent - это java.io.InputStream, определяющий интерфейс как для FileInputStream (ConcreteComponent), так и для BufferedInputStream (ConcreteDecorator).

Хотя это и не является необходимостью, реализации Decorator часто предоставляют AbstractDecorator, который содержит ссылку на декорируемый компонент и обеспечивает основные механизмы декорирования без добавления какого-либо дополнительного поведения. В java.io такую функциональность обеспечивает FilterInputStream. Наконец, ConcreteDecorator расширяет AbstractDecorator, переопределяет методы, требующие декорирования, и добавляет поведение до и после вызова того же метода декорируемого компонента. В данном случае ProgressMonitorInputStream работает с ConcreteDecorator.

Глядя на реализацию ProgressMonitorInputStream фирмы Sun (который я не буду здесь перепечатывать из-за лицензионных ограничений), вы можете увидеть, что он создает экземпляр javax.swing.ProgressMonitor. После каждого метода read,он обновляет монитор и значение счетчика количества считанных байт из зависимого потока. Отдельный класс ProgressMonitor определяет, когда открыть контролирующее диалоговое окно, и обновляет видимый экран.

Для использования ProgressMonitorInputStream вам необходимо просто наложить его поверх другого входного потока (как в листинге 2) и сослаться на экземпляр этого потока при чтении. Обратите внимание на сходство между шаблонами Adapter и Decorator: оба требуют программной реализации дополнительного поведения в целевом классе.

Листинг 2. Мониторинг InputStream
private void actuallyReadFile() {
  try {

    InputStream in = createInputStream();
    byte[] b =new byte[1000];
    while (in.read(b) != -1) {
      //выполнить какие-либо действия
      bytesRead+=1000;
    }
    bytesReadLabel.setText("Read " +  (bytesRead/1000) + "k");
    bytesRead = 0;
    in.close();
  } catch (Exception e) {
    //обработка...
  }
}


private InputStream createInputStream() throws FileNotFoundException   
{
  InputStream stream = new FileInputStream(name.getText());
  stream = new BufferedInputStream(stream);
  
  //_this_ - это компонент  JPanel GUI 
  stream = new ProgressMonitorInputStream(
       this, "This is gonna take a while", stream);
  return stream;
}

Анализ Java Decorator

Глядя на этот пример, можно подумать, что не может быть ничего легче, чем использование шаблона Decorator. Однако не забывайте о том, что для того, чтобы этот пример работал, Sun реализовала InputStream, FilterInputStream и ProgressMonitorInputStream - нетривиальное количество кода.

В этом примере предмет мониторинга пересекает InputStream. В более общем смысле, декорация пересекает цель декорации. Более того, предмет декорации может пересекать приложение. Например, пользователи могут требовать ProgressMonitor для всех файловых операций чтения. (Чтобы вы не думали, что это искусственный пример, спросите себя, сколько раз вы использовали входные потоки без их буферизации.)

Теперь взглянем на оставшиеся критерии:

  • Понимание: Зная, что Decorator работает, его довольно легко освоить. Но я никогда не забуду о смятении, которое ощутил, когда в первый раз открыл java.io и попытался разобраться в изобилии структурных классов, формирующих Decorator применительно к потокам. И хотя краткий курс мог бы легко прояснить ситуацию, не существует простого пути освоения шаблонов, только лишь просматривая код. Более конкретный измеритель сложности освоения – это строки кода. Я посмотрю на счетчик строк после исследования реализации AspectJ. Стоит отметить, что из-за использования наложения Decorator испытывает те же проблемы идентификации объектов, что и Adapter.
  • Повторное использование: Для повторного использования этого шаблона вы должны заново реализовать его.
  • Поддержка: Существует два ключевых сценария поддержки. В первом вы добавляете новый декоратор к существующей реализации. В зависимости от количества методов в Decorator эта процедура может быть трудоемкой, но не тяжелой. Во втором сценарии вы добавляете новую операцию в AbstractComponent (то есть, InputStream). Это означает обновление всех существующих декораторов для реакции на новую операцию и принятия решения в каждом из них о том, должна ли быть применена декорация к новому методу.
  • Компоновка: Поскольку декораторы и компонент разделяют общий интерфейс, Decorator разрешает прозрачную компоновку декораторов на данном экземпляре. (Просто взгляните на листинг 2, в котором код буферизирует и контролирует входной поток.) Это очень хорошо, особенно из-за того, что декорируемые цели не обязаны знать об этой операции.

AspectJ Decorator

В своей работе Хэннеман и Кицзейлз утверждают:

При использовании AspectJ, реализация некоторых шаблонов полностью исчезает, поскольку конструкции языка AspectJ реализуют их напрямую. Это относится к [Decorator].

Глядя на раздел Motivation для шаблона Decorator в книге GoF, становится очевидным, почему это становится возможным:

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

Как в конце концов выглядит advice? Ведь существует возможность прозрачно добавлять дополнительные "действия" к любой операции. В некотором смысле AspectJ позволяет декорировать любой метод. Чтобы увидеть, как это работает в реальной системе, вы можете исследовать контроль чтений входного потока в AspectJ.

Идентификация декорированных операций

Для корректной реализации контроля аспект должен идентифицировать операции чтения в потоке. Это решается написанием pointcut, который выбирает операции чтения:

pointcut arrayRead() :
    call(public int InputStream+.read(..));

Теперь вы можете применить некоторый advice следующей общей формы:

after() returning (int bytesRead) :
    arrayRead()
  {
    updateMonitor(bytesRead);
  }

Этот advice использует форму returning для получения возвращаемого значения из вызванного метода. Количество прочитанных байтов затем передается private методу аспекта: updateMonitor(). Этот метод заботится о деталях обновления реального ProgressMonitor (боле подробно об этом далее).

Экспонирование значимого контекста

Пока решение простое. Однако, как оказалось, класс ProgressMonitor требует немного большего для выполнения работы. А именно: он нуждается в GUI-компоненте для привязки к контролирующему диалоговому окну. Вы видели это требование в традиционных реализациях:

//это JPanel GUI-компонент 
stream = new ProgressMonitorInputStream(this, "This is gonna take a while", stream);

Для получения необходимого GUI-компонента аспект должен присоединить его в pointcut, так чтобы он мог быть использован в advice. Листинг 3 cодержит исправленные pointcut и advice. Обратите внимание, что pointcut fromAComponent() использует примитивный pointcut cflow(). По существу, pointcut говорит: "выбрать все точки соединения, возникающие в результате выполнения метода, в Jcomponent и экспонировать этот компонент для использования в advice."

Листинг 3. Перенос контекста на монитор при помощи cflow
pointcut arrayRead(JComponent component, InputStream is) :
    call(public int InputStream+.read(..)) && target(is)
    && fromAComponent(component);
  
  pointcut fromAComponent(JComponent component) : 
    cflow(execution(* javax.swing.JComponent+.*(..))
      && this(component));

after(JComponent component, InputStream is) returning (int bytesRead) :
  arrayRead(component, is)
{
  updateMonitor(component, is, bytesRead);
}

Поддержка состояния

Чтобы сделать аспект широко применимым (и точно подражающим другой реализации), аспект должен поддерживать состояние. То есть, он должен высветить для каждого контролируемого потока уникальный индикатор прогресса. AspectJ предлагает несколько вариантов. Лучшим выбором для данного аспекта, вероятно, является поддержка сохранения состояния для каждого объекта с использованием Map. Этот технический прием снова появится в моей реализации шаблона Observer, поэтому сделайте заметку! (Другие способы сохранения состояния, специфичного для объекта, включают объявления intertype и аспекты pertarget/perthis, но рассмотрение этих концепций выходит за рамки данной статьи.)

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

Метод updateMonitor() использует затем эту карту для "неторопливой" инициализации нового IncrementMonitor. После того, как метод убедится в существовании монитора, он обновляет его последней информацией о текущем состоянии операции чтения (указанной в возвращаемом значении read()). В листинге 4 приведен код для инициализации и обновлений индикатора прогресса, а также полный код IncrementMonitor:

Листинг 4. "Неторопливая" инициализация монитора потока
  //Из аспекта...
  private void updateMonitor(JComponent component, InputStream is,
           int amount){
    IncrementMonitor monitor = 
      (IncrementMonitor)perStreamMonitor.get(is);
    if(monitor == null){
      monitor = initMonitor(is, component);
    }
    monitor.increment(amount);
  }
  
  private IncrementMonitor initMonitor(InputStream is,
                 JComponent component){
    try {
      int size = is.available();
      IncrementMonitor monitor = 
        new IncrementMonitor(component, size);
      perStreamMonitor.put(is, monitor);
      return monitor;
    } catch (Exception e) {
      //...обработка
    }
  }
  
}//...конец аспекта

public class IncrementMonitor extends ProgressMonitor{

  private int counter;
  
  public IncrementMonitor(Component component, int size){
    super(component, "Some Title", null, 0, size);
  }
  
  public void increment(int amount){
    counter += amount;
    setProgress(counter);
  }
}

Наконец, аспект должен отказаться от монитора, после того как поток полностью прочитан. Если вы сейчас думаете об аспектах, то догадаетесь о возможном действии. InputStream определяет удобный метод close() аспекта для сообщения о закрытии:

before(InputStream is): 
    call(public void InputStream+.close())
    && target(is)
  {
    System.out.println("Discarding monitor.");
    perStreamMonitor.remove(is);
  }

К этому моменту вы завершили упражнение. Однако, если вы знакомы с реализацией InputStream, то обнаружите кое-что, о чем я преднамеренно умолчал. Метод read() (без параметров) должен обрабатываться не так, как другие методы read, поскольку его возвращаемое значение не количество прочитанных байт, а следующий байт в потоке. Код примера, сопровождающего статью, расширяет аспект для преодоления этого ограничения, но я советую вам подумать, как бы вы решили этот вопрос, если бы писали аспект до обращения к исходному коду.

Анализ AspectJ Decorator

Аналогично шаблону Adapter две реализации шаблона Decorator отличаются в локализации. В версии AspectJ шаблон полностью находится внутри одного аспекта. (Я исключил вспомогательный (helper) класс, поскольку он не играет структурной роли в шаблоне.) В версии языка Java реализация базового шаблона (остающаяся вне клиентского кода) требует трех классов. Какой эффект это дает?

  • Понимание: Благодаря мощи языка pointcut AspectJ, аспект может воздействовать на несколько операций одним advice. В противном случае класс декоратора должен повторять поведение в каждой операции. Частично из-за этого реализация фирмы Sun содержит более чем в два раза большее количество строк кода по сравнению с реализацией в аспекте. Я насчитал примерно 110 строк для ProgressMonitorInputStream и 40 для FilterInputStream (Я не учитывал InputStream, так как он мог быть законным суперклассом в отсутствие шаблона Decorator). В отличие от этого, аспект MonitorFileReads занимает 53 строки, а вспомогательный класс IncrementMonitor занимает 12. Линейное соотношение равно 160 к 65, или около 2.4 к 1. Хотя "количество строк"(LOC - lines-of-code) является грубой оценкой, в общем случае более короткий код является более понятным кодом.
    Более того, если вы знакомы с АОП, решение AspectJ не дает вам почувствовать, что происходит что-то особенное. Решение Java требует аккуратной совместной работы нескольких классов, в то время как в версии AspectJ работа выглядит так, как и для большинства аспектов: добавление поведения к набору точек соединения с использованием рекомендации (advice).
    И, наконец, стоит вспомнить, что частая критика АОП состоит в том, что вы "не можете больше сказать, что делает модуль при чтении исходного кода". Если вы применили декораторы к объектам без помощи аспектов, нет основанного на коде намека ни в клиентском коде (отличного от определения наложения), ни в декорируемом объекте (FileInputStream) на то, что объект отображает дополнительное поведение. В противоположность этому, если вы работаете с GUI из листинга 2 в AJDT, вы увидите дружественную аннотацию в строке while (in.read(b) != -1), которая указывает, что контролирующий аспект воздействует на вызов read. Комбинация AspectJ и его среды разработки обеспечивает в этом случае лучшую информацию, чем в оригинальной реализации.
  • Повторное использование: Поскольку декорация встроена в язык, почти все аспекты повторно используют этот шаблон. Более специализированным использованием могло бы быть создание абстрактного аспекта мониторинга и разрешение субаспектам указать pointcut для контролируемых операций. Таким образом, практически любой объект может быть декорирован при помощи мониторинга без подготовки, требующейся в традиционной реализации. (Если вы интересуетесь абстрактными аспектами, во второй части статьи их использование объясняется более детально.)
  • Поддержка: Добавление новой декорации к объекту не требует специальных усилий. Если цель декорации меняется (допустим новый тип метода read), то вы должны (возможно) обновить pointcut для учета этого. Необходимость обновления pointcut обременительна, но бремя может быть уменьшено написанием устойчивых pointcut, которые, вероятно, захватят новые операции. (Смотрите в разделе "Ресурсы" ссылку на замечательный блог по написанию устойчивых pointcut.) В любом случае, обновление pointcut кажется менее проблемным, чем обновление всех декораторов, которое понадобилось бы для аналогичного изменения в Java-реализации.
    Вот еще один интересный сценарий (упоминавшийся ранее при анализе Java-реализации): мониторинг всех операций чтения файлов. В ОО-декораторе это означает, что каждый класс, который читает поток, должен не забыть наложить на него ProgressMonitorInputStream. В отличие от этого аспект MonitorFileReads будет мониторить операции чтения любого входного потока до тех пор, пока они появляются изнутри потока управления JComponent. Поскольку ProgressMonitor появляется только тогда, когда операция длится больше предварительно установленного значения, этот аспект мог бы четко гарантировать, что пользователи никогда не будут раздражены необходимостью ожидания операции чтения файла.
  • Композиция: Аналогично конкурирующей реализации AspectJ-версия разрешает явную композицию нескольких декораторов с минимальными усилиями.

Как я упоминал ранее, главный трюк шаблона Decorator (явное добавление поведения к операции) включен в язык AspectJ. Единственным вопросом для AspectJ-реализации является ассоциирование состояния аспекта (обновленный монитор процесса чтения) с конкретным экземпляром - в примере используется карта для этой ассоциации. Эта необходимость управления ассоциацией сохраняет Decorator в качестве шаблона в AspectJ. Иногда, когда механизм декорирования уже существует, кажется более простым использование традиционного шаблона Decorator - особенно из-за того, что шаблон не затрагивает декорируемый класс. Однако если механизм декорирования не существует, гибкость и простота AspectJ-реализации является лучшим выбором.


Заключение к первой части

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

Как же все обходились без этого до сих пор? Не являясь волшебной палочкой, AspectJ сумел обеспечить некоторые четкие преимущества при использовании его для реализации традиционных ОО-шаблонов. Эти преимущества вытекают из способности AspectJ лучше справляться с пересекающимися процессами. Собирая код для шаблона в один аспект, AspectJ облегчает понимание шаблона при чтении исходного кода. Поскольку код шаблона не обнаруживается в нешаблонных классах (например, определений наложения требующихся в шаблонах Adapter и Decorator), эти другие классы тоже легче понять. Комбинирование также облегчает расширение и поддержку системы, а также повторное использование шаблонов в другом месте.

Шаблоны Adapter и Decorator представляют собой шаблоны среднего уровня сложности. Во второй части этой статьи я исследую, распространяется ли аспектная ориентация на более сложные шаблоны. Особенное внимание во второй части уделяется шаблону Observer - шаблону, который имеет несколько ролей и динамических взаимосвязей. Обсуждается также и повторное использование аспектов - способность определять шаблон или протокол в виде абстрактного аспекта и применение его со специализированным аспектом.


Загрузка

ОписаниеИмяРазмер
Source codej-aopwork56code.zip142KB

Ресурсы

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Профиль создается, когда вы первый раз заходите в developerWorks. Информация в вашем профиле (имя, страна / регион, название компании) отображается для всех пользователей и будет сопровождать любой опубликованный вами контент пока вы специально не укажите скрыть название вашей компании. Вы можете обновить ваш IBM аккаунт в любое время.

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=96667
ArticleTitle=AOP@Work: Улучшенные шаблоны проектирования AspectJ, часть 1
publish-date=05172005