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

Разрешение явной композиции и повторное использование сложных шаблонов на уровне кода

Николас Лесицки (Nicholas Lesiecki) продолжает дискуссию о преимуществах реализации шаблонов проектирования с использованием аспектно-ориентированной технологии этим детальным изучением шаблона Observer. В этой статье из цикла AOP@Work он показывает, как AspectJ позволяет преобразовать сложные шаблоны в повторно используемые базовые аспекты, тем самым давая возможность авторам сред разработки предоставлять разработчикам для использования готовые библиотеки шаблонов.

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

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



17.05.2005

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

Я выбрал для рассмотрения шаблон Observer, потому что он является главным шаблоном проектирования ОО. Он широко используется (особенно в GUI-приложениях) и формирует центральную часть MVC-архитектуры. Он решает сложную проблему достаточно хорошо. Однако, он также ставит серьезные задачи с точки зрения усилий по реализации и понимания кода. В отличие от шаблонов Decorator или Adapter, в формировании которых участвуют главным образом новые классы, созданные для шаблона, шаблон Observer требует, чтобы вы изменили существующие классы в вашей системе для его поддержки, по крайней мере, на языке Java.

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

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

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

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

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

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

Шаблон Observer

Согласно Portland Pattern Repository Wiki (смотрите раздел "Ресурсы" для детальной информации) целью шаблона Observer является:

"Определить зависимость "один ко многим" между объектами так, чтобы при изменении состояния одного объекта автоматически уведомлялись и обновлялись все его зависимые объекты.

Это делает Observer пригодным для любого рода уведомлений. Рассмотрим следующие:

  • Столбчатая диаграмма может наблюдать за объектами данных, которые она отображает, таким образом, что будет перерисовываться при изменении этих объектов.
  • Объекты AccountManager могут наблюдать за объектами Account таким образом, что они могут отправить письмо по электронной почте продавцу при изменении статуса счета.
  • Служба биллинга может наблюдать за событиями проигрывания песен в интерактивном музыкальном магазине таким образом, чтобы выписывать счет пользователю.

Я собираюсь сконцентрироваться на последнем сценарии в качестве примера для этой статьи. (Идея была позаимствована из статьи "Реализация шаблона Observer в .NET"; смотрите раздел "Ресурсы" для детальной информации.) Предположим, перед вами стоит задача добавления некоторых функций в интерактивный музыкальный магазин. В магазине уже реализованы функции индивидуального проигрывания Songs и объединения их в Playlists. Пользователи могут также просмотреть тексты конкретной песни. Вам нужно добавить выписку счетов и статистику. Во-первых, система должна отслеживать события проигрывания и показа текста песни для соответствующей записи на счет пользователя. Во-вторых, система должна обновлять список наиболее часто проигрываемых песен для отображения раздела "самые популярные". В листинге 1 приведен код основных объектов, уже существующих в системе:

Листинг 1. Основные объекты в службе интерактивного магазина
//общий интерфейс для элементов,
//которые могут быть проиграны
public interface Playable {
  String getName();
  void play();
}

public class Song implements Playable{
  private String name;

  public Song(String name) {
    this.name = name;
  }
  
  public String getName() {
    return name;
  }

  public void play() {
    System.out.println("Playing song " + getName());
  }
  
  public void showLyrics(){
    System.out.println("Displaying lyrics for " + getName());
  } 
}

public class Playlist implements Playable {
  private String name;
  private List songs = new ArrayList();
  
  public Playlist(String name) {
    this.name = name;
  }

  public void setSongs(List songs) {
    this.songs = songs;
  }
  
  public void play() {
    System.out.println("playing album " + getName());
    for (Song song : songs) {
      song.play();
    }
  }

  public String getName() {
    return name;
  }
}

public class BillingService{

  public void generateChargeFor(Playable playable) {
    System.out.println("generating charge for : " + playable.getName());
  }
}

public class SongPlayCounter {
  public void incrementPlays(Song s){
    System.out.println("Incrementing plays for " + s.getName());
  }
}

Теперь, когда вы увидели основную систему, рассмотрим реализацию Observer без применения АОП.


JavaObserver

