Продолжается подписка на наши издания! Вы не забыли подписаться?

Аспектно-ориентированное программирование

Автор: Валентин Павлов

АСПЕКТ (от лат. aspectus — вид), точка зрения, 
с которой рассматривается какое-либо явление, понятие, перспектива. Большой энциклопедический словарь

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

Объект исследования

Современные программные системы обладают весьма высоким уровнем сложности: один разработчик практически не в состоянии охватить все детали системы. Сложность программных систем обусловлена несколькими причинами: сложностью реальной предметной области, из которой исходит заказ на разработку; необходимостью обеспечения достаточной гибкости программы; неудовлетворительными способами описания поведения больших дискретных систем [9].

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

Система как набор функциональных требований

Как правило, любая программная система состоит из основной (предметно-ориентированной) и системной частей. Например, ядро системы обработки кредитных карт предназначено для работы с платежами, тогда как функциональность системного уровня предназначена для ведения журнала событий, целостности транзакций, авторизации, безопасности, производительности и т.д. Большинство подобных частей системы, известных как сквозная функциональность [2], затрагивает множество основных предметно-ориентированных модулей. Можно рассматривать сложную программную систему как комбинацию модулей, каждый из которых включает в себя, кроме бизнес-логики, часть сквозной функциональности из набора требований к системе. Рисунок 1 показывает систему как набор таких требований, разбитых на разные модули.

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


Рисунок 1. Система как набор функциональных модулей.

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


Рисунок 2. Декомпозиция требований: выделение функциональности.

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

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

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

public class SomeBusinessClass 
{
  // Бизнес-данные
  // вспомогательные данные
  public void performSomeOperation(OperationInformation info)
  {
    // проверить уровень доступа к данным
    // проверить соответствие входных данных контракту
    // запретить доступ к данным другим потокам выполнения
    // проверить состояние кэша данных
    // занести в журнал отметку о начале операции
    // ==== Реализация логики данного класса ====
      // занести в журнал отметку о конце операции
      // разрешить доступ к данным другим потокам выполнения
  }
}

В этом примере можно выделить две проблемы. Во-первых, определяемые вспомогательные данные не относятся к требованиям, накладываемым на данный модуль, а нужны для работы сквозной функциональности. Во-вторых, реализация performSomeOperation(...) выглядит более нагруженной, чем просто "Реализация логики данного класса". Для правильной работы данного метода необходимо выполнить ряд действий, не относящихся конкретно к данному модулю — проверить уровень доступа к данным, проверить соответствие входных данных контракту, запретить доступ к данным другим потокам выполнения, проверить состояние кэша данных, занести в журнал отметку — это требования системного уровня. К тому же, многие из этих общих требований должны быть реализованы в других модулях.

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


Рисунок 3.

Ведение журнала событий — типичный пример сквозной функциональности, аспекта функционирования системы, реализация которого присутствует во многих программных модулях. При анализе требований к системе можно выделить и другие аспекты функционирования подобного типа. Данные аспекты функционирования системы объединяет то, что их реализация не может быть локализована имеющимися средствами языка программирования в отдельном программном модуле (или ряде модулей). Схематичное распределение функциональности "ведение журнала событий" в программной системе в некотором "идеальном" варианте представлено на рисунке 4.


Рисунок 4.

Сквозная функциональность в системе

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


Рисунок 5.

Признаки.

На проблемы с реализацией сквозной функциональности при использовании существующих подходов могут указывать несколько признаков. Разобьем эти признаки на две категории:

Следствия

Наличие запутанного и рассредоточенного кода влияют на проектирование и реализацию во многих отношениях:

Поскольку сложность программных систем возросла и появилась сквозная функциональность, находящаяся на срезе системы, неудивительно, что появилось несколько подходов к решению этих проблем. Эти подходы включают в себя классы-примеси (mix-in) [20], шаблоны проектирования [6] и специфичные доменные решения.

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

Поведенческие шаблоны проектирования, такие, как Visitor или Template Method, позволяют поместить сквозную функциональность в главные классы, которые как в случае mix-In классов будут обходить компоненты, при этом вызывая логику, специфичную для посещения данного компонента или вызывая специфичный шаблонный метод.

Специфичные доменные решения, такие как, например, каркасы (framework) и серверы приложений, позволяют разработчикам выносить некоторые сквозные требования на уровень этих решений. Например, архитектура EJB позволяет вынести на уровень сервера приложения сквозную функциональность следующего вида — безопасность, администрирование, анализ производительности и управление поведением перманентных объектов, и сфокусироваться только на разработке компонентов уровня предприятия. Специфичные доменные решения предлагают специализированный механизм для решения специфичных проблем, однако при смене технологии приходится заново изучать новые подходы для решения тех же проблем.

Введение в АОП

Исследователи изучили различные пути выделения в отдельные модули сквозной функциональности в сложных программных системах. Аспектно-ориентированное программирование (АОП) является одним из этих решений. АОП предлагает средства выделения сквозной функциональности в отдельные программные модули — аспекты.

С точки зрения АОП в процессе разработки достаточно сложной системы программист решает две ортогональные задачи:

Современные языки программирования (такие как, например, C++, VB и т.п.) ориентированы, прежде всего, на решение первой задачи. Код компонента представляется в виде класса, т.е. он хорошо локализован и, следовательно, его легко просматривать, изучать, модифицировать, повторно использовать. С другой стороны, при программировании процессов, в которые вовлечены различные объекты, мы получаем код, в котором элементы, связанные с поддержкой такого процесса, распределены по коду всей системы. Эти элементы встречаются в коде множества классов, их совокупность в целом не локализована в обозримом сегменте кода. В результате мы сталкиваемся с проблемой "запутанного" кода.

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

Основные концепции АОП

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

АОП позволяет реализовывать отдельные концепции в слабосвязанном виде, и, комбинируя такие реализации, формирует конечную систему. АОП позволяет построить систему, используя слабосвязанные, разбитые на отдельные модули (аспекты) реализации общесистемных требований.

Разработка в рамках АОП состоит из трех отдельных шагов:


Рисунок 6. Фазы аспектно-ориентированной разработки ПО.

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

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

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

Пример интеграции аспектов

Для иллюстрации работы интегратора аспектов вернемся к примеру обработки кредитных карт. Для краткости рассмотрим только 2 операции — кредитования и дебетования:

public classCreditCardProcessor 
{
  public void debit(CreditCard card, Currency amount)
    throws InvalidCardException, NotEnoughAmountException,
       CardExpiredException 
  {
    // логика по дебету
  }
  
  public void credit(CreditCard card, Currency amount)
     throws InvalidCardException 
  {
    // логика по кредиту
  }
}

и интерфейс журнала событий:

public interface Logger 
{
  public void log(Stringmessage);
}

Для получения желаемой композиции требуется применение следующих правил, выраженных на обычном языке:

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

Интегратор аспектов, применяя такие правила, получит код, эквивалентный следующему:

public class CreditCardProcessorWithLogging
{
  Logger _logger;

  public void debit(CreditCard card, Money amount)
    throws InvalidCardException, NotEnoughAmountException,
      CardExpiredException\
  {
    _logger.log("Starting CreditCardProcessor.debit(CreditCard, Money) "
      + "Card: " + card + " Amount: " + amount);
    // Дебитование
    _logger.log("Completing CreditCardProcessor.debit(CreditCard, Money) "
      + "Card: " + card + " Amount: " + amount);
  }  

  public void credit(CreditCard card, Money amount)
    throws InvalidCardException
  {
    System.out.println("Debiting");
    _logger.log("Starting CreditCardProcessor.credit(CreditCard, Money) " 
     + "Card: " + card + " Amount: " + amount);
    // Кредитование
    _logger.log("Completing CreditCardProcessor.credit(CreditCard, Money) " 
      + "Card: " + card + " Amount: " + amount);
  }
}


Рисунок 7. Процесс компоновки аспектных и традиционных модулей.

