Мифы и реальности АОП

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

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

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

Для примеров в этой статье я использовал AspectJ в качестве основной АОП-реализации, хотя все сказанное применимо и к другим АОП-системам, таким как Spring и JBoss. Я начну с самого распространенного мифа.

Миф 1: Технология АОП хороша только для трассировки и ведения журналов

Реальность: АОП подходит для решения многих других пересекающихся задач

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

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

На системном уровне задачи защиты, многопоточности и управления транзакциями широко реализуются с использованием АОП. Многие проблемы бизнес-логики (известные также под названием «domain-specific concern» — «зависящие от предметной области задачи») при близком рассмотрении тоже проявляют признаки пересекающихся задач и стремятся к модуляризации с применением аспектов. С другой стороны, такие модули как классы и пакеты ведут к запутыванию и рассеиванию кода в меньшем масштабе (я это называю пересечением локальных или микро задач). Аспектно-ориентированный рефакторинг может помочь решить этот тип проблем путем использования аспектов для улучшения структуры кода (в разделе Ресурсы приведены ссылки на дополнительные материалы по вопросам аспектно-ориентированного рефакторинга).

Поступательное изучение

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

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

Миф 2: АОП не решает никаких новых проблем

Реальность: Вы правы – не решает!

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

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

Два типа сложности

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

Рисунок 1. Роль АОП в уменьшении накладных расходов на реализацию

Роль АОП в уменьшении накладных расходов на реализацию

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

Миф 3: Хорошо спроектированные интерфейсы устраняют необходимость в АОП

Реальность: Хорошие интерфейсы – это прекрасно, но они не заменяют аспекты

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

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

А что если…

Стандартной реакцией на этот сценарий является вопрос «А как насчет Commons Logging?» Действительно, Commons Logging позволит вам заменить реализацию системы журналирования без больших изменений остальных частей системы. Но учитывайте, что log4j был выпущен в 2000, тогда как Commons Logging – в 2003. Сколько из нас писали надстройки для log4j до того, как был разработан Commons Logging? Не я, это точно. Кроме того, если вы видите новый API, как вы реагируете? Вы говорите «Давайте создадим надстройку над ним» или просто «Давайте его использовать»?

Когда первичной целью является решение бизнес-проблем, в большинстве случаев не очень продуктивно писать надстройку над всеми API (особенно над теми, которые не являются основными для данной бизнес-проблемы). Более того, даже если бы вы как-то нашли ресурсы, написать хороший интерфейс не так-то просто. Любой такой интерфейс с точки зрения клиента должен быть достаточно абстрактным для использования в разнообразных реализациях и достаточно точным для обеспечения унифицированного поведения всех реализаций. Если в решении абстрактность доминирует, вы столкнетесь с проблемой наименьшего общего делителя (НОД), когда мощь внутренней реализации недоступна для пользователей API. И наоборот, если доминирует точность, вы получаете негибкий API, который не вписывается во многие реализации. Все это еще больше усложняется тем, что вы не знаете о реализациях, с которыми столкнетесь в будущем. Если бы вы написали надстройку над log4j в 2000, насколько просто было бы вам сменить реализацию на стандартный API журналирования?

Если я вас еще не убедил, подумайте вот о чем. Для библиотеки Concurrency Дуга Ли (Doug Lea) нет широко доступной надстройки. Это означает, что вы должны либо написать эту надстройку самостоятельно, либо (если нужно переключиться с классов Java 5 java.util.concurrent) должны изменить каждое использование этого API на новое. С другой стороны, если вы используете аспекты, то могли бы просто изменить их, и задача была бы решена. Более подробно об этом смотрите в разделе Ресурсы.

Миф 4: Шаблоны проектирования заменяют АОП

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

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

Рассмотрим шаблон «цепочка обязанностей» (chain of responsibility — COR), который часто позиционируется как объектно-ориентированный способ модуляризации пересекающихся задач. Основная идея этого шаблона – добавить цепочку обработчиков вокруг целевой логики. Затем вы можете заключить пересекающуюся логику в обработчик. Этот шаблон на самом деле полезен, как видно из его использования в фильтрах сервлетов и обработчиках Axis. При помощи COR вы можете легко реализовать контроль производительности, управление сессиями для уровней персистентности (persistence layers), шифрование/сжатие потоков и некоторые виды защиты. Но такая модульность возможна только для уже существующей инфраструктуры. Без такой поддержки вы должны были бы реализовывать ее самостоятельно или работать без модуляризации. Например, фильтры сервлетов стали доступны только после появления спецификации «Servlet Specification 2.3». До нее каждый реализовывал индивидуальные решения.

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