Хотя реализации будут заметно отличаться, между ними существует некоторое сходство. Независимо от того, как вы реализовали Observer, вы должны ответить в коде на следующие четыре вопроса:

  • Какие объекты являются субъектами (Subject), а какие являются наблюдателями (Observer)?
  • Когда Subject должен послать уведомление своим Observer?
  • Что должен делать Observer при получении уведомления?
  • Когда должно начинаться и заканчиваться взаимодействие для наблюдения?

Я проведу вас через процесс создания ОО-реализации шаблона Observer, используя эти вопросы в качестве основы.

Определение ролей

Начнем с назначения ролей при помощи интерфейсов разметки (marker interface). Интерфейс Observer определяет один метод update(), который соответствует действию, совершаемому после передачи уведомления объектом Subject. Subject берет на себя больше ответственности; его интерфейс разметки определяет два метода для слежения за наблюдателями и один для уведомления этих наблюдателей о событии:

public interface Subject {   
    public void addObserver(Observer o);

    public void removeObserver(Observer o);

    public void notifyObservers();
}

После определения этих ролей вы применяете их к соответствующим проигрывателям системы.

Применение роли Observer

Вы можете модифицировать BillingService для реализации интерфейса Observer при помощи нескольких строк кода:

public class BillingService implements Observer {

  //...

  public void update(Subject subject) {
    generateChargeFor((Playable) subject);
  }
}

Отслеживание и уведомление наблюдателей

После этого вы можете перейти к двум Subject. Вот изменения в Song:

  private Set observers = new HashSet();
  
  public void addObserver(Observer o) {
    observers.add(o);
  }

  public void removeObserver(Observer o) {
    observers.remove(o);
  }

  public void notifyObservers() {
    for (Observer o : observers) {
      o.update(this);
    }
  }

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

Включение событий

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

 //...в Song
  public void play() {
    System.out.println("Playing song " + getName());
    notifyObservers();
  }
  
  public void showLyrics(){
    System.out.println("Displaying lyrics for " + getName());
    notifyObservers();
  }
  
  //...в Playlist
  public void play() {
    System.out.println("playing album " + getName());
    for (Song song : songs)  {
      //...
    }
    notifyObservers();
  }

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

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

Запуск взаимодействий для наблюдения

Для того чтобы BillingService начал наблюдение за Song или Playlist, необходимо добавить связывающий код, который будет вызывать song.addObserver(billingService). Это требование аналогично необходимости в связывающем коде для шаблонов Adapter и Decorator, описанных в первой части статьи. Кроме воздействия на проигрыватели шаблон требует от вас модификации части вашей системы для активизации шаблона. В листинге 2 приведен код, симулирующий взаимодействие клиента с системой, а также включен связывающий код (выделен жирным шрифтом). В листинге также показан пример выводимой на экран системы информации:

Листинг 2. Клиентский код, соединяющий Subjects и Observers (а также выходная информация)
public class ObserverClient {
  public static void main(String[] args) {
    BillingService basicBilling = new BillingService();
    BillingService premiumBilling = new BillingService();
    
    Song song = new Song("Kris Kringle Was A Cat Thief");
    song.addObserver(basicBilling);
    
    
    Song song2 = new Song("Rock n Roll McDonald's");
    song2.addObserver(basicBilling);
    //эта песня обслуживается двумя службами,
    //возможно требуется призовой тип интерактивного доступа
    song2.addObserver(premiumBilling);
    
    Playlist favorites = new Playlist("Wesley Willis - Greatest Hits");
    favorites.addObserver(basicBilling);
    
    List songs = new ArrayList();
    songs.add(song);
    songs.add(song2);
    favorites.setSongs(songs);
    
    favorites.play();
    
    song.showLyrics();
  }
}

//Вывод:
playing playlist Favorites
Playing song Kris Kringle Was A Cat Thief
generating charge for : Kris Kringle Was A Cat Thief
Playing song Rock n Roll McDonald's
generating charge for : Rock n Roll McDonald's
generating charge for : Rock n Roll McDonald's
generating charge for : Favorites
Displaying lyrics for Kris Kringle Was A Cat Thief
generating charge for : Kris Kringle Was A Cat Thief

Анализ Java Observer

Как вы, возможно, почувствовали из этапов реализации, Observer является тяжеловесным шаблоном. Исследуем некоторые из текущих и потенциальных воздействий шаблона на систему:

Возможен ли лучший ОО Observer?