Автоматическая компоновка аспектов и традиционных модулей программы (компонентов) является ключевым свойством АОП, которое определяет основное преимущество данной технологии: делает возможной инкапсуляцию сквозной функциональности в отдельных программных модулях.

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

Подходы к интеграции аспектов определяются языком, поддерживающим АОП, и детально изложены в [2] .

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

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

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

Абстрагирование — это метод обобщения, с помощью которого разработчики могут решать различные проблемы. Затем можно использовать имеющиеся готовые решения полученных типовых проблем в качестве строительных блоков, из которых разработчики получают решения, пригодные для реализации различных проектов [7].

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

Двумя расширениями концепции повторного использования кода являются библиотеки функций и API. Они предоставляют разработчику полный пакет функциональности без необходимости копирования программного кода из приложения в приложение [7].

Для иллюстрации важности повторного использования кода в таблице 1 приведена сравнительная характеристика из [7] различных подходов к повторному использованию.

Метод Повторное
использование
Абстракция Универсальность подхода
Копирование и вставка Очень плохо Отсутствует Очень плохо
Структуры данных Хорошо Тип данных Средне — хорошо
Функциональность Хорошо Метод Средне — хорошо
Типовые блоки кода Хорошо Типизируемая Хорошо
Алгоритмы Хорошо Формула Хорошо
Классы Хорошо Данные + Метод Хорошо
Библиотеки Хорошо Функции Хорошо — очень хорошо
API Хорошо Классы утилит Хорошо — очень хорошо
Компоненты Хорошо Группы классов Хорошо — очень хорошо
Шаблоны проектирования Отлично Решения проблем Очень хорошо
Сквозная функциональность Средне — хорошо Отсутствует Плохо
Сквозная функциональность Хорошо Аспект Очень хорошо

Показатель степени повторного использования очень сильно зависит от эффективности применения того или иного метода на практике.

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

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

Недостатки аспектного подхода

В настоящий момент аспектно-ориентированный подход обладает рядом недостатков:

AspectJ как одна из реализаций АОП

АОП можно поддерживать в рамках уже существующих языков. Так, в частности, исследовательский центр Xerox PARC разработал систему AspectJ, поддерживающую АОП в рамках языка Java. Этот пакет встраивается в такие системы разработки, как Eclipse, Sun ONE Studio, Forte 4J и Borland JBuilder. Я выбрал AspectJ из-за того, что эта реализация АОП обладает наиболее широкими возможностями.

AspectJ — это простое и практическое расширение языка Java, которое добавляет к Java возможности, предоставляемые АОП. Пакет AspectJ состоит из компилятора (ajc), отладчика (ajdb), и генератора документации (ajdoc). Поскольку AspectJ является расширением Java, то любая программа, написанная на Java, будет правильной с точки зрения семантики AspectJ. Компилятор AspectJ выдает байт-код, совместимый с виртуальной машиной Java. Поскольку в качестве базового языка для AspectJ был выбран язык Java, то он унаследовал от Java все преимущества, и спроектирован таким образом, что будет легко понятен Java-разработчикам. Добавленные расширения касаются в основном способов задания правил интеграции аспектов и java-объектов. Данные правила выражаются в ключевых понятиях AspectJ:

  1. JoinPoint — строго определенная точка выполнения программы, ассоциированная с контекстом выполнения (вызов метода, конструктора, доступ к полю класса, обработчик исключения, и т.д.).
  2. Pointcut — набор (срез) точек JoinPoint удовлетворяющих заданному условию.
  3. Advice — набор инструкций языка java, выполняемых до, после или вместо каждой из точек выполнения (JoinPoint), входящих в заданный срез (Pointcut).
  4. Aspect — основная единица модульности AspectJ. В аспектах задаются срезы точек выполнения (Pointcut) и инструкции, которые выполняются в точках выполнения (Advice).
  5. Introduction — способность аспекта изменять структуру Java-класса путем добавления новых полей и методов, так и иерархию класса.

Pointcut и Advice определяют правила интеграции. Аспект — единица, напоминающая класс в ООП, она соединяет элементы pointcut и элементы advice вместе, и формирует модуль на срезе системы.

Рассмотрим пример, демонстрирующий, как язык AspectJ реализует принципы АОП. В качестве примера возьмем модель простого графического редактора. Этот редактор может работать с двумя типами графических элементов — точкой и линией. Диаграмма классов редактора представлена на рисунке 8. Классы Point и Line реализуют интерфейс FigureElement, содержащий метод перемещения фигуры. Операциями, влияющими на обновление экрана, являются операции перемещения фигур. После перемещения фигуры необходимо обновить изображение (Display). Обновление изображения в данном случае является сквозной функциональностью, которая должна вызываться при некоторых условиях (изменении положения фигур).


Рисунок 8. Модель графического редактора.

Класс Line содержит в себе 2 экземпляра класса Point. Классы, реализующие точку и линию, могут быть представлены в следующем виде:

class Line implements FigureElement
{
  private Point p1, p2;
  Point getP1() { return p1; }
  Point getP2() { return p2; }
  void setP1(Point p1) { this.p1 = p1; }
  void setP2(Point p2) { this.p2 = p2; }
  void moveBy(int dx, int dy) { ... }
}

class Point implements FigureElement 
{
  private int x = 0, y = 0;
  int getX() { return x; }
  int getY() { return y; }
  void setX(int x) { this.x = x; }
  void setY(int y) { this.y = y; }
  void moveBy(int dx, int dy) { ... }
}

Предположим, пользователь переместил фигуру на 2 пункта, при этом отработал метод lineInst.moveBy(2,2). На схеме вызываемых функций (рисунок 9) можно увидеть, как работает этот метод. В теле вызываемого метода moveBy() содержится логика перемещения фигуры; метод имеет входные параметры, возвращаемое значение и набор инструкций языка, среди которых находится вызов аналогичного метода у двух точек — элементов класса Line. Метод moveBy() класса Point также имеет входные параметры, возвращаемое значение и набор инструкций языка. Каждая строго определенная точка выполнения программы, ассоциированная с контекстом выполнения, с учетом ограничений языка AspectJ является JoinPoint-ом. Выполнение метода moveBy() класса Line, вызов метода moveBy у Point — являются точками JoinPoint.


Рисунок 9. Схема вызываемых функций.

Здесь необходимо ввести различия между точками выполнения программы в рамках AspectJ. Точки выполнения бывают двух типов — точка выполнения метода, которая представляет собой выполнение всего тела метода, и точка вызова метода, которая представляет собой точку непосредственного вызова метода. Другими словами, тело метода может быть представлено точкой со стороны клиента либо только вызов метода может быть представлен как точка.

Язык AspectJ позволяет описывать несколько типов подобных точек выполнения программы.

При вызове клиентом метода moveBy все точки выполнения (joinPoint) находятся внутри потока управления, начиная с точки, удовлетворяющей условию. Средствами языка AspectJ можно влиять на поток управления, описав соответствующий поток управления как набор точек pointcut.

Конструкцией языка, позволяющей описывать набор точек JoinPoint, удовлетворяющих заданному условию, является pointcut. Например, call(void Line.setP1(Point)) соответствует каждой точке выполнения программы, если она является вызовом метода с сигнатурой "void Line.setP1(Point)". При конструировании подобных срезов можно использовать логические операции &&, || и !, что дает большую гибкость при описании наборов точек выполнения программы. Срезы точек могут иметь имя, что позволяет их повторно использовать при конструировании других срезов и описании набора инструкций Advice.


Рисунок 10. Поток выполнения метода moveBy.

Advice — набор инструкций языка java, выполняемых до, после или вместо каждой из точек выполнения, входящих в заданный срез. Язык AspectJ позволяет описывать подобные инструкции по следующим правилам:

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

Аспект — единица модульности AspectJ. В аспектах задаются срезы точек выполнения и инструкции, которые выполняются в точках выполнения. Аспект имеет сходство с классом — аспект может содержать методы и поля, расширять другие классы или аспекты, или реализовывать интерфейсы.