Вы говорите о повышенной сложности?

Оптимальное использование шаблона COR требует наличия одного (или очень небольшого количества) служебного метода. Вот почему он хорошо работает с сервлетами и Web-службами. COR не является таким хорошим выбором, если вы хотите добавить цепочку в класс Account или Customer без нарушения дизайна самих классов. В таких ситуациях вы остались бы без хорошей поддержки ваших бизнес-классов.

Итак, что потребовалось бы для реализации COR-фильтра сервлетов? Надо отметить, немало. Во-первых, вы должны определить API фильтра, как показано в листинге 1:

Листинг 1. Интерфейс Servlet Filter
1
2
3
4
5
6
7
public interface Filter {
    public void init(FilterConfig filterConfig);
    public void destroy();
    public void doFilter(ServletRequest request,
                         ServletResponse response,
                         FilterChain chain);
}

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

Листинг 2. Основная часть реализации цепочки фильтров
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class FilterChainImpl implements FilterChain {
    private int currentPos, totalFilters;
    private Filter[] filters;
    private Servlet servlet;
  
    ...
    public void doFilter(ServletRequest request,
                         ServletResponse response)
        throws IOException, ServletException {
        if(currentPos < totalFilters) {
            Filter currentFilter = filters[currentPos++];
            currentFilter.doFilter(request, response, this);
        }
        servlet.doService(request, response);
    }
}

Затем необходим какой-нибудь способ настройки фильтров. Фрагмент XML-файла (листинг 3), добавляющего фильтр сервлетов, использует фильтр мониторинга производительности. В листинге 4 приведена реализация фильтра.