В этой статье внимание фокусируется на использовании языка Java для ОО-реализации шаблона. Вы можете поинтересоваться, способны ли другие более функциональные ОО-языки лучше справиться с проблемами, вызванными ролями Observer. .NET-реализация, из которой эта статья черпала вдохновение, использует делегаты и события для радикального уменьшения нагрузки на свои версии классов Song и Playlist. (Для сравнения реализаций обратитесь в раздел "Ресурсы".) Аналогично ОО-язык поддерживающий множественное наследование, может ограничить нагрузку на Song до смешивания в классе поддержки Subject. Любой подход помогает достаточно хорошо, но не имеет дела ни с проблемами инициирования уведомлений, ни с композицией нескольких экземпляров шаблона. Позже в данной статье я буду обсуждать разветвление этих проблем.

  • Пересечение: В приложении Music Store вопрос биллингового наблюдения вовлекает как субъекта для выписки ему счета (Song и Playlist), так и службу выписки счетов (BillingService). Реализация на языке Java перемещает связывающий код по объектам. Поскольку затрагиваются три различных класса, в цели которых не входит взаимодействие для наблюдения, биллинговое наблюдение представляет пересекающийся процесс.
  • Понимание: Вы можете начать обзор системы с точки зрения доменных объектов. ОО-реализация шаблона Observer требует модификации доменных классов, вовлеченных в шаблон. Как вы видели в классах реализации, эти модификации не являются тривиальными. Для Subject вам необходимо добавить несколько методов к каждому классу, для того чтобы они играли свою роль в шаблоне. Простые абстракции доменных понятий (списки песен, песни) как бы придавлены махиной шаблона Observer. (Обратитесь к вкладке "Возможен ли лучший ОО Observer?", чтобы узнать, почему некоторые отличные от Java языки могут сделать это проще.)
    Вы можете также исследовать систему с точки зрения шаблона. В Java-реализации шаблон рискует "исчезнуть в коде". Вы не можете ясно сконструировать мысленную модель шаблона без изучения всех трех составляющих шаблона: Observer, Subject и клиентского кода, соединяющего их. Даже сообразительный разработчик, обнаружив одну часть шаблона, продолжит искать другие, но здесь нет модуля "BillingObserver", который соберет для вас кусочки вместе.
  • Повторное использование: Для повторного использования этого шаблона вы должны повторно реализовать его с нуля. Обычно вы можете использовать готовые классы поддержки (например, java.util содержит Observer и Observable), которые заполняют некоторые части шаблона за вас, но основная часть работы остается за вами.
  • Поддержка: Для того, чтобы помочь вам подумать о стоимости обслуживания Observer, рассмотрим необходимость устранения двойного биллинга. Если вы снова взглянете на выводимую информацию, сгенерированную первой реализацией шаблона (смотрите листинг 2), то увидите, что приложение выписало счет на песни, играемые в контексте списка песен. Это вас не должно удивлять, поскольку это точно то, что вы написали в коде. Однако отдел маркетинга хотел бы иметь суммарную цену списка песен для поощрения оптовых покупок. Другими словами, они хотели бы выписывать счета на список песен, а не на песни, находящиеся в нем.
    Вы легко можете себе представить проблемы, которые это вызовет при традиционной реализации шаблона Observer. Вы могли бы модифицировать каждый метод play() для приема параметра, указывающего, что он был вызван другим методом play(). В качестве альтернативы вы могли бы поддерживать ThreadLocal для отслеживания информации. В любом случае вы должны были бы вызывать notifyObservers() только после гарантирования контекста вызова. Эти изменения добавят вес и сложность и без того перегруженным участникам шаблона. Поскольку изменение затрагивает несколько файлов, при повторном дизайне могут вкрасться ошибки.
  • Композиция: Еще одним сценарием, который вы должны рассмотреть, является наблюдение различными наблюдателями. Вспомните, что музыкальная служба должна отслеживать наиболее популярные песни. Однако собираемая статистика относится только к проигрываемым песням, а не к просматриваемым текстам или проигрыванию списков песен. Это означает, что SongCountObserver следит за несколько иным набором операций из наблюдателя биллинга.
    Чтобы завершить эту задачу, вы должны модифицировать Song для поддержки списка Observer, которые интересуются только уведомлениями из play() (но не операциями с текстами песен). Затем метод play() мог бы инициировать это событие независимо от события биллинга. ОО-шаблон защищен от прямой зависимости конкретного Subject от конкретного Observer, но в данном случае этого, кажется, невозможо избежать, поскольку Song должен инициировать разные события для каждого типа Observer. Столкнувшись с необходимостью создания второго типа Observer, Java-реализация просто терпит неудачу.