Далее представлено два варианта кода рассматриваемого графического редактора — с использованием языка AspectJ и без него. В варианте без использования AspectJ видно, что при изменении координат фигур в методы, отвечающие за эту функциональность, встроен код, реализующий обновление дисплея. В примере с использованием аспектного подхода классы содержат код, отвечающий только за свою логику. Набор инструкций, реализующий сквозную функциональность "обновление дисплея", находится в аспектном модуле — aspect DisplayUpdating. На этом примере наглядно показано основное преимущество аспектного подхода — локализация сквозной функциональности в отдельных модулях и встраивание подобной функциональности в требуемые участки системы.

Пример кода графического редактора без использования языка AspectJ:

class Line implements FigureElement 
{
  private Point p1, p2;
  Point getP1() { return p1; }
  Point getP2() { return p2; }
  void setP1(Point p1) 
  {
    this.p1 = p1;
    Display.update(this);
  }
  void setP2(Point p2) 
  {
    this.p2 = p2;
    Display.update(this);
  }
}

class Point implements FigureElement 
{
  private int x = 0, y = 0;
  int getX() { return x; }
  int getY() { return y; }
  void setX(int x) 
  {  
    this.x = x;
    Display.update(this);
  }
  void setY(int y) 
  {  
  this.y = y;
  Display.update(this);
  }
}

Пример кода графического редактора с использованием языка AspectJ:

class Line implements FigureElement 
{
  private Point p1, p2;
  Point getP1() { return p1; }
  Point getP2() { return p2; }
  void setP1(Point p1) 
  {
    this.p1 = p1;
  }
  void setP2(Point p2) 
  {
    this.p2 = p2;
  }
}
class Point implements FigureElement 
{
  private int x = 0, y = 0;
  int getX() { return x; }
  int getY() { return y; }
  void setX(int x) {this.x = x; }
  void setY(int y) {this.y = y; }
}
aspect DisplayUpdating 
{
  pointcut move(FigureElement figElt):
    target(figElt) &&
    (call(void FigureElement.moveBy(int, int) ||
    call(void Line.setP1(Point))       ||
    call(void Line.setP2(Point))       ||
    call(void Point.setX(int))        ||
    call(void Point.setY(int)));
  after(FigureElement fe) returning: move(fe) 
  {
  Display.update(fe);
  }
}


Рисунок 11. Аспект DisplayUpdating на срезе системы.

При разработке программных систем с использованием средств языка AspectJ можно следовать трем принципам разработки аспектного подхода: выделять в отдельные модули сквозную функциональность — провести аспектную декомпозицию; реализовать каждое требование отдельно; интегрировать аспекты в программный код. В примере с графическим редактором на этапе аспектной декомпозиции была выявлена сквозная функциональность — обновление дисплея. Данное требование было реализовано в аспектном модуле DisplayUpdating. В этом аспекте определяется срез точек move(..), которые включают в себя точки выполнения программы, после которых будет встроена требуемая сквозная функциональность. На рисунке 11 схематично изображен аспект, находящийся на срезе модели графического редактора.

Интеграция аспектов (weaving) происходит в момент компиляции. Модель построения готовой программной системы при использовании компилятора ajc изображена на рисунке 12.


Рисунок 12. Интеграция аспектов.

После компиляции получаем готовую систему с интегрированной сквозной функциональностью по правилам, описанным в аспектных модулях.

Другие реализации АОП

В данный момент, кроме представленного выше языка AspectJ, известно несколько систем, реализующих принципы АОП:

Критерии сравнения аспектной реализации с объектно-ориентированной

Для объективного анализа представленных вариантов использования аспектных реализаций по сравнению с объектно-ориентированными реализациями необходимо ввести критерии сравнения этих реализаций.

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

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

Второй вид метрик — это метрики, которым соответствует порядковая шкала. Она позволяет ранжировать некоторые характеристики путем сравнения с опорными значениями, т.е. измерение по этой шкале фактически определяет взаимное положение конкретных модулей или программных систем. Для объекта измерения устанавливается приоритетность признаков.

Третий вид метрик — это метрики, которым соответствует номинальная или категорированная шкала. Данный вид метрик характеризует наличие рассматриваемого свойства или признака у объекта, в частности у программного модуля, без учета градаций по данному признаку. Фиксируется, есть или нет данное качество в зависимости от наличия рассматриваемого показателя у комплекса программ (например, наличие структурирования, гибкости, простоты освоения). Например, такую характеристику как сложность модуля можно количественно оценить значениями этой метрики: [нетрудная для понимания], [умеренно трудная для понимания], [трудная для понимания], [очень трудная для понимания].

Существующие качественные оценки программных систем можно сгруппировать по нескольким интересующим в данной работе направлениям:

Топологическая и информационная сложность программного модуля

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

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

Далее Холстед вводит сложность программы (Halstead Difficulty), которая вычисляется как:

HDiff = NUOprtr/2* (NOprnd / NUOprnd)

Используя HDiff, Холстед вводит оценку HEff (Halstead Effort) Heff = HDiff* HPVol, с помощью которой описываются усилия программиста при разработке.

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

Для вычисления цикломатического числа Маккейба CC (Cyclomatic Complexity) применяется формула:

CC = L — N + 2P,

где L — число дуг ориентированного графа;

N — число вершин;

P — число компонентов связности.

К метрикам сложности также относится:

В эту группу метрик также попадают базовые метрики:

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

Данная группа метрик будет собрана при помощи автоматизированного программного средства — TogetherJ. Настоящая (6 версия) продукта TogetherJ не поддерживает сбора метрик для аспектных модулей языка AspectJ, следовательно, в случае аспектной реализации собранные метрики будут подсчитываться только для компонентов, реализованных на языке Java, то есть для "чистых" компонентов без учета сквозной функциональности. Можно утверждать, что данная грубая оценка будет верна в случае небольших тестовых проектов, где отношение разницы кода объектной реализации и кода компонента к коду сквозной функциональности (например, в терминах LOC) не будет значительно превышать 1. Это говорит о небольшом количестве вынесенной сквозной функциональности, достаточном, тем не менее, для того, чтобы оценить модульность полученных компонентов в терминах топологической и информационной сложности. Сравнение метрик компонента до и после вынесения сквозной функциональности позволит оценить, в какую сторону изменилась реализация данного компонента. Для учета топологической сложности вынесенной сквозной функциональности и для анализа больших реальных проектов необходимо вырабатывать другие метрики анализа.

Уровень языковых средств и их применения

Для оценки степени применимости аспектного подхода к конкретной программной системе введем следующую метрику:


Рисунок 13.

где P — размер кода программной системы без применения аспектного подхода;

Q — размер кода компонентов, реализующих основную логику системы;

Z — размер кода аспектных модулей, реализующих сквозную функциональность.

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

Трудность восприятия и понимания программных текстов

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

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

Варианты применения АОП на разных этапах ЖЦ

Приведем варианты использования аспектно-ориентированного подхода на различных этапах жизненного цикла программной системы.

Использование АОП на этапе проектирования

Как отмечает Дейкстра, "Способ управления сложными системами был известен еще в древности — divide et imperia (разделяй и властвуй)" [10]. При проектировании сложной программной системы необходимо разделять ее на все меньшие и меньшие подсистемы, каждую из которых можно совершенствовать независимо. Декомпозиция вызвана сложностью программирования системы, поскольку именно эта сложность вынуждает делить пространство состояний системы. При построении системы необходимо провести алгоритмическую декомпозицию, объектно-ориентированную декомпозицию и аспектно-ориентированную. Объектно-ориентированная декомпозиция позволяет выделить из требований компоненты, описывающие проблемную область, алгоритмическая — описать взаимодействие этих компонентов и сконцентрировать внимание на порядке происходящих событий [9]. Аспектно-ориентированная декомпозиция на этапе анализа и проектирования позволяет выделить сквозную функциональность на разных уровнях абстракции и локализовать ее в отдельных модулях-аспектах. Такие аспекты являются неотъемлемой частью результирующей программной системы.

Реализация протокола взаимодействия объектов для шаблонов проектирования

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

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

Каталог шаблонов проектирования [6] включает в себя 23 известных шаблона, их назначение, сценарии, иллюстрирующие задачи проектирования, и то, как они решаются при помощи конкретного шаблона, описание ситуаций, в которых можно применить шаблон, и графическое представление компонентов в шаблоне. В работе [5] проведен сравнительный анализ аспектной реализации шаблонов проектирования и сравнение с "традиционной" объектно-ориентированной реализацией. Улучшения выражаются в терминах метрик с номинальной шкалой — модульности кода, готовности к повторному использованию, и понятности кода. Под модульностью кода в этой работе подразумевается локализация сквозной функциональности (в данном случае абстрактной модели шаблона) в коде аспекта. Под готовностью кода к повторному использованию — возможность использовать компоненты-участники и аспектные модули в других контекстах, так как для шаблона был разработан общий протокол взаимодействия компонентов-участников. Под понятностью кода подразумевается то, что код реализации не является запутанным. Это означает, что компоненты-участники могут быть заняты в нескольких шаблонах проектирования, шаблоны могут разделять между собой компоненты-участники. Также в этой работе введена еще одна метрика — способность к встраиванию, которая характеризует возможность удалить или добавить в систему аспектный код, реализующий данный шаблон, без усилий и модификации компонентов-участников шаблона, что говорит о готовности данных компонентов к повторному использованию в других контекстах.

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

Диаграмма классов шаблона Observer представлена на рисунке 14.


Рисунок 14. Диаграмма классов шаблона Observer.

Составными частями данного шаблона являются:

Затруднения при использовании данного шаблона вытекают из реализуемой в нем модели, построенной на обмене сообщениями. Можно использовать как специальную, так и универсальную стратегию рассылки сообщений, каждая из которых имеет свои недостатки.

При использовании универсальной стратегии сложнее определить, что конкретно происходит с наблюдаемым объектом, поскольку сообщения носят универсальный характер. Кроме того, универсальная стратегия характеризуется избыточным потоком сообщений — некоторые события, пересылаемые наблюдателям, последними никак не обрабатываются, а лишь влекут за собой увеличение накладных расходов. В качестве примера применения данного шаблона и демонстрации увеличения накладных расходов при обработке лишних сообщений можно привести пример графического редактора, обсуждаемого ранее. Вызов обновления дисплея Display.update(this) может происходить как при вызове метода установки точки — setX, так и при вызове setXY или moveBy, которые инкапсулируют в себе setX. Наконец, универсальная стратегия требует дополнительных затрат на разработку классов наблюдателей, так как им нужно анализировать сообщения и вычленять из них необходимую информацию.

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

Для "традиционной" объектно-ориентированной реализации проведен расчет метрических характеристик при помощи автоматизированного программного средства. Значения представлены в таблице 3.

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

Общими частями в структуре шаблона Observer являются:


Рисунок 15. Модель шаблона Observer.

Каждая конкретная реализация шаблона определяет собственный вид отслеживания взаимосвязей между объектами. В конкретной реализации абстрактного аспекта определяется следующее:

На рисунке 15 представлено две различных реализации шаблона Observer с использованием компонентов Point, Line и Screen. В первом случае компоненты Point и Line играют роль Subject, а компонент Screen играет роль Observer. Во втором – Point и Line играют роль Observer, а Display — Observer и Subject.

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

Для аспектно-ориентированной реализации также проведен расчет метрических характеристик, и результат расчета представлен в таблице 3.

Метрика Объектно-ориентированная реализация Аспектно-ориентированная реализация
Цикломатическая сложность (CC) 11 7
Сложность программы (HDiff) 16 9
Усилия разработчика (HEff) 7319 3563
Длина программы (HPLen) 283 190
Словарь программы (HPVoc) 37 35
Объем программы (HPVol) 1434 898
Количество строк кода (LOC) 109 56
Недостаток связности методов (LOCOM1) 15 3
Недостаток связности методов (LOCOM2) 68 57
Недостаток связности методов (LOCOM3) 75 66
Количество классов (NOC) 5 3
Общее число операндов в программе (Noprnd) 149 96
Общее число операторов в программе (Noprtr) 134 94
Количество вызываемых удаленных методов (NORM) 9 5
Число уникальных операндов программы (NUOprnd) 33 31
Число уникальных операторов программы (NUOprtr) 7 4
Отклик на класс (RFC) 17 7
Взвешенная насыщенность класса (WMPC1) 11 7
Взвешенная насыщенность класса (WMPC2) 18 13

Степень применимости аспектного подхода: Adaptability = 1,308

Реализация авторизованного доступа к данным

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

В качестве демонстрационного примера используем расширение JAAS (Java Authentication and Authorization service) платформы Java, которое разрабатывалось для обеспечения стандартного, основанного на аутентификации пользователей, способа разграничения доступа к ресурсам. API для регистрации и завершения работы в системе, предоставляемые JAAS, обеспечивают стандартные средства аутентификации пользователей и передачу информации о контексте защиты и полномочиях. Модель поставщика службы JAAS позволяет работать с различными базовыми средствами аутентификации и авторизации.

Рассмотрим простую банковскую систему

Ресурсами, которые нуждаются в защите, являются сервера счета пользователей. Следовательно, перед вызовом методов каждого из этих объектов необходимо удостоверится в том, что пользователь имеет право работать с данной системой. Для этого перед каждым вызовом метода защищаемого объекта необходимо проверить права пользователя, если этого еще не было сделано. Объектом, инкапсулирующим API JAAS будет объект Autentificator:


Рисунок 16. Модель банковской системы.

Рисунок 16. Модель банковской системы.
public class Autentificator 
{
  private static Subject _authenticatedSubject=null;
  public static void perform()
  {
    if(_authenticatedSubject != null) 
    {
      return;
    }
    try 
    {
      authenticate();
    } 
    catch (LoginException ex) 
    {
      throw new AuthenticationException(ex);
    }
  }
  private static void authenticate() throws LoginException 
  {
    LoginContext lc = new LoginContext("Sample",
                      new TextCallbackHandler());
    lc.login();
    _authenticatedSubject = lc.getSubject();
  }
}

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

Для некоторых систем выдвигаются более сильные требования по безопасности, когда кроме аутентификации необходимо проверять права пользователя на вызов конкретного метода — авторизацию. Вызов, например, метода кредитования объекта Account может выглядеть следующим образом:

Subject.doAsPrivileged(authenticatedSubject,
          new PrivilegedAction() {
            public Object run() {
              account1.credit(300);
              return null;
            }}, null);
      try {
        Subject
          .doAsPrivileged(authenticatedSubject,
            new PrivilegedExceptionAction() {
              public Object run() throws Exception {
                account1.debit(200);
                return null;
              }}, null);
      } catch (PrivilegedActionException ex) {
        Throwable cause = ex.getCause();
        if (cause instanceof InsufficientBalanceException) {
          throw (InsufficientBalanceException)ex.getCause();
      }
}

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

При использовании АОП подобную сквозную функциональность можно на этапе проектирования системы вынести в аспектный модуль.

На рисунке 17 представлена диаграмма аспектов модели безопасности. В абстрактный аспект AbstractAuthAspect выносится логика аутентификации и авторизации. Определяется набор инструкций языка, который должен быть вставлен до защищаемого кода — адвайзер аутентификации before():authOperations(), в котором будет выполняться закрытый метод данного аспекта — authenticate().

В аспекте определяется набор инструкций, который будет непосредственно вызван вместо защищаемого кода — адвайзер авторизации Object around(): authOperations(), который инкапсулирует в себе вызываемый код, что позволяет перед его выполнением выполнить код службы авторизации, и в случае успеха авторизации позволить выполнить защищаемый код. Данный код будет выполнен вместо точек выполнения программы из набора authOperations. В абстрактном аспекте определен принципал — authenticatedSubject, для которого в конкретном аспекте будут получаться права путем вызова переопределенного метода getPermission(). В конкретном аспекте должен быть определен набор точек связывания — точек интеграции аспектного кода (authOperations). В случае BankingAuthAspect такими точками являются вызовы публичных методов компонентов Account, Server и Bank. Во все эти методы интегрируется функциональность, описанная в аспектном модуле. При этом сами компоненты будут содержать только свою сущность без кода, не относящегося к ним по смыслу, что значительно улучшает модульность системы и повышает возможность повторного использования кода. Вся сквозная функциональность будет вынесена в аспектный модуль, и может быть повторно использована в других контекстах путем наследования аспектов.


Рисунок 17. Модель безопасности в виде аспектов.

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

Метрика Объектно-ориентированная реализация Аспектно-ориентированная реализация
Цикломатическая сложность (CC) 8 5
Сложность программы (HDiff) 22 11
Усилия разработчика (HEff) 7314 1255
Длина программы (HPLen) 212 90
Словарь программы (HPVoc) 32 16
Объем программы (HPVol) 934 328
Количество строк кода (LOC) 148 77
Недостаток связности методов (LOCOM1) 8 8
Недостаток связности методов (LOCOM2) 60 60
Недостаток связности методов (LOCOM3) 0 0
Количество классов (NOC) 6 8
Общее число операндов в программе (Noprnd) 99 50
Общее число операторов в программе (Noprtr) 113 40
Количество вызываемых удаленных методов (NORM) 10 8
Число уникальных операндов программы (NUOprnd) 23 12
Число уникальных операторов программы (NUOprtr) 9 5
Отклик на класс (RFC) 20 19
Взвешенная насыщенность класса (WMPC1) 8 5
Взвешенная насыщенность класса (WMPC2) 8 8

Степень применимости аспектного подхода: Adaptability = 1,73

Ведение журнала событий

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

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

public class Main {
    public Main() {
    }
    public void foo() {
      Logger.entry("foo()");
      System.out.println("foo1");
      Logger.exit("foo()");
    }
    public void foo(int i) {
      Logger.entry("foo(int)");
      System.out.println(i++);
      Logger.exit("foo(int)");
    }
    public double bar(double x, double y) {
      Logger.entry("bar(double, double)");
      double result = x * y;
      Logger.exit("bar(double, double)");
      return result;
    }
    public static void main(String[] args) {
      Logger.entry("main(String[])");
      Main main1 = new Main();
      main1.foo();
      System.out.println(main1.bar(1.2, 1.3));
      main1.foo(10);
      Logger.exit("main(String[])");
    }
}

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

public aspect AutoLog {
  pointcut publicMethods(): execution(public * *..*(..));
  pointcut logObjectCalls(): execution(* Logger.*(..));
  pointcut loggableCalls(): publicMethods() && (!logObjectCalls());
  before(): loggableCalls() {
    Logger.entry(thisJoinPoint.getSignature().toString());
  }
  after(): loggableCalls() {
    Logger.exit(thisJoinPoint.getSignature().toString());
  }
}

В таблице 5 приведены значения метрических характеристик для двух типов реализаций.

Метрика Объектно-ориентированная реализация Аспектно-ориентированная реализация
Цикломатическая сложность (CC) 7 5
Сложность программы (HDiff) 15 5
Усилия разработчика (HEff) 6258 1001
Длина программы (HPLen) 172 44
Словарь программы (HPVoc) 31 23
Объем программы (HPVol) 830 199
Количество строк кода (LOC) 67 20
Недостаток связности методов (LOCOM1) 1 -
Недостаток связности методов (LOCOM2) 33 -
Недостаток связности методов (LOCOM3) 0 -
Количество классов (NOC) 2 1
Общее число операндов в программе (Noprnd) 92 23
Общее число операторов в программе (Noprtr) 80 21
Количество вызываемых удаленных методов (NORM) 7 2
Число уникальных операндов программы (NUOprnd) 24 16
Число уникальных операторов программы (NUOprtr) 7 7
Отклик на класс (RFC) 10 7
Взвешенная насыщенность класса (WMPC1) 7 5
Взвешенная насыщенность класса (WMPC2) 9 9

Степень применимости аспектного подхода: Adaptability = 2.9

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

Использование АОП на этапе разработки системы

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

Профилирование

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

Профилировщик позволяет получить следующую информацию о процессе выполнения программы:

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

public class Profiler {
  public static boolean DEBUG_MODE = false;
  private static Map profiles = new Hashtable();
  public static void TIMING(String pointName){
    if (!DEBUG_MODE) return;
    long ts = System.currentTimeMillis();
    Object profile = profiles.get(pointName);
    if (profile==null){
      profiles.put(pointName,new Long(ts));
    }else{
      profiles.remove(pointName);
      ts = ts-((Long)profile).longValue();
      System.out.println("Profile of [" + pointName + "] is
"+ts+" ms");
    }
  }
}

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

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

public aspect ProfilerAspect {
  public pointcut appMethod() :
    call(public * Application.*(..)) ;
  void around() : appMethod() {
    long ts = System.currentTimeMillis();
    proceed();
    ts = System.currentTimeMillis()-ts;
    Signature sig = thisJoinPointStaticPart.getSignature();
   System.out.println("Profile of [" + sig.getName() + "] is
 "+ts+" ms");
  }
}

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

В таблице 6 приведены значения метрических характеристик для двух типов реализаций.

Метрика Объектно-ориентированная реализация Аспектно-ориентированная реализация
Цикломатическая сложность (CC) 63 62
Сложность программы (HDiff) 93 79
Усилия разработчика (HEff) 587453 580420
Длина программы (HPLen) 1413 1291
Словарь программы (HPVoc) 166 166
Объем программы (HPVol) 9857 9210
Количество строк кода (LOC) 362 329
Недостаток связности методов (LOCOM1) 66 66
Недостаток связности методов (LOCOM2) 92 92
Недостаток связности методов (LOCOM3) 0 0
Количество классов (NOC) 4 3
Общее число операндов в программе (Noprnd) 797 732
Общее число операторов в программе (Noprtr) 616 559
Количество вызываемых удаленных методов (NORM) 7 5
Число уникальных операндов программы (NUOprnd) 139 139
Число уникальных операторов программы (NUOprtr) 27 27
Отклик на класс (RFC) 14 14
Взвешенная насыщенность класса (WMPC1) 63 63
Взвешенная насыщенность класса (WMPC2) 58 58

Степень применимости аспектного подхода: Adaptability = 2.8

Трассировка

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

Используя принципы АОП, легко можно добавить логику по трассировке компонентов системы в программный код. Например, при работе с БД при поиске ошибки можно создать аспект, который будет складывать в трассировочный файл выполняемые системой SQL запросы.

aspect DatabaseDebugging{
  private interface TypesDebugged{}
  declare parents : DataCollection1 ||
         DataCollection2 ||
           ...
 DataCollectionN implements TypesDebugged;
 pointcut queryExecution(String sql):
  call(* Statement.*(String))
  this(TypesDebugged)
  args(sql);
 before(String sql): queryExecution(sql){
     System.out.println(sql);
 }
}

Для полного анализа событий, проходящих в системе, можно создать несколько аспектов, инкапсулирующих в себе необходимую логику. На рисунке 18 представлена иерархия аспектов. Абстрактный аспект Trace содержит в себе логику трассировки в зависимости от уровня диагностики. Аспект TraceMyClasses определяет трассируемые классы.


Рисунок 18. Трассировочные аспекты.

В таблице 7 приведены значения метрических характеристик для двух типов реализаций.

Метрика Объектно-ориентированная реализация Аспектно-ориентированная реализация
Цикломатическая сложность (CC) 16 7
Сложность программы (HDiff) 47 21
Усилия разработчика (HEff) 33410 6697
Длина программы (HPLen) 693 264
Словарь программы (HPVoc) 46 31
Объем программы (HPVol) 3730 1247
Количество строк кода (LOC) 289 85
Недостаток связности методов (LOCOM1) 37 9
Недостаток связности методов (LOCOM2) 73 43
Недостаток связности методов (LOCOM3) 60 25
Количество классов (NOC) 6 4
Общее число операндов в программе (Noprnd) 392 142
Общее число операторов в программе (Noprtr) 301 122
Количество вызываемых удаленных методов (NORM) 10 10
Число уникальных операндов программы (NUOprnd) 38 26
Число уникальных операторов программы (NUOprtr) 11 7
Отклик на класс (RFC) 19 11
Взвешенная насыщенность класса (WMPC1) 16 7
Взвешенная насыщенность класса (WMPC2) 26 13

Степень применимости аспектного подхода: Adaptability = 2.25

Соблюдение контрактов

При оценке новых методов и средств разработки ПО обычно ориентируются на их производительность. Объектные технологии действительно могут существенно повысить производительность, при этом, однако, нельзя упускать из виду качество создаваемого ПО. Качественное ПО — это, прежде всего надежное ПО. Надежность — это способность системы функционировать в соответствии со спецификацией ("корректность") и при этом успешно справляться с возникающими ненормальными ситуациями ("устойчивость" — robustness). Другими словами, надежная программа не содержит ошибок.

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

Если разработчик хочет быть уверенным в надлежащей работе объектно-ориентированного ПО, ему требуется систематический подход к специфицированию и реализации объектно-ориентированных программных сущностей и их взаимосвязей в программной системе. Такой подход существует и называется rонтрактным проектированием ("Design by Contract"), и в его рамках программная система рассматривается в виде множества взаимодействующих компонентов, чьи отношения строятся на основе точно определенной спецификации взаимных обязательств — контрактов, которые являются сквозной функциональностью.

При использовании аспектного подхода достаточно просто придерживаться принципов контрактного программирования. Можно выделить два подхода к соблюдению контрактов — на этапе выполнения и на этапе компиляции. На рисунке 19 представлена схема применение системных соглашений при использовании AspectJ. Соглашения описываются в аспектных модулях и воздействуют на компоненты системы посредством аспектов. В момент компиляции происходит интеграция аспектов времени выполнения в компоненты и выдается информация о нарушениях соглашений времени компиляции.


Рисунок 19. Схема применения системных соглашений при использовании AspectJ.

Контракты на этапе выполнения

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

Предусловие – это условие, которое налагается на входные данные для некоторого логически выделенного блока в программе. Например, для входных данных метода. Постусловие аналогично предусловию, но проверяется при возврате значения из блока "наружу" для проверки целостности результата. Инвариант же – это условие, которое должно выполняться всегда в течение какой-либо итерации (например, внутри цикла). Инварианты класса — это ограничения, характеризующие его семантику с точки зрения непротиворечивости. Это понятие является особенно важным в контексте управления конфигурацией и регрессионного тестирования — оно описывает более глубокие свойства класса: здесь не просто устанавливается характеристика, верная для определенного момента эволюции класса, но и налагаются ограничения на все последующие изменения.

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

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

public abstract aspect InvariantProtocol{
   protected interface InvariantCheck{}
   abstract protected pointcut methodCall(InvariantCheck obj);
   abstract protected void assert(InvariantCheck obj);
   after (InvariantCheck obj): methodCall(obj){
     assert(obj);
   }
   before (InvariantCheck obj): methodCall(obj){
     assert(obj);
   }
}
aspect InvariantProtocolImp {
   declare parents TestClass implements InvariantCheck;
   protected pointcut methodCall(InvariantCheck obj):
     target(obj) && execution(* .* (. .));
   protected void assert(InvariantCheck obj){
     TestClass testClass = (TestClass) obj;
     // проверка инварианта для класса TestClass
   }
}

Конкретная реализация проверки инвариантов представлена в виде аспекта InvariantProtocolImpl, который реализует проверку инварианта для некого TestClass. Этот аспект определяет метод, в котором будет проверяться инвариант и точку выполнения программы, в которой будет вызываться проверка. В данном случае точка, в которой будет проверяться инвариант класса, — это вызов любого метода класса TestClass. Идеи проверки соответствия данных контракту достаточно просты, и каждый программист их так или иначе выполняет. Простейшая проверка внутри функции, обрабатывающей текстовые строки, на то, не "подсунули" ли ей указатель на строку, равный null, является проверкой предусловия. Другое дело, что если это оформить в виде обычного условного оператора, то читающий программу человек сразу не догадается, что к общему алгоритму работы эта проверка условия не имеет никакого отношения, а при оформлении такой проверки в виде аспектов основной код не будет обременен такими проверками вообще.

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

public aspect DetectUISingleThreadRuleViolationAspect 
{
  pointcut viewMethodCalls()
    : call(* javax..JComponent+.*(..));
  pointcut modelMethodCalls()
    : call(* javax..*Model+.*(..))
    || call(* javax.swing.text.Document+.*(..));
  pointcut uiMethodCalls()
    : viewMethodCalls() || modelMethodCalls();
  before() : uiMethodCalls() && if(!EventQueue.isDispatchThread()) 
  {
    System.err.println(
      "Нарушение: Метод библиотеки Swing вызван из не-AWT нити"
      + "\nВызывающий метод: "
      + thisJoinPointStaticPart.getSignature()
      + "\nВызывающий объект: "
      + thisEnclosingJoinPointStaticPart.getSignature()
      + "\nИсточник: "
      + thisJoinPointStaticPart.getSourceLocation()
      + "\nНить: " + Thread.currentThread() +"\n");
  }
}

В примере аспекта DetectUISingleThreadRuleViolationAspect обнаруживаются любые вызовы компонентов или моделей библиотеки Swingязыка Java, при этом перед вызовом этого метода выдается сообщение об ошибке.

Контракты на этапе компиляции

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

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

public aspect DetectLogUsage 
{
  declare warning : call(* Logger.log(..))
    : "Consider Logger.logp() instead";
}

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

aspect DetectSystemOutErrorUsage 
{
  declare warning : (get(* System.out) || get(* System.err))
    && within(compile_time..*)
    : "Consider Logger.logp() instead";
}

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

public aspect DetectEJBViolations 
{
  pointcut uiCalls() : call(* java.awt.*+.*(..));
  declare error : uiCalls() && within(EnterpriseBean+)
    : "UI calls are not allowed from EJB beans.See EJB 2.0
  specification section 24.1.2";
  before() : uiCalls() && cflow(call(* EnterpriseBean+.*(..))) 
  {
    System.out.println("Detected call to AWT from enterprise bean");
    System.out.println("See EJB 2.0 specification section 24.1.2");
    Thread.dumpStack();
  }
  pointcut staticMemberAccess() : set(static * EnterpriseBean+.*);
  declare error : staticMemberAccess()
    : "EJBs are not allowed to have non-final static variables.
  See EJB 2.0 specification section 24.1.2";
}

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

public class Product 
{
  public Product() 
  {
    // реализация конструктора
  }
  public void configure() 
  {
    // configuration implemntation
  }

   //методы классаProduct
 
  static aspect FlagAccessViolation 
  {
    pointcut factoryAccessViolation()
      : call(Product.new(..)) && !within(ProductFactory+);
    pointcut configuratorAccessViolation()
      : call(* Product.configure(..)) &&
      !within(ProductConfigurator+);
      declare error
        : factoryAccessViolation() ||
        configuratorAccessViolation()
        : "Нарушение доступа.
        ТолькоProductFactory.createProduct() может создавать Product";
  }
}

На протяжении нескольких лет сообщество программистов выработало ряд удачных подходов к решению различных проблем. Описав подобные подходы в виде аспектов, можно отслеживать их выполнение во всем проекте. Например, после добавления в проект аспекта DetectPublicAccessMembers при сборке будет выдаваться сообщение о попытках обратиться к не final-полю класса или установить значение в любое public-поле без использования специально предназначенных для этого геттеров или сеттеров.

aspect DetectPublicAccessMembers 
{
  declare warning :
    get(public !final * *) || set(public * *) :
    "Please consider using non-public access";
}

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

В настоящий момент существуют следующие подходы к соблюдению контрактов в проекте:

При использовании существующих в настоящий момент подходов к соблюдению контрактов возникают следующие проблемы:

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

Управление объектами в многопоточной среде

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

Используя АОП и возможность окружать код компонентов сквозными инструкциями (around advice), достаточно легко можно окружить объекты сквозной функциональностью — защитой в многопоточной среде.

public abstract aspect AbstractThreadSafe
{
  abstract pointcut myClass(Object obj);
  pointcut safeMethods(Object obj): myClass(obj)
    && execution(* *(..));
  Object around (Object obj): safeMethods(obj)
  {
    synchronized(this)
    {
      return proceed(obj);
    }
  }
}

Аспекты, расширяющие данный абстрактный аспект, должны определить набор классов, методы которых необходимо защитить от совместного доступа. Так как для примеров используется язык AspectJ, то в примере используется примитив синхронизации языка Java. Однако вполне возможно написание собственного примитива синхронизации, и благодаря аспектному подходу код такого механизма синхронизации не будет "перемешан" с кодом бизнес-компонентов.

Визуализация алгоритмов

АОП предлагает подход к интеграции кода в уже существующий код. Рассмотрим простую задачу: пять философов сидят за круглым обеденным столом. Между каждыми двумя философами есть одна вилка, которая может быть общей для них. Каждый философ может либо думать, не требуя вилок, либо есть, используя две соседние вилки, расположенные по одну и по другую стороны от него. Время обеих фаз "думать" и "есть" — произвольная конечная величина. При отсутствии двух свободных вилок, необходимых философу для еды, последний переходит в состояние ожидания.

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


Рисунок 20.

События, происходящие во время работы алгоритма, могут быть следующими:

Первый путь решения проблемы — это по описанным событиям реализовать прокси-объект, пропускающий через себя все события, и по необходимому событию выполнять соответствующие действия по интерпретации. Подходы к созданию прокси-объекта описаны в [6]. Затем такой объект необходимо интегрировать в код алгоритма.

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

В таблице 8 приведены значения метрических характеристик для двух типов реализаций.

Метрика Объектно-ориентированная реализация Аспектно-ориентированная реализация
Цикломатическая сложность (CC) 35 27
Сложность
программы (HDiff)
85 26
Усилия разработчика (HEff) 164206 50201
Длина программы (HPLen) 1618 331
Словарь программы (HPVoc) 118 92
Объем программы (HPVol) 10110 2126
Количество строк кода (LOC) 563 119
Недостаток связности методов (LOCOM1) 248 49
Недостаток связности методов (LOCOM2) 94 74
Недостаток связности методов (LOCOM3) 82 74
Количество классов (NOC) 10 2
Общее число операндов в программе (Noprnd) 804 166
Общее число операторов в программе (Noprtr) 814 165
Количество вызываемых удаленных методов (NORM) 37 21
Число уникальных операндов программы (NUOprnd) 101 71
Число уникальных операторов программы (NUOprtr) 21 21
Отклик на класс (RFC) 355 36
Взвешенная насыщенность класса (WMPC1) 35 27
Взвешенная насыщенность класса (WMPC2) 52 21

Степень применимости аспектного подхода: Adaptability = 1.69

Использование АОП при поддержке существующей системы

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

Кэширование

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

Рассмотрим пример математической библиотеки, вычисляющей значение факториала:

public class MathLibrary 
{
  public static long factorial(int n)
  {
    if (n==0)
    {
      return 1;
    }
    else
    {
      return n* factorial(n-1);
    }
  }
}

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

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

public aspect OptimizeFactorial 
{
  pointcut factorialOperation(int n) :
    call(long MathLibrary.factorial(int)) && args(n);
  pointcut topLevelFactorialOperation(int n) :
    factorialOperation(n)
    && !cflowbelow(factorialOperation(int));
 
  private Map _factorialCache = new HashMap();
 
  before(int n) : topLevelFactorialOperation(n) 
  {
    System.out.println("Seeking factorial for " + n);
  }
  long around(int n) : factorialOperation(n) 
  {
    Object cachedValue = _factorialCache.get(new Integer(n));
    if (cachedValue != null) {
      System.out.println("Found cached value for " + n
        + ": " + cachedValue);
      return ((Long)cachedValue).longValue();
    }
    return proceed(n);
  }
  after(int n) returning(long result)
    : topLevelFactorialOperation(n) 
  {
    _factorialCache.put(new Integer(n), new Long(result));
  }
}

В таблице 9 приведены значения метрических характеристик для двух типов реализаций.

Метрика Объектно-ориентированная реализация Аспектно-ориентированная реализация
Цикломатическая сложность (CC) 2 2
Сложность программы (HDiff) 18 11
Усилия разработчика (HEff) 3527 1909
Длина программы (HPLen) 141 89
Словарь программы (HPVoc) 26 13
Объем программы (HPVol) 569 325
Количество строк кода (LOC) 38 21
Недостаток связности методов (LOCOM1) 3 2
Недостаток связности методов (LOCOM2) 72 47
Недостаток связности методов (LOCOM3) 69 42
Количество классов (NOC) 7 2
Общее число операндов в программе (Noprnd) 17 10
Общее число операторов в программе (Noprtr) 9 6
Количество вызываемых удаленных методов (NORM) 8 3
Число уникальных операндов программы (NUOprnd) 2 2
Число уникальных операторов программы (NUOprtr) 2 2

Степень применимости аспектного подхода: Adaptability = 1.23

Управление ресурсами

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

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

Рассмотрим детально:


Рисунок 21. Организация ресурсного пула.

Метрика Объектно-ориентированная реализация Аспектно-ориентированная реализация
Цикломатическая сложность (CC) 7 6
Сложность программы (HDiff) 33 27
Усилия разработчика (HEff) 9821 8745
Длина программы (HPLen) 356 311
Словарь программы (HPVoc) 38 37
Объем программы (HPVol) 1710 1503
Количество строк кода (LOC) 165 135
Недостаток связности методов (LOCOM1) 1 0
Недостаток связности методов (LOCOM2) 33 0
Недостаток связности методов (LOCOM3) 0 0
Количество классов (NOC) 7 6
Общее число операндов в программе (Noprnd) 177 154
Общее число операторов в программе (Noprtr) 179 157
Количество вызываемых удаленных методов (NORM) 14 13
Число уникальных операндов программы (NUOprnd) 31 31
Число уникальных операторов программы (NUOprtr) 9 9
Отклик на класс (RFC) 65 62
Взвешенная насыщенность класса (WMPC1) 7 6
Взвешенная насыщенность класса (WMPC2) 4 4

Степень применимости аспектного подхода: Adaptability = 1.11

Обработка ошибок

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

При использовании АОП обработку исключений можно рассматривать как сквозную функциональность и добавить общее поведение в систему при обработке исключения определенного типа.

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

Например, при определении абстрактного аспекта ExceptionHandlingAspect задается общая стратегия обработки ошибок в программной системе. Любой наследник этого аспекта должен определить срез точек, на которые будет действовать этот аспект, и метод обработки исключительной ситуации exceptionHandling(Throwable ex).

public aspect ExceptionHandlingAspect 
{
  public abstract pointcut exceptionJoinPoints();
  protected abstract void exceptionHandling(Throwable ex);
  after() throwing (Throwable ex): exceptionJoinPoints()
    this.exceptionHandling(ex);
  }
}


Рисунок 22. Структурированная обработка ошибок.

В Web-части распределенного приложения можно дополнить этот аспект аспектом ServletsExceptionHandlingAspect:

public aspect ServletsExceptionHandlingAspect extends ExceptionHandlingAspect
{
  protected abstract void exceptionHandling(Throwable ex)
  {
    // handling exceptions in servlets Java
  }
}

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

public aspect DistributionExceptionHandlingAspect
  extends ServletsExceptionHandlingAspect
{
  public pointcut exceptionJoinPoints():
    DistributionAspect.facadeMethodsCall();
}


Рисунок 23. Диаграмма аспектов.

В работе [12] проводится изучение применимости аспектного подхода при обработке ошибок. В данной работе исследуются два варианта некоторой программной системы — оригинальный с нормальным следованием обработчиков ошибок и проверки предусловий и постусловий, интегрированных в компоненты системы, и второй вариант системы, переписанный на AspectJ. В таблице 11 приведены результаты улучшений использования аспектного подхода в терминах метрики LOC (количество строчек кода)

без использования аспектов с использованием аспектов
Определение ошибок 2120 проверки предусловий (2120 LOC) 666 проверок постусловий (666 LOC) 620 проверок предусловий (660 LOC) 291 проверка предусловий (300 LOC)
Обработка ошибок 414 операторов catch (2070 LOC) 31 аспект обработки ошибок (200 LOC)
% от общего количества LOC 10.9% 2.9%

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

Изменение ролей объектов и иерархии классов

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

Рассмотрим пример иерархии классов

class A 
{
  void n() { print("A.n()");}
}
class B extends A 
{
  void m() { print("B.m()");}
}
class C extends B 
{
  public void x() {print("C.x()"); }
}
class D extends B 
{
  public void y() { print("D.y()");}
  public void x() {print("D.x()");}
}
class E extends C 
{
}
class F extends D 
{
  void n() {print("F.n()");}
}
class G extends B 
{
  void n() {print("G.n()");}
}
interface I 
{
  void x();
  void y();
}

Применим к этой иерархии классов аспект M:

aspect M 
{
  declare parents: C implements I;
  declare parents: D implements I;
  public void I.y() { print("I.y()");}
}

В результате получим


Рисунок 24.

Добавив аспект N, получаем, что все наследники класса B получают новый метод

aspect N 
{
  void B.n()
  {
    print("B.n()");
  }
}


Рисунок 25.

После добавления аспекта O класс D и его наследники приобретают свойства объекта G

aspect O 
{
  declare parents: D extends G;
}


Рисунок 26.

В рассмотренном выше примере реализации шаблона проектирования Observer при реализации протокола взаимодействия объектов описываются интерфейсы Subject и Observer, относительно которых строится взаимодействие этих сущностей.

public abstract aspect ObserverProtocol 
{
  protected interface Subject { }
  protected interface Observer { }
  //реализация логики шаблона проектирования
}
 
public aspect CoordinateObserver extends ObserverProtocol
{
  declare parents: Point implements Subject;
  declare parents: Screen implements Observer;
  //:
} 

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

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

Заключение

Подведем итоги.

Результаты сравнения реализаций показывают несомненный положительный эффект применении АОП на разных этапах жизненного цикла программных систем.

При оценке топологической и информационной сложности программ были получены следующие результаты:

На графике светлое значение представляет среднее значение метрики, подсчитанное для ОО-реализации, темная — аспектной. Среднее значение метрики для ОО реализации принято за 100%, значение метрики в случае аспектной реализации преобразовано в процентное отношение к метрике объектной реализации.

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


Рисунок 27.

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


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

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

По метрикам второй группы (уровень языковых средств) на всех этапах ЖЦ можно утверждать о положительном эффекте от применения аспектной декомпозиции.


Рисунок 29.

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

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

В настоящее время АОП — единственная методология, позволяющая справиться со сложностью, присущей очень большим системам.

Направления будущих исследований

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

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

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

В написании этого труда неоценимую помощь мне оказали мои руководители Журавлев Е.А. и Кирьянчиков В.А. Отдельное спасибо Изьюрову А.Л. за "жизненные примеры" применения новой парадигмы программирования.

Список литературы

  1. G. Kiczales, J. Lamping, A. Mendhekar, etc. Aspect-oriented programming. Published in proceedings of the European Conference on Object-Oriented Programming (ECOOP). Finland, Springer-Verlag LNCS 1241. June 1997.
  2. Е.А. Журавлев, В.А. Кирьянчиков. О возможности динамической интеграции аспектов в аспектно-ориентированном программировании.Изв. СПбГЭТУ (ЛЭТИ) Сер. Информатика, управление и компьютерные технологии. 2002. Вып. 3. С. 81 — 86.
  3. Е. А. Журавлев, В. Н. Павлов. Об одном подходе к реализации Аспектно-ориентированного программирования. Изв. СПбГЭТУ (ЛЭТИ) Сер. Информатика, управление и компьютерные технологии. 2003
  4. I. Kiselev. Aspect-Oriented Programming with AspectJ. Indianapolis, IN, USA: SAMS Publishing, 2002.
  5. J. Hannemann, G. Kiczales. Design pattern implementations in Java and AspectJ OOPSLA 02, New York, USA, November 2002. P. 161 — 173.
  6. Э. Гамма, Р. Хелм, Р. Джонсон, Дж. Влиссидес. Приемы объектно-ориентированного проектирования. Паттерны проектирования. Издательство Питер, Санкт-Петербург, 2001.
  7. S. Stelting, O. Maassen. Применение шаблонов Java. Библиотека профессионала.Издательство Вильямс, Санкт-Петербург, 2002
  8. М. Холстед. Начала науки о программах. Москва, 1981
  9. Г. Буч. Объектно-ориентированный анализ и проектирование. Издательство Бином, Невский диалект, Санкт-Петербург, 1999.
  10. E. Dijkstra. Programming Considered as a Human Activity. Classics in Software Engineering. New York, Yourdon Press, 1979.
  11. B. Meyer, Applying Design by Contract, Prentive Hall, 1992
  12. M. Lippert, C Videira Lopes. A Study on Exception Detection and Handling Using Aspect-Oriented Programming.Xerox PARCTechnical Report P9910229 CSL-99-1, Dec. 99
  13. O. Hachani, D. Bardou. Using aspect-oriented programming for design patterns implementation. Equipe SIGMA, LSR-IMAG, 38402 Saint Martin d'Heres Cedex, France.
  14. M. Aksit, L. Bergmans, and S. Vural. An Object-Oriented Language-Database Integration Model: The Composition-Filters Approach. In Proceedings of the ECOOP'92 Conference, LNCS 615, Springer-Verlag,1992
  15. K.Leiberherr. Component Enhancement: An Adaptive Reusability Mechanism for Groups of Collaborating Classes. In Information Processing'92, 12th World Computer Congress, Madrid, Spain, J. van Leeuwen (Ed.), Elsevier, 1992, pp.179-185
  16. Krzysztof Czarnecki, Ulrich Eisenecker Generative Programming: Methods, Tools, and Applications, Addison-Wesley, Paperback, Published June 2000.
  17. The AspectJ Programming Guide1998-2002, Xerox Corporation
  18. K.Czarnecki. Generative Programming: Principles and Techniques of Software Engineering Based on Automated Configuration and Fragment-Based Component Models. PhD thesis, Technische Universitat Ilmenau, Germany, 1998. (Глава Aspect-Oriented Decomposition and Composition)
  19. Dharma Shkla, Simon Fell, Chris Sells. Aspect-Oriented Programming Enables Better Code Encapsulation and Reuse. MSDN Magazine, March 2002.
  20. Bruno Schaffer Design and Implementation of Smalltalk Mixin Classes. Ubilab Technical Report 98.11.1 Universitдtsstrasse 84 CH-8033 ZurichSwitzerland
  21. Laddad, R. (2002). I want my aop!, part 1. JavaWorld. Avaliable at http://www.javaworld.com/javaworld/jw-01-2002/jw-0118-aspect.html.
  22. Aspect-Oriented software development network www.aosd.net
  23. Ch. Simony. The Death of Computer Languages, The Birth of Intentional Programming, Microsoft Research, 1995, http://research.microsoft.com/pubs/view.aspx?tr_id=4.
  24. Rational Software Corporation www.rational.com
  25. Homepage of the Subject-Oriented Programming Project, IBM Thomas J. Watson Research Center, YorktownHeights, New York, http://www.research.ibm.com/sop/
  26. Homepage of the TRESE Project, University of Twente, The Netherlands, http://wwwtrese.cs.utwente.nl/; also see the online tutorial on Composition Filters at http://wwwtrese.cs.utwente.nl/sina/cfom/
  27. TogetherJ official web site. http://www.togethersoft.com

Copyright © 1994-2016 ООО "К-Пресс"