Листинг 3. Настройка фильтра мониторинга производительности
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<web-app>
    ...
    <filter>
        <filter-name>monitor</filter-name>
        <filter-class>
            com.aspectivity.servlet.filter.PerformanceMonitorFilter
         </filter-class>
    </filter>
    <filter-mapping>
        <filter-name>monitor</filter-name>
        <url-pattern>/*.do</url-pattern>
    </filter-mapping>
    ...
</web-app>

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

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

Листинг 4. Реализация фильтра мониторинга производительности
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class PerformanceMonitorFilter implements Filter {
    public void doFilter(ServletRequest req,
                       ServletResponse res,
                       FilterChain chain)
        throws IOException, ServletException {
        long startTime = System.nanoTime();
        chain.doFilter(req, res);
        long endTime = System.nanoTime();
  
        monitorAgent.record(((HttpServletRequest)req).getRequestURI(),
                            endTime - startTime);
    }
    // пустая реализация init() и destroy()
}

Теперь рассмотрим следующий аспект, реализующий ту же функциональность:

Листинг 5. Реализация аспекта для мониторинга производительности сервлета
1
2
3
4
5
6
7
8
9
10
11
public aspect ServletPerformanceMonitor {
    Object around(HttpRequest request)
        : execution(* HttpServlet.do*(..)) && args(request,..){
        long startTime = System.nanoTime();
        Object retValue = proceed(request);
        long endTime = System.nanoTime();
        monitorAgent.record(request.getRequestURI(),
                            endTime - startTime);
        return retValue;
    }
}

И это все. Нет других файлов в решении с аспектом (в частности, нет необходимости в коде для инфраструктуры). Хотя вы можете возразить, что в COR-реализации код инфраструктуры пишется только один раз, согласитесь с тем, что реализация шаблонов требует определенной степени предсказуемости. Например, хотя раньше фильтр сервлета являлся отличной идеей, его нельзя было реализовать до выхода «Servlet Specification 2.3», поддерживающей эти фильтры. Еще тяжелее предсказать будущие требования для ваших бизнес-классов.

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

Листинг 6. Реализация аспекта для мониторинга производительности RMI-вызовов
1
2
3
4
5
6
7
8
9
10
11
12
public aspect RMIPerformanceMonitor {
    Object around()
        : execution(public *
        Remote+.*(..) throws RemoteException+) {
        long startTime = System.nanoTime();
        Object retValue = proceed(request);
        long endTime = System.nanoTime();
        monitorAgent.record(thisJoinPointStaticPart,
                            endTime - startTime);
        return retValue;
    }
}

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

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

Миф 5: Динамические прокси заменяют АОП

Реальность: АОП предоставляет более простое решение, чем динамические прокси

Динамические прокси (реализация шаблона проектирования Proxy (Заместитель)) кажутся хорошей альтернативой АОП. В конце концов, вам нужно только написать метод invoke(), который выглядит имитацией advice. Сильная сторона такого подхода состоит в том, что он заключает пересекающееся поведение в объекты, инициирующие вызов (invocation object). Слабые стороны решения с динамическими прокси заключаются в следующем:

  • Вынуждает использовать сложную основанную на отражениях программную модель;
  • Ослабляет статическую проверку типов, поскольку в обработчике инициатора вызова типом объектов является только Object. Вы можете встретить этот недостаток в основанных на прокси АОП-реализациях;
  • Требует создания объектов через фабрику объектов (пока вы не замените каждый оператор new кодом для создания динамических прокси – это тоже пересекающаяся задача). Вы можете встретить этот недостаток в основанных на прокси АОП-реализациях, хотя он может быть скрыт внутри системы;
  • Создает проблему тождественности объектов: прокси и внутренний объект являются двумя различными сущностями, логически представляющими один объект. То есть, нужно ли рассматривать эти два объекта как одинаковые (например, при реализации метода equals())? Ответ зависит от цели проверки на равенство и может не подходить для всех случаев.

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

Миф 6: Прикладные интегрированные системы заменяют АОП

Реальность: Прикладные интегрированные системы имеют ограничения, отсутствующие в АОП

Прикладные интегрированные системы, такие как EJB, широко используются для модуляризации пересекающихся задач. Такие системы предлагают простой подход для решения проблем при разработке приложений: (1) понять общие возникающие проблемы в конкретной предметной области, (2) наложить ограничения на пользовательский код для совместной работы с системой и (3) реализовать выбранные задачи. Например, среда EJB использует компоненты (bean) для управления персистентностью, транзакциями, аутентификацией и авторизацией, совместной работой и т.д. С первого взгляда это выглядит хорошим подходом, но при более близком рассмотрении видны некоторые проблемы:

  • Прикладные интегрированные системы имеют крутую кривую обучения. Вы не только должны знать основы системы, но и передовой опыт и приемы. АОП тоже имеет кривую обучения. Однако вам необходимо пройти обучение только один раз и потом повторно использовать полученные знания для решения разнотипного набора проблем. Если вы полагаетесь на интегрированные системы, вам придется изучать каждую из них заново (и даже каждую значимую версию);
  • Прикладные интегрированные системы обычно предлагают подход «принять или не принять». Если вам не нравится предлагаемое системой решение, вы имеете две альтернативы: принять то, что предлагает система, или реализовать эту конкретную функциональность самостоятельно, гарантируя нормальную работу вашего решения с остальной частью системы. В частности, вы должны удовлетворить все допущения, принятые в системе. Реализация в виде подключаемых модулей может смягчить эту проблему, но, как уже было сказано, создание полезного, подключаемого интерфейса может быть сложной задачей. В контексте интегрированных систем проблема НОД проявляется довольно часто;
  • Вы все равно остаетесь наедине с любой задачей, не реализованной в вашей интегрированной системе. Например, большинство систем не реализуют такие задачи как ведение журналов, создание развитой защиты и бизнес-правил. Выполнение этих задач вручную означает, что вы будете иметь разрозненную реализацию;
  • Прикладные интегрированные системы доступны для ограниченного круга прикладных задач. Что если для вашей предметной области не существует интегрированной системы? Например, как вы будете работать с пересекающимися задачами в UI-приложении?

Аспектно-ориентированный подход рассматривает предметную область в целом и предоставляет общее решение. В АОП каждая реализация пересекающейся задачи заключается в отдельный модуль. Затем вы собираете систему, используя необходимые аспекты. Современные «легкие» интегрированные системы (такие как Spring) предлагают другой подход. Ядро системы обеспечивает такие службы как связывание объектов (через внедрение зависимостей) и оставляет пересекающуюся функциональность аспектам. Естественно, интегрированная система может предоставлять некоторые предопределенные аспекты, работающие в большинстве систем. Однако использование этих аспектов не является обязательным – вы всегда можете использовать свои собственные для удовлетворения конкретных требований.

Миф 7: Аннотации заменяют АОП

Реальность: Аннотации и АОП хорошо дополняют друг друга

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

Аннотации – это попросту способ присоединения дополнительных данных, называемых метаданными, к программным элементам. Сами по себе они не несут никакой семантики о потоке программы вокруг аннотируемых элементов. Такие инструментальные программы как инструменты обработки аннотаций (annotation processing tools — APT), специализированные компиляторы и интегрированные системы (например, Hibernate и JSF) ассоциируют значение с аннотациями, как делает и АОП. В отличие от специализированных средств АОП предлагает намного более простую программную модель для работы с аннотациями. В отличие от интегрированных систем АОП предлагает полную гибкость в ассоциировании значений с аннотациями. При правильном использовании аннотации упрощают задачу написания аспектов. В определенных случаях аспекты тоже предлагают хороший способ избежать беспорядочности аннотаций (известной также под названием аннотационный ад (annotation hell)). В общем, аннотации и АОП составляют прекрасную пару. Они не заменяют друг друга, а фактически дополняют. Более глубокое обсуждение взаимосвязей между АОП и аннотациями приведено в материалах, ссылки на которые находятся в разделе Ресурсы.

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

Миф 8: Аспекты скрывают схему работы программы

Реальность: АОП повышает уровень абстракции

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

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

Парадокс: чем выше уровень абстракции ваших классов, тем менее ясен ход работы программы. Например, использование интерфейсов усложняет понимание процесса работы вашей программы. Если у вас есть вызов объекта, статический тип которого — интерфейс, просмотр фрагмента кода не даст вам никакого представления о том, какая реализация будет вызвана. Аналогичная основная проблема абстракции, скрывающей очевидность, проявляется во многих ситуациях: каскадные таблицы стилей (Cascading Stylesheet — CSS) отделяют форматирование, но затрудняют понимание HTML-форматирования; внедрение зависимостей или инверсия управления (inversion of control — IOC) в интегрированных системах, таких как Spring, абстрагирует конфигурирование служб, но затрудняет понимание точной природы службы.

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

Целесообразный компромисс

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

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

Миф 9: Аспекты затрудняют отладку

Реальность: АОП упрощает отладку пересекающейся функциональности

Отладка требует применения правильных инструментальных средств – независимо от того, есть аспекты, или нет аспектов. Это факт. С процедурным программированием все понятно, поскольку вы можете видеть ход выполнения программы, который прямо отображается в элементах программы. Объектно-ориентированные программы, особенно хорошо написанные, требуют дополнительной работы для отображения хода выполнения. Например, вы можете видеть DAO-объект (объект доступа к данным), но, возможно, понадобится исследовать, какая была использована реализация при инициализации объекта (например, основанная на JDBC или на Hibernate). Это может потребовать некоторых поисковых действий: определить, как реализована ссылка, затем переходить по ссылкам выше, возможно в файл конфигурации (например, если вы используете интегрированную среду IOC, скажем, Spring).

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

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

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

Миф 10: Аспекты могут не работать после модификации классов

Реальность: Плохо написанные аспекты могут не работать после модификации классов

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

1
2
3
4
pointcut accountActivity() :
 execution(public void Account.credit(float))
  || execution(public void Account.debit(float))
   || execution(public float Account.getBalance());

Теперь допустим, что один из методов прошел через рефакторинг «переименование метода». Например, вы переименовали методы debit() в классе Account в withdraw(). В результате advice (фрагмент кода для вставки) в срезе точек accountActivity() не будет применен в методе withdraw(). Вы можете справиться с этой проблемой по-разному. Во-первых, вы можете попробовать написать более стабильный pointcut, использующий внутренние характеристики точек присоединения. Например, вы можете определить pointcut accountActivity() следующим образом:

1
2
pointcut accountActivity() : execution(public void Account.*(..))
   && !execution(public String Account.toString(..));

Вторым вариантом могло бы быть использование такого инструментального средства как сравнение пересечений в AspectJ, показывающего, как изменилось взаимодействие аспекта с классами. На рисунке 2 показана копия экрана, на котором отображены пересекающиеся ссылки для переименованного метода, нарушившего работу advice:

Рисунок 2. Сравнение пересечений отслеживает воздействие на аспект модификации класса

Сравнение пересечений отслеживает воздействие на аспект модификации класса

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

Миф 11: Нельзя выполнить модульный тест аспектов

Реальность: Аспекты могут быть протестированы также легко, как и классы

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

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

Миф 12: АОП-реализации не требуют нового языка

Реальность: Каждая АОП-реализация использует новый язык

Ну ладно, АОП – это хорошо и даже полезно. Но действительно ли нам нужен новый язык для создания АОП-реализаций? Разработчики, которые поверхностно знакомы с основанными на XML или на аннотациях АОП-реализациями (например, Spring AOP, JBoss AOP или AspectWerkz), обычно отвечают «нет». На высоком уровне кажется, что эти реализации обеспечивают поддержку АОП без нового языка.

На самом деле каждая АОП-реализация использует новый язык и единственным отличием является разновидность используемого языка. Если традиционный синтаксис AspectJ использует прямые расширения базового языка, то другие реализации используют язык, основанный на XML или на аннотациях. Рассмотрим таблицу 1, в которой вы можете увидеть определение pointcut, написанное для разных АОП-реализаций.

Таблица 1. Определение pointcut в АОП-реализациях

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

Миф 13: АОП слишком сложен

Реальность: Да, но все-таки проще, чем другие альтернативы!

Если вы посмотрите на приведенный ниже pointcut, естественно, что вы придете в уныние от сложности синтаксиса (особенно при отсутствии большого опыта в синтаксисе pointcut, или без подсказки кого-то более опытного):

1
2
3
4
cflow(execution(* HttpServet.do*(..)))
&& call
(public * Remote+.*(..) throws RemoteException+)
&& target(remoteService)

Однако если вы потратите всего лишь несколько часов на изучение синтаксиса pointcut, то будете чувствовать себя комфортно. Когда я провожу вводные занятия по АОП, то знакомлю с базовым синтаксисом pointcut, а затем прошу посетителей распутать синтаксис более сложных pointcut. И почти всегда получаю правильные ответы, что говорит о логичности и предсказуемости синтаксиса pointcut – двух достоинствах хорошего языка. Хотя я привел пример для AspectJ, тоже самое верно и для других АОП-реализаций, например, Spring AOP и JBoss AOP.

Компоновка pointcut

Рассмотренный здесь pointcut на самом деле является хорошим примером того, как не надо писать pointcut. Лучшим подходом к компоновке pointcut является написание более простых и семантически понятных pointcut и, затем, компоновка их для формирования более высокоуровневых pointcut. Вы можете увидеть эту технологию в действии, когда я буду переписывать pointcut. Прежде всего, я записываю pointcut, выбирающий все операции, происходящие в ходе выполнения любого метода HttpServlet, начинающегося с do, например, doService() и doGet():

1
2
pointcut duringServletRequest()
 : cflow(execution(* HttpServlet.do*(..));

Затем я записываю pointcut, выбирающий все RMI-вызовы и собирающий объект, для которого вызов был инициирован (называемый также целевым объектом):

1
2
3
4
pointcut remoteCall(Remote remoteService) :
    call(public * Remote+.*
    (..) throws RemoteException+) &&
    target(remoteService);

Наконец, я информирую о скомпонованном pointcut:

1
2
3
4
5
before(Remote remoteService) :
    remoteCall (remoteService)
    && duringServletRequest () {
    ...
}

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

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

Миф 14: АОП содействует сырому проектированию

Реальность: Для оптимального использования любой технологии нужен опыт

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

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

Должны ли вы ждать, пока не будут разработаны все шаблоны использования перед применением любой новой технологии? Или вам лучше использовать историю развития в качестве руководства и действовать прагматически? Последний подход позволяет вам учиться и развиваться вместе с технологией. Вы извлечете выгоду из нее с минимальным риском. Что вы теряете?

Миф 15: АОП можно либо внедрять полностью, либо вообще не внедрять

Реальность: АОП может внедряться постепенно

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

Второй тип трассировочного аспекта – принудительное применение политики с использованием аспектов. Здесь вы используете комбинацию деклараций времени компилирования (в AspectJ) и advice времени выполнения для обнаружения нарушения политик в вашей системе. Обнаружение политики может быть оставлено в системе во время фазы разработки и QA-фазы. Хорошим примером такого типа аспектов является обнаружение нарушения правила защищенности потока в Swing, когда только потоку диспетчера событий разрешено обращаться или изменять состояние UI-компонента. Нарушения этой политики могут привести к разным симптомам – от простых UI-аномалий до полного краха системы. Без аспектов такие нарушения обнаружить и исправить очень трудно. При помощи коротенького аспекта вы можете обнаружить нарушения, собрать достаточное количество информации для исправления и выпустить более качественную систему.

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

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

В заключение

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