Как можно ожидать, AspectJ Observer предлагает элегантное решение сценария, описанного в этом анализе. Перед его исследованием я познакомлю вас с тем, как AspectJ обрабатывает базовый шаблон.

Проект шаблонов проектирования с применением АОП

Мое очарование шаблонами проектирования и AspectJ началось с проекта Aspect-Oriented Design Pattern Implementation (смотрите раздел "Ресурсы"), спонсирующегося University of British Columbia. Как я упоминал в первой части этой статьи, Ян Хэннеман и Грегор Кицзейлз исследовали 23 GoF-шаблона и создали рекомендованные реализации каждого из них как на языке Java, так и в AspectJ. Результаты идеальны для изучения; 17 оригинальных шаблонов были улучшены в АОП-реализации. Еще более впечатляющей является Pattern Library. Авторы проекта сумели извлечь пригодные для повторного использования абстрактные аспекты из 13 шаблонов и сделали их свободно доступными под лицензией Mozilla Public License. Аспект ObserverProtocol, показанный в настоящей статье, был взят из этого проекта. Обратитесь к разделу "Ресурсы" для загрузки шаблонов.


AspectJ Observer

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

  • Какие объекты являются субъектами (Subject) и какие являются наблюдателями (Observer)?
  • Когда Subject должен послать уведомление своим Observer?
  • Что должен делать Observer при получении уведомления?
  • Когда должно начинаться и заканчиваться взаимодействие для наблюдения?

Абстрактный аспект шаблона

Вы должны расширить абстрактный аспект перед его использованием во многом также как и абстрактный класс. Абстрактный аспект может определить абстрактные pointcut (pointcut с именем, но без тела) и затем определить конкретный advice по этим pointcut. Это означает, что супер-аспект указывает поведение, а субаспект контролирует, где применить это поведение. Кроме того, абстрактные аспекты могут определять абстрактные методы, которые субаспект должен переопределять, также как и абстрактный класс. Зная эти факты, взгляните на аспект ObserverProtocol из проекта АОП-шаблонов проектирования.

Определение роли

Как и в другой реализации роли определяются интерфейсами разметки. Однако в случае AspectJ-реализации интерфейсы являются пустыми (и могут, вероятно, быть заменены аннотациями в AspectJ 5). Интерфейсы являются членами ObserverProtocol:

/**
 * Этот интерфейс используется расширяющими аспектами для указания типов,
 * которыми могут быть Subjects.
 */
public interface Subject{}

/**
* Этот интерфейс используется расширяющими аспектами для указания типов,
 * которыми могут быть Observers.
 */
public interface Observer{}

Отслеживание наблюдателей

Вместо принуждения участников к отслеживанию наблюдателей аспект централизует эту функцию. В листинге 3 содержится код, который реализует эту часть шаблона. Главная идиома должна быть вам знакома из АОП-реализации шаблона Decorator в первой части статьи. Опять аспект использует "неторопливо" (lazily) инициализированный Map для отслеживания специфичного для объекта состояния. (И опять же существуют альтернативы этому шаблону, использующие модели создания экземпляров, но они немного выходят за рамки этой статьи). Обратите внимание на один интересный элемент в листинге 3 - методы addObserver и removeObserver являются public. Это означает, что код в любом месте системы может программно определить участие в шаблоне:

Листинг 3. ObserverProtocol управляет Observers, наблюдающими Subject
/**
   * Хранит отображение между Subjects и 
   * Observers. Для каждого Subject, сохраняется LinkedList
   * своих Observers.
   */
  private WeakHashMap perSubjectObservers;


  /**
   * Вовращает Collection Observers  
   * конкретного субъекта. Предназначен для внутреннего использования.
   */
  protected List getObservers(Subject subject) { 
    if (perSubjectObservers == null) {
      perSubjectObservers = new WeakHashMap();
    }
    List observers = (List)perSubjectObservers.get(subject);
    if ( observers == null ) {
      observers = new LinkedList();
      perSubjectObservers.put(subject, observers);
    }
    return observers;
  }

  
  /**
   * Добавляет Observer к Subject.
   */ 
  public void addObserver(Subject subject, Observer observer) { 
    getObservers(subject).add(observer);  
  }
  
  /**
   * Удаляет Observer из Subject. 
   */
  public void removeObserver(Subject subject, Observer observer) { 
    getObservers(subject).remove(observer); 
  }
  
  //продолжение аспекта...

Уведомление наблюдателей

Для реализации фактического обновления наблюдателей ObserverProtocol использует цикл, очень похожий на тот, который вы применяли в Java-реализации. Однако инициируется уведомление совершенно по-разному. Прежде всего, аспект определяет абстрактный pointcut:

protected abstract pointcut subjectChange(Subject s);

Этот pointcut существует для конкретизации в субаспектах. ObserverProtocol продолжает определять часть advice, которая реагирует на точки соединения, выбранные при помощи subjectChange():

after(Subject subject) returning : subjectChange(subject) {
  for (Observer observer : getObservers(subject)) {
    updateObserver(subject, observer);
  }
} 

protected abstract void updateObserver(Subject subject, Observer observer);

Для каждого Observer измененного объекта аспект вызывает updateObserver(), абстрактный метод, который он тоже определяет. Таким способом конкретные аспекты могут определить, что значит получить обновление.

Конкретный субаспект

На данном этапе вы увидели все, что ObserverProtocol может вам предложить. Для применения шаблона вам необходимо расширить аспект и обеспечить конкретные ответы на четыре центральных для Observer вопроса, определенных в начале этого обсуждения. После загрузки ObserverProtocol вы можете начать создавать ваш аспект и объявить, что он расширяет ObserverProtocol. Как и следует ожидать, компилятор AspectJ любезно информирует вас о том, что вы не завершили определение аспекта. Вы должны конкретизировать pointcut subjectChange и реализовать метод updateObserver. Эти напоминания служат вам проводником при использовании шаблона в вашей системе.

Назначение роли

В AspectJ вы можете назначить роли участникам шаблона без прямой модификации классов Song, Playlist и BillingService, что показано ниже:

declare parents : Playable extends Subject;  
declare parents : BillingService implements Observer;

Компилятор не заставляет вас вставлять эти объявления, но вы нашли бы невозможным использовать остальную часть механизма аспектов без них, поскольку ObserverProtocol определяет свои pointcut и методы в терминах этих интерфейсов разметки.

Включение событий

Возможно, в самой важной части аспекта вы конкретизируете pointcut subjectChange для определения тех операций, которые составляют достойное уведомления событие. В данном случае pointcut указывает выполнение метода play() любого класса, реализующего интерфейс Playable (его реализуют и Song, и Playlist). Pointcut также выбирает выполнение метода showLyrics() в Song:

pointcut titleUse(Playable playable) :
    this(playable)
    && ( 
    execution(public void Playable+.play()) ||
    execution(public void Song.showLyrics())
    );
    
  public pointcut subjectChange(Subject subject) : 
    titleUse(Playable) && this(subject);

Реакция на обновления

Для реакции на обновление вы реализуете метод update, во многом аналогично версии на Java. Различие состоит в том, что здесь вы добавляете метод к аспекту, а не напрямую к BillingService, который остается неосведомленным о своем участии в шаблоне:

public void updateObserver(Subject s, Observer o){
    BillingService service = (BillingService)o;
    service.generateChargeFor((Playable)s);
  }

Запуск взаимодействия для наблюдения

Как я упоминал ранее, присутствие public методов addObserver и removeObserver в аспекте делает возможным конфигурировать аспект программным способом. Для запуска наблюдения за песнями и списками песен вы можете продублировать Java-реализацию клиентской части и заменить каждый экземпляр song.addObserver(basicBilling) на ObserverBillingPolicy.aspectOf().addObserver(song, basicBilling).

Чтобы сделать присутствие шаблона как можно более незаметным, имеет смысл начать взаимодействие автоматически, используя advice, как показано ниже:

  //в конкретном аспекте
  
  //может быть вставлено средой вставки зависимостей, например Spring
  //см. раздел "Ресурсы" для ссылки на отличный блог по этой теме
  private BillingService defaultBillingService =
    new BillingService();
  
  pointcut playableCreation(Subject s) :
    execution(public Playable+.new(..))
    && this(s);
  
  after(Subject s) returning : playableCreation(s){
    addObserver(s, defaultBillingService);
  }

Эти pointcut и advice начинают наблюдение за биллингом немедленно после создания Playable, используя сконфигурированный в аспекте BillingService по умолчанию. Однако ObserverProtocol предоставляет здесь гибкость. Вы можете представить следующий advice, который применяет дополнительный биллинг для определенных песен или извлекает план биллинга, связанный с текущим пользователем системы. Также advice может быть использован для автоматического прекращения взаимоотношений, как только песня становится не пригодной для дальнейшего биллинга.


Анализ AspectJ Observer

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

  • Понимание: С точки зрения участников шаблона, АОП-версия Observer проще, поскольку она не требует ничего специального для Song, Playlist или BillingService. Удаление этой зависимости из участников делает их более гибкими и повторно используемыми. Например, они могут быть реализованы на системах, не требующих шаблона Observer. Они также могут участвовать в других шаблонах без боязни, что объем кода шаблонов завалит основную функциональность классов.
    С точки зрения шаблонов АОП-версия опять же проще. Вместо того, чтобы полагаться на вас и собирать фрагменты шаблона в вашей голове, AspectJ-реализация предлагает аспект ObserverProtocol, который хранит общую структуру шаблона. Конкретный аспект определяет, как шаблон применяется к системе. Оба модуля являются реальными и, с точки зрения шаблона, завершенными. То есть, если вы используете AJDT и замечаете, что, скажем, метод play() рекомендуется аспектом наблюдателя, вы можете последовать по ссылке, которую вам предоставляет AJDT, и понять общую картину. (Раздел "Ресурсы" содержит ссылку на прекрасную статью о том, как AJDT может помочь вам в навигации и понимании кода AspectJ.)
    Генерирование "событий" уведомления собирается из модулей в AspectJ-реализации особенно хорошо. В традиционной реализации эти запросы существуют в трех отдельных местах. Не существует никакого прямого указания относительно того, почему определенные операции сгенерировали бы события, либо какие другие операции могли бы сделать это. В АОП-шаблоне именованная pointcut взаимодействует с общим свойством операций (использование заголовка). Спецификация pointcut указывает на другие точки соединения, на которые влияет шаблон.
  • Повторное использование: Потенциал для повторного использования аспекта ObserverProtocol высок. Фактически, повторное использование аспектов шаблонов является половиной причины, из-за которой я писал эту статью. Как разработчик я могу в любое время положиться на компетентность автора среды разработки, вместо создания своей собственной реализации. Я счастлив. Аспекты обеспечивают возможность повторного использования пересекающегося кода, кода, который прежде был настолько охвачен деталями своей реализации, что он не имел самостоятельного существования. Вы увидите хороший пример повторного использования в разделе "Композиция" анализа.
  • Поддержка: Для просмотра конкретного примера того, как аспекты облегчают развитие системы, возвратитесь к трудностям исключения песен из двойного биллинга в Java-версии системы. Для того чтобы выполнить это требование в AspectJ, вам необходимо только отредактировать pointcut subjectChange(). В следующем коде используется pointcut cflowbelow() для исключения выполнения play(), которое вызывается внутри потока управления другого выполняющегося play():
    //оригинальный titleUsePointcut
    pointcut titleUse(Playable playable) :
      this(playable)
      && ( 
        execution(public void Playable+.play()) ||
        execution(public void Song.showLyrics())
      );
    
    //исключить использование заголовка, возникающего в результате
    //использования другого заголовка
    pointcut topLevelTitleUse(Playable playable) :
      titleUse(playable) && ! cflowbelow(titleUse(Playable));
    
    //определить subjectChange в терминах более ограничивающей
    //pointcut
    public pointcut subjectChange(Subject subject) : 
      topLevelTitleUse(Playable) && this(subject);

    Эта модификация тривиальна, особенно из-за того, что затрагивает один файл. Более того, при этом становится ясна цель новой политики. Другие модификации, такие как добавление новых Subject или операции изменения субъекта, требуют аналогичных простых изменений в аспекте, а не координированных изменений в нескольких файлах.
  • Композиция: Вспомните, что Java-реализация шаблона Observer также имела проблемы со вторым экземпляром шаблона, применяемого к (некоторым из) тем же самым участникам. Для реализации этого требования в AspectJ вы просто расширяете абстрактный аспект второй раз с новыми определениями для pointcuts и абстрактных методов. Поскольку каждый аспект управляет своим собственным списком Observer и Subject, и каждый аспект определяет также свою собственную pointcut subjectChange, два аспекта не конфликтуют друг с другом. Вы можете исследовать код для SongCountObserver в листинге 4:
    Листинг 4. Второй аспект Observer работает с теми же участниками
    public aspect SongCountObserver extends ObserverProtocol {
      declare parents : Song extends Subject;
      declare parents : SongPlayCounter implements Observer;
    
      pointcut titleUse(Song song) :
        this(song)
        && execution(public void Song.play());
    
      public pointcut subjectChange(Subject subject) : 
        titleUse(Song) && this(subject);
    
      public void updateObserver(Subject s, Observer o) {
        SongPlayCounter counter = (SongPlayCounter) o;
        counter.incrementPlays((Song) s);
      }
    
      //может быть вставлено средой вставки зависимостей,
      //например Spring
      private SongPlayCounter defaultCounter = new SongPlayCounter();
    
      pointcut songCreation(Subject s) :
        execution(public Song.new(..))
        && this(s);
    
      after(Subject s) returning : songCreation(s){
        // различные песни могут отслеживаться в различиных статистических наборах...
        addObserver(s, defaultCounter);
      }
    
    }

    Поскольку в AspectJ несколько шаблонов (и даже экземпляров одного и того же шаблона) могут открыто комбинироваться, AspectJ-системы лишены некоторых проблем, связанных с "плотностью шаблона". Это может дать возможность более частого использования лучших практических приемов, которые шаблоны проектирования реализовывают без боязни того, что объем реализации переполнит код. (Ждите готовящуюся в серии AOP@Work статью Веса Изберга (Wes Isberg) по использованию аспектно-ориентированной технологии для устранения плотности шаблонов в ОО-шаблонах проектирования.)

Заключение

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

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

Новые решения принимают различные формы. АОП может радикально упростить некоторые шаблоны (например Decorator) до такого состояния, что они едва ли станут заслуживать статуса шаблона. Другие шаблоны (такие как Observer) начинают новую жизнь как пригодные для повторного использования библиотечные модули. Однажды вы обнаружите, что расширение и реализация аспекта шаблона так же естественны, как использование класса из библиотеки Collections в настоящее время.

Являются ли шаблоны специфичными для АОП?

Интригующей темой дальнейшего изучения являются шаблоны проектирования, решающие рекуррентные проблемы в аспектно-ориентированных языках. Вы можете увидеть совет по этой теме в данной статье, когда два аспекта использовали "неторопливо" (lazily) инициализированную карту для слабой связи с объектом. В дополнение к этому в сообществе пользователей начинают появляться специфичные для АОП шаблоны (или шаблоны-кандидаты). Рамнивас Ладдад (Ramnivas Laddad) описывает несколько таких шаблонов, например, Worker Object и Wormhole, в своей книге "AspectJ в действии" (смотрите раздел "Ресурсы"). Стефан Ханенберг (Stefan Hanenberg) и Арно Шмидмайер (Arno Schmidmeier) предложили несколько интересных кандидатов, таких как "Template Advice" и "Pointcut Method" в своей работе "Идиомы для построения программных сред в AspectJ." Только время покажет, являются ли эти появляющиеся шаблоны полезными и прямыми идиомами или "обходными путями сделать то, что не сделано в AspectJ." Например, на конференции AOSD этого года я вел сессию, в которой обсуждалась возможность лучшей языковой поддержки идиомы "неторопливо инициализированной карты (lazily initialized map)". Исследование шаблонов сегодня может дать сильный толчок развитию функций языков завтра.

Благодарности

Своим появлением эта статья во многом обязана Весу Избергу (Wes Isberg), Мику Керстену (Mik Kersten), Рону Бодкину (Ron Bodkin) и Рамнивасу Ладдаду (Ramnivas Laddad), которые просмотрели ранние черновики и дали полезные советы и поправки.


Загрузка

ОписаниеИмяРазмер
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=96669
ArticleTitle=AOP@Work: Улучшенные шаблоны проектирования AspectJ, часть 2
publish-date=05172005