AOP@Work: Проектирование с использованием pointcut для устранения плотности шаблонов

Практичность и надежность в статье "JUnit: Cook's Tour"

Авторы Эрик Гамма и Кент Бек рассмотрели дизайн JUnit. В этой статье Вес Исберг покажет, как использование pointcut АОП вместо объектно-ориентированного подхода поможет избежать плотности шаблонов, усложняющей процесс изменения проектов.

Вес Исберг (Wes Isberg), Консультант

Вес Исберг (Wes Isberg) является консультантом и участником проекта Eclipse AspectJ. Он был в команде AspectJ в Xerox PARC, работал в Lutris Technologies над их сервером приложений с открытым исходным кодом Enhydra J2EE и изучал язык программирования Java, начиная с JDK 1.1.2, пока работал в отделе JavaSoft фирмы Sun. Адрес для контактов - wesisberg@yahoo.com.



14.06.2005

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

В этой статье я рассмотрю дизайн, предложенный в статье "JUnit: Cook's Tour" Эриком Гаммой и Кентом Беком (см. раздел "Ресурсы"). Для каждого представленного ими Java-шаблона я предлагаю альтернативу AspectJ и оцениваю, как они соответствуют некоторым из следующих канонических целей проектирования:

  • Функциональность: Насколько мощными или полезными являются представленные службы?
  • Практичность: Насколько легко клиенту получить службы?
  • Расширяемость: Насколько легко их расширить или адаптировать при изменениях программ?
  • Способность к (де)композиции: Насколько хорошо они работают с другими компонентами?
  • Защита: Насколько защищен API от ошибок времени исполнения или последовательных ошибок?
  • Понятность: Имеет ли код смысл?

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

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

Для всех примеров в этой статье я использую AspectJ, но они должны работать и с другими АОП-средствами и должны иметь смысл даже для новичков в AspectJ. (Несомненно, более полезным было бы прочитать сначала "Cook's Tour" и узнать о шаблонах проектирования, чем наличие опыта в AspectJ или JUnit.) Для загрузки исходных кодов, используемых в данной статье, нажмите пиктограмму Code (или перейдите в секцию Загрузки) в верхней или нижней части страницы.

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

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

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

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

Command или Hypothesis?

Вот отправная точка для Гаммы и Бека в статье "JUnit: A Cook's Tour":

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

Для преобразования тестов в объекты они используют шаблон Command, который инкапсулирует "запрос в виде объекта, позволяя вам таким образом […] ставить в очередь или вести журнал запросов." Очень конкретно.

Фокусируясь на практичности, Гамма и Бек удивляются, что разработчики пишут тесты различными способами, и настаивают на том, чтобы разработчики писали тесты только одним способом: инкапсулировали в виде объекта. Почему они делают это? Для облегчения работы с тестами. Здесь имеет место несоответствие: для получения выгоды от сервиса вы должны соответствовать форме.

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

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

Плотность шаблона по дизайну

Идентифицируя тестовый сценарий как ключевую абстракцию и инкапсулируя его при помощи Command, "Cook's Tour" продолжает идентификацию новых требований и добавляет новые функциональные возможности к объектам, представляющим ключевую абстракцию. Результат красиво обобщен на следующем плане:

Рисунок 1. План шаблонов JUnit
План шаблонов JUnit

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

Учитывая тестовый сценарий...

АОП предлагает другой способ указания абстракции: в виде pointcut, указывающей на точку соединения. Точка соединения представляет собой точку в процессе выполнения программы, где вы с пользой можете присоединить поведение. Типы точек соединения отличаются в зависимости от разновидности АОП, но все они должны быть стабильными при незначительных изменениях программы и легкими для указания многозначительности. Вы используете pointcut для указания точек соединения программы и advice для указания поведения для присоединения. Advice - это способ сказать, "когда X сделать Y".

Шаблон Command говорит, "Мне все равно, какой код выполняется; нужно просто вставить этот метод." Это требует вставки кода в метод command класса command - для JUnit, в метод runTest() класса Test, например TestCase:

public class MainTest extends TestCase {
  publicvoid runTest(...) {...}
}

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

pointcuttestCase() : ... ;

Например, вы могли бы определить тестовые сценарии Runnable.run(), методы main и, конечно же, тесты JUnit:

pointcut testCase() : execution(void Runnable+.run());
pointcut testCase() : execution(static void main(String[]));
pointcut testCase() : execution(public void Test+.run(TestResult));
pointcut testCase() : execution(public void TestCase+.test*());

Практичность pointcut

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

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

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

Повторное использование

Подобно методу pointcut может быть объявлен абстрактным; вы можете использовать его в advice, но позволить субаспектам объявить его конкретно. Обычно абстрактный pointcut указывает не конкретное время и место ("Четверг в 16-00 на Pennsylvania Avenue") а общее явление, представляющее интерес для многих ("выборы"). Тогда вы можете сказать истину о любом таком явлении ("В течение выборов новостные агентства..." или "После выборов победитель..."), а ваши пользователи могут указать, когда, кто и где для данных выборов. Указывая тестовый пример в виде абстрактной pointcut, готов поспорить, что не только многие функции для набора тестов могут быть выражены в форме "когда X, сделать Y", но также что я могу написать большинство из "сделать Y" без знания слишком многих деталей о "когда X".

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

Для оставшейся части "Cook's Tour" я реализую функции с pointcut testCase(), поскольку Гамма и Бек добавляют их в TestCase. На каждом этапе я пытаюсь избежать компромиссов, на которые они вынуждены были идти, путем оценки того, важен ли порядок точек соединения, путем уклонения от предположений о контексте в точке соединения и путем поддержки такого количества API клиентов, которое является разумным.


Template Method или advice around?

Используя Command для инкапсулирования тестового кода, Гамма и Бек распознают общий поток к тестам, которые используют некоторые общие механизмы данных: "установите тестовый механизм, запустите некоторый код в механизме, проверьте некоторые результаты и удалите механизм." Для инкапсуляции этого они используют шаблон Template Method.

Специально процитируем: "Определите скелет алгоритма в операции, откладывая некоторые шаги для субклассов. Template Method позволяет субклассам переопределить определенные шаги алгоритма без изменения структуры алгоритма."

В JUnit разработчики используют setUp() и cleanUp() с целью управления данными для TestCase. Инструменты JUnit отвечают за вызов методов перед и после каждого выполнения тестового сценария; TestCase делает это при помощи метода шаблона runBare():

public void runBare() throws Throwable {
  setUp();
  try {
    // запуск тестового метода 
    runTest();
  } finally {
    tearDown();
  }
}

В AspectJ, когда нужно выполнить код до или после точки соединения, вы можете использовать комбинацию advice before и after, либо один фрагмент advice around, как я это сделал ниже:

/** вокруг каждого тестового сценария выполнить установку и очистку */
Object around() : testCase() {
  setup(thisJoinPoint);
  try{
    // продолжить выполнение тестового сценария точки соединения
    return proceed();
  } finally {
     cleanup(thisJoinPoint);
   }
}       
protected void setup(JoinPoint jp) {}
protected void cleanup(JoinPoint jp) {}

Подобный advice предлагает три степени свободы:

  • Можно работать с любой точкой соединения, поддерживающей advice around.
  • Можно работать с любым типом теста, поскольку не делается никаких предположений об этом.
  • Помещая код setup/cleanup в методы, которые могут быть переопределены или реализованы при помощи делегирования, можно адаптироваться к различным способам управления механизмами, представленными различными типами тестовых объектов. Некоторые могут управлять своими собственными данными, как это делает TestCase; другие могут пользоваться выгодами от инверсии зависимости, где их конфигурация устанавливается извне.
    Однако эти методы используют JoinPoint, которая обеспечивает в виде Object любой контекст, доступный в точке соединения (возможно, включая этот объект, целевой объект и любые аргументы). Использование JoinPoint повлечет за собой опускание из Object в любой фактический тип, жертвуя универсальностью ради безопасности типов. (Ниже я предложу способ получения безопасности типов без потери универсальности.)

Этот advice предлагает такие же гарантии, что и Template Method, но без ограничений Java-реализации. В JUnit TestCase должен взять управление над методом command для реализации метода template, а затем делегировать управление в другой метод для выполнения реального теста, создавая специфичный TestCase-протокола для кода command. В результате, в то время как Command облегчает управление тестом, контракт command фактически варьируется (для разработчика) от Test до TestCase, делая функции API трудными для понимания.


Собирать Parameter или ThreadLocal?

"Cook's Tour" продолжает свои странствия: "Если TestCase зайдет в лес, будет ли кто-нибудь беспокоиться о результатах? " Естественно, Гамма и Бек отвечают: Вы должны записать неудачи и просуммировать успехи”. Для этого они используют шаблон Collecting Parameter:

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

JUnit инкапсулирует обработку результата в одном TestResult. Именно здесь подписчики могут найти результаты всех тестов и могут управлять вопросами сбора результатов. Для фактического проведения операции сбора Template Method TestResult.runProtected(..) "заключает в скобки" выполнение теста при помощи вспомогательных вызовов start и end и интерпретирует исключительные ситуации, сгенерированные при отрицательных результатах теста.

Способность к композиции

Теперь, когда имеется N>1 шаблонов, каковы взаимодействия между реализациями шаблона? Когда объекты хорошо работают друг с другом, говорят, что они способны к композиции. Аналогично, реализации шаблона могут открыто конфликтовать (например, когда оба требуют разных суперклассов), сосуществовать, но не взаимодействовать между собой, или сосуществовать, но взаимодействовать более или менее эффективным способом.

В JUnit взаимодействие между тестовой конфигурацией и процессом сбора результатов влечет за собой протокол call-последовательности, разделяемый между TestCase и TestResult, что показано ниже:

Test.runTest(TestResult) calls...
  TestResult.run(TestCase) calls...
    TestResult.runProtected(Test, Protectable) calls...
      Protectable.protect() calls...
        TestCase.runBare() calls...
          Test.runTest() ...
          (TestCase.runTest() invokes test method...)

Это демонстрация того, как плотность шаблона затрудняет изменение кода. Если вы хотите изменить метод template тестовой конфигурации или собирающий параметр, то должны изменить их оба, и в TestResult, и в TestCase (или ваших подклассах). Более того, поскольку методы тестовой конфигурации setUp() и cleanUp() выполняются в защищенном контексте обработки результатов, эта последовательность вызовов кодирует решение дизайна: любые исключительные ситуации, сгенерированные в коде тестовой конфигурации, интерпретируются как ошибки теста. Если вы хотите выдать отчет об ошибках тестовой конфигурации отдельно, то должны изменить не только оба компонента, но также их способ вызова друг друга. Может ли AspectJ улучшить это?

В AspectJ вы можете использовать advice для предоставления таких же гарантий, но избежать блокировки порядка:

/** Записать начало и конец теста, неудачу при ошибке */
void around(): testCase() {
  startTest(thisJoinPoint);
  try {
    proceed();
    endTest(thisJoinPoint);
  } catch (Error e) {
    error(thisJoinPoint, e);
  } catch (Exception e) {
    failure(thisJoinPoint, e);
  }
}

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

Кто первый?

В JUnit, методы шаблона для сбора результатов и управления тестовой конфигурацией должны быть расположены (навсегда ли?) в определенной последовательности вызовов. В AspectJ множество advice могут выполняться в точке соединения без каких-либо знаний о других advice этой точки соединения. В ситуациях, когда они не взаимодействуют, вы можете (и должны) игнорировать порядок, в котором они запускаются. Однако, если вы знаете, что один может влиять на другой, то можете управлять тем, как они запускаются используя приоритет. В этом случае, если вы дадите advice обработки результатов больший приоритет, при работе точки соединения этот advice запустится перед advice управления тестовой конфигурацией, вызовет для продолжения proceed(..) и передаст управление назад по завершении работы. Вот как это выглядит во время исполнения:

# начало работы точки соединения 
start result-handling around advice; proceed(..) invokes.. 
  start fixture-handling around advice; proceed(..) invokes.. 
    run underlying test case join point
  finish fixture-handling around advice
finish result-handling around advice 
# конец работы точки соединения

О точках соединения

Говоря о том, как работают точки соединения, рассматривайте их как программный стек (но не ждите каких-либо фреймов стека). Точка соединения (полностью) начинает работать перед началом работы первого advice и заканчивает работу после завершения последнего advice. "Зависимая" или "оригинальная" точка соединения - это рекомендуемый код; она может в свою очередь выполнить другие (вложенные) точки соединения. Advice выполняется в порядке старшинства перед или после зависимой точки соединения. Когда advice around вызывает proceed(..), он "продолжает точку соединения", вызывая какой-либо advice с меньшим старшинством и зависимую точку соединения. Некоторые advice не могут выполняться, если pointcut в данное время не совпадает (например, если ведение журналов запрещено). Поэтому, когда разработчики говорят "Точка соединения выполняла..." они имеют в виду точку соединения полностью, advice и все (даже если advice или зависимая точка соединения не запустилась из-за несовпадения pointcut, proceed(..) не был вызван или была сгенерирована исключительная ситуация).

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

aspect ReportingFixtureErrors {
  // ошибки тестовой конфигурации 
  //выдаются после ошибок обработки результатов 
  declare precedence: ResultHandling+, FixtureHandling+;
}

Два аспекта Handling не нуждаются в знании друг о друге, в отличие от двух классов JUnit TestResult и TestCase, которые должны договориться между собой о том, кто первый запускает команду. Для изменения дизайна в будущем мне нужно будет только изменить ReportingFixtureErrors.

Удобство использования Collecting Parameter

Большинство разработчиков JUnit-тестов не используют TestResult напрямую; это означало бы передачу его в виде параметра в каждый метод в цепочке вызовов, которую Гамма и Бек назвали "загрязнением сигнатуры". Вместо этого они предлагают утверждения (assertion) JUnit для оповещения о неудаче и для сворачивания теста.

TestCase расширяет Assert, который определяет несколько полезных методов static assert{something}(..) для проверки и регистрации неудач. Когда утверждение терпит неудачу, методы генерируют AssertionFailedError, которую TestResult перехватывает и интерпретирует в методе обработки результатов шаблона тестовой конфигурации. Таким образом, JUnit аккуратно обходит пользовательскую проблему API передачи собранных параметров и разрешает пользователям забыть о требованиях TestResult. JUnit объединяет процедуру вывода результатов со службами верификации и регистрации.

Пакетирование

Пакетирование затрудняет пользователям выбор нужных им служб. Использование Assert.assert{something}(..) связывает TestCase с TestResult более глубоко и скрывает гибкость сбора параметров. Пакетирование применяет быстро завершающую (fast-fail) семантику с тестами, даже если некоторые тесты желают продолжить работу после неудачной верификации. Для выдачи результатов напрямую, JUnit-тесты могли бы реализовать Test, но тогда они потеряли бы другие функциональные возможности TestCase (заменяемый селектор, управление тестовой конфигурацией, перезапуск тестового сценария и др.).

Это еще одна цена плотности шаблона: пользователи API часто вынуждены принимать или отвергать пакет полностью. Более того, хотя может быть и удобно пакетировать процессы, иногда это может уменьшить пригодность в повторному использованию. Например, многие инварианты классов или методов сначала пишутся в виде JUnit-утверждений (assertion); эти инвариантные проверки могут быть использованы повторно для производственной диагностики, если они не вызывают исключительные ситуации автоматически.

Как показывалось выше, AspectJ может поддерживать обработку результатов для утверждений в стиле JUnit; может ли он в то же время поддерживать API пользователей, которые хотят иметь гибкость прямого использования сборщика результатов и самостоятельно решать, когда свернуть тест? А если они определят свой собственный сборщик результатов и будут выдавать промежуточные результаты? Я думаю так. Существует четыре части решения: (1) поддержать конструкторы для сборщиков результатов; (2) сделать сборщик результатов доступным для компонентов без загрязнения сигнатур методов; (3) разрешить тестам свертываться после выдачи отчета непосредственно в сборщик результатов; (4) убедиться, что сгенерированные исключительные ситуации обрабатываются правильно. Это было бы трудно сделать в то время, когда писалась "Cook's Tour", но в настоящее время новые API и AspectJ облегчили этот процесс.

Сборщик ThreadLocal

Для того, чтобы сделать сборщик результатов доступным для всех компонентов и для реализации конструктора, я использую public static метод, для получения локального в потоке сборщика результатов. Вот скелет сборщика результатов TestContext:

public class TestContext {
  static final ThreadLocal<TestContext> TEST_CONTEXT 
    = new ThreadLocal<TestContext>();

  /** Клиенты вызывают этот метод для получения текстового контекста */
  public static TestContext getTestContext(Object test) { 
    ...     
  }

  ...
}

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

Свертывание теста

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

public class TestContext {
  ...
  public Error safeUnwind() { 
    return new ResultReported();
  }

  private static class ResultReported extends Error {}
}

Тогда тест генерирует любую исключительную ситуацию, которую определит конкретный TestContext:

  public void testClient() { 
    ...
    TestContext tc = TestContext.getTestContext(this);
    tc.addFailure(..);
    ..
    throw tc.safeUnwind(); // could be any Error
  }
}

Это связывает тест с TestContext, но safeUnwind() используется только тестами, которые выдают свой собственный отчет о результатах.

Гарантирование выдачи отчета по исключительным ситуациям

Ниже приведен advice, собирающий результаты для TestContext. Он достаточно общий и может быть применен для различных тестовых сценариев и различных подтипов TestContext:

/** Записать начало и конец каждого теста, или исключительную ситуацию */
void around() : testCase() {
  ITest test = wrap(getTest(thisJoinPoint));          
  TestContext testContext = TestContext.getTestContext(test); 
  testContext.startTest(test);
  try {
    proceed();
    testContext.endTest(test);
  } catch (ResultReported thrown)  {
    testContext.checkReported(test);
  } catch (Error thrown) { 
    testContext.testError(test, null, thrown);
  } catch (Throwable thrown) {
    testContext.testFailure(test, null, thrown);
  }
}

protected abstract Object getTest(JoinPoint jp);

Поскольку этот advice навязывает инварианты TestContext, я вложил аспект внутрь TestContext. Чтобы разрешить разработчикам тестов указать другие тестовые сценарии, и pointcut и метод являются абстрактными. Например, вот как я могу адаптировать это в TestCase:

aspect ManagingJUnitContext 
  extends TestContext.ManagingTestResults {
    
  public pointcut testCase() : within(testing.junit..*) 
    && execution(public !static void TestCase+.test*());

  protected Object getTest(JoinPoint jp) {
    assert jp.getTarget() instanceof TestCase;
    return jp.getTarget();
  }
}

Я ограничил решение в одном важном месте: advice around объявляет тип возврата void. Если бы я объявил тип возврата Object, я мог бы использовать advice с любой точкой соединения. Но, поскольку я перехватываю исключительные ситуации, я возвращаюсь нормально и должен был бы знать, какой Object возвратить. Я мог бы вернуть null и скрестить пальцы, но я предпочитаю сигнализировать о проблеме в какие-нибудь субаспекты, а не показывать ее во время исполнения в виде NullPointerException.

Хотя объявление void ограничивает возможности testCase() pointcut, оно уменьшает сложность и увеличивает защищенность. Advice в AspectJ имеет такую же проверку безопасности типов и исключительных ситуаций, что и методы в языке Java. Advice может объявить, что он генерирует контролируемую исключительную ситуацию (checked exception), и AspectJ будет сигнализировать об ошибке, если pointcut выбирает какую-либо точку соединения, которая не генерирует эту исключительную ситуацию. Аналогично, advice around может объявить значение возврата (выше использовалось "void"), которое требует, чтобы любая точка соединения имела такое же значение возврата. Наконец, если связать конкретный тип для устранения приведения типов (например, используя this(..), как я покажу позже), я должен быть способен найти его в точке соединения. Эти ограничения гарантируют, что advice AspectJ имеет такую же безопасность во время компоновки, что и метод Java (в отличие от подходов в АОП, использующих отражение или прокси).

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


Adapter, Pluggable Selector или Configuration?

"Cook's Tour" представляет Pluggable Selector как альтернативу "раздутию класса", вызванную созданием подкласса для каждого нового тестового сценария. Вот как авторы описывают это:

Идея состоит в использовании одного класса, который может быть параметризован для выполнения различной логики без необходимости создания подклассов [...] Pluggable Selector сохраняет [...] селектор метода в переменной экземпляра.

TestCase, таким образом, играет роль Adapter, преобразующего Test.run(TestResult) в TestCase.test...(), используя шаблон Pluggable Selector с именем поля таким же как селектор метода. Метод TestCase.runTest() вызывает метод, соответствующий имени поля. Эти соглашения позволяют разработчикам добавить тестовый сценарий путем простого добавления метода.

Для разработчиков JUnit-тестов это легко использовать, но трудно для разработчиков наборов тестов менять и расширять. Для удовлетворения runTest(), параметр конструктора TestCase(String name) должен быть именем экземпляра public метода, не имеющего параметров. TestSuite реализует этот протокол, поэтому если вы хотите изменить отраженный вызов в TestCase.runTest(), то должны изменить TestSuite.addTestSuite(Class), или наоборот. Для написания управляемых данными или спецификацией тестов, основанных на TestCase, вы должны создать отдельный набор для каждой конфигурации, закодировать конфигурацию в имя набора и сконфигурировать каждый тест, после того как он будет определен при помощи TestSuite.

Конфигурирование точек соединения

Может ли AspectJ идти на один шаг далее простого выбора обработки тестовой конфигурации? Существует два способа для доступа к настраивающимся тестам в точке соединения.

Во-первых, вы можете сконфигурировать точку соединения непосредственно путем изменения некоторого контекста, доступного в точке соединения, например аргументов метода или самого выполняющегося объекта. Простым примером для выполнения метода main(String[]) мог бы быть возврат точки соединения с различными массивами String[] для генерирования множества тестов. Более сложный пример соединяет варианты контекста в точке соединения. Ниже приведен advice для проверки, работает ли тест на всех принтерах (цветных и монохромных):

void around(Printer printer) : testCase() && context(printer) {
  // для всех известных принтеров...
  for (Printer p : Printer.findPrinters()) {
    // попробовать для монохромных и цветных...
    p.setMode(Printer.MONOCHROME);
    proceed(p);
    p.setMode(Printer.COLOR);
    proceed(p);
  }
  // также попробовать оригинальный принтер, в монохромном и цветном режимах
  printer.setMode(Printer.MONOCHROME);
  proceed(printer);
  printer.setMode(Printer.COLOR);
  proceed(printer);
}

Хотя этот пример специфичен для Printer, он не знает о том ,предназначен ли тест для печатания или инициализации, или является Printer назначением вызова метода или параметром метода. То есть, даже если advice требует какого-либо конкретного типа, он может быть более или менее равнодушен к тому, откуда идет ссылка; здесь advice делегирует субаспекту, определяющему pointcut, обе функции: какая точка соединения, и как получить контекст.

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

public abstract aspect Configuration {

  protected abstract pointcut configuring(IConfigurable s);

  public interface IConfigurable {
    Iterator getConfigurations();
    void configure(Object input);
  }

  void around(IConfigurable me) : configuring(me) {
    Iterator iter = me.getConfigurations();
    while (iter.hasNext()){
      me.configure(iter.next());
      proceed(me);
    }
  }
}

Этот advice может работать только тогда, когда контекст является IConfigurable, но после его запуска он может работать с точкой соединения много раз.

Как это взаимодействует с другими типами тестов в точке соединения, с другими advice в точке соединения и с каким-либо кодом, выполняемым точкой соединения? Что касается тестов, если тест не является IConfigurable, advice не работает. Здесь нет никаких конфликтов.

Что касается других advice, предполагая что вы определили configuring() как testCase() и включили ваши другие advice, поскольку это эффективно создает множество тестов, результат и advice тестовой конфигурации должны иметь более низкий приоритет, так чтобы они могли управлять и выдавать отчет для других конфигураций и результатов. Более того, конфигурация должна быть каким-то образом реализована в тестовой идентификации, используемой сборщиком результатов для выдачи результатов; это обязанность компонента, который знает, что тест является конфигурируемым и идентифицируемым (более подробно в следующем разделе).

Что касается кода, выполняющегося в точке соединения, в отличие от обычного advice around, он вызывает proceed(..) один раз для каждой конфигурации, то есть зависимая точка соединения может выполняться много раз. В данном случае какой результат должен быть возвращен из advice? Как и в advice, обрабатывающем результаты, я ограничил advice, возвратив void для сигнализирования о возможной проблеме разработчику теста, пишущему pointcut.

Делайте то, что вам нужно

Если бы я был разработчиком набора тестов, пытающимся адаптироваться к тесту, очевидно, что я пополнил бы "плотность шаблона" моего теста, если необходимо было реализовать IConfigurable в тестовом классе. Чтобы избежать этого в AspectJ, вы можете объявить членов и предков других типов, включая реализации по умолчанию для интерфейсов, поскольку любое определение сохраняет бинарную совместимость. Использование объявлений inter-type повышает безопасность типов в advice, что дает возможность избежать нисходящего с Object приведения типов.

Увеличивает ли это сложность целевого типа подобно другим членам? Члены public, объявленные в других типах, являются видимыми, поэтому они могут увеличить теоретическую сложность целевого типа. Однако вы можете также объявить членов других типов как private для аспекта, так чтобы только аспект мог использовать их. Это дает вам способ компоновки составных объектов без обычных коллизий и взаимодействий, возможных, если бы все члены были объявлены в самом классе.

Следующий код показывает пример, адаптирующий Run к IConfigurable, используя метод init(String):

public class Run {

  public void walk() { ... }

  public void init(String arg) { ... }
}


public aspect RunConfiguration extends Configuration {

  protected pointcut configuring(IConfigurable s) : 
    execution(void Run+.walk()) && target(s);
    
  declare parents : Run implements IConfigurable;

  /** Реализовать IConfigurable.getConfigurations() */
  public Iterator Run.getConfigurations() {
    Object[] configs = mockConfigurations();
    return Arrays.asList(configs).iterator();
  }

  /** Реализовать IConfigurable.configure(Object next) */
  public void Run.configure(Object config) {
    // hmm – нисходящее приведение типов от элемента mockConfigurations() element
    String[] inputs = (String[]) config; 
    for (String input: inputs) {
      init(input);
    }
  }

  static String[][] mockConfigurations() {
      return new String[][] { {<one>, <two>}, {<three>, <four>}};
  }
}

Тестовые идентификаторы

Тестовый идентификатор может совместно использоваться результатами отчета, вариантами выбора или конфигурацией и самим зависимым тестом. В некоторых системах необходимо только сказать пользователю, какой тест выполнялся; в других системах необходимо наличие уникального и согласованного ключа во время исполнения для проверки того, какой неудачный тест пройден (ошибка исправлена) и какой удачный тест нет (регрессия). JUnit предлагает только представление и обходит необходимость совместного использования при помощи String Object.toString() для получения String-представления. Набор тестов, созданный в AspectJ, может сделать такое же допущение, но он может также расширить любые тестовые объекты чем-то похожим на IConfigurable, описанным выше, вычислить и сохранить идентификатор для данного типа в соответствии с системными требованиями. "Тот же самый" тест может быть сконфигурирован с различными идентификаторами в зависимости от требований (например, для диагностики или тестирования регрессии), уменьшая конфликты, возможные из-за плотности шаблонов в языке Java. Пока конфигурация является локальной для аспекта и конфигурируемого компонента (и поэтому может быть private), идентификатор может быть видимым множеству объектов и, следовательно, должен быть представлен как public интерфейс.


Composite или Recursion?

В "Cook's Tour" признается, что набор тестов должен выполнять множество тестов - "набор наборов наборов тестов". Шаблон Composite успешно решает эти требования:

Специально процитируем: "Компонуйте объекты в древовидные структуры для представления иерархии целое-часть. Шаблон Composite позволяет клиентам рассматривать индивидуальные объекты и композиции объектов унифицировано."

Шаблон Composite представляет трех участников: Component, Composite и Leaf. Component объявляет компонент, который мы хотим использовать для взаимодействия с нашими тестами. Composite реализует этот интерфейс и содержит набор тестов. Leaf представляет тестовый сценарий в композиции, согласованной с интерфейсом Component.

Это обеспечивает JUnit-дизайн полным циклом, поскольку интерфейс Test.runTest(..) Command является интерфейсом Component, реализованным при помощи Leaf TestCase Composite TestSuite.

Удобство сопровождения

В "Cook's Tour" подчеркивается, "как прыгает сложность картины, когда мы применяем Composite." В данном шаблоне роли узла и листа наложены на существующие компоненты, и обоим необходимо знать их ответственность в реализации интерфейса компонента. Между ними определен протокол вызова, реализованный узлами, которые тоже собирают потомков. Это означает, что узлы знают о потомках и набор тестов знает об узлах.

В JUnit, TestSuite (уже) знает много о TestCase, и пользователи JUnit-тестов предполагают, что они генерируют набор путем загрузки класса набора. Как вы видели в конфигурации, поддержка конфигурируемого теста включает управление генерированием набора тестов. Composite повышает плотность шаблонов.

Шаблон Composite может быть реализован в AspectJ при помощи объявлений inter-type, как было показано выше в разделе по конфигурированию. В AspectJ все члены могут быть объявлены в одном аспекте, а не быть разбросанными по существующим классам. Это облегчает визуальную проверку того, что роли не загрязнены задачами из существующих классов, и понимание (при взгляде на реализацию) того, что это шаблон, а не просто другой член класса. Наконец, Composite является одним из шаблонов, которые могут быть реализованы в виде абстрактного аспекта с использованием тэговых интерфейсов для указания классов, играющих роли. Это означает, что вы можете написать пригодную для повторного использования реализацию шаблона. (Более подробная информация по AspectJ-реализации шаблонов проектирования приводится в статье Николаса Лесицки "Улучшение шаблонов проектирования с использованием AspectJ" в разделе "Ресурсы".)

Рекурсия

Может ли AspectJ удовлетворить оригинальные требования, не прибегая к шаблону Composite? AspectJ предлагает много способов запуска нескольких тестов. Пример конфигурации, приведенный выше, предложил один путь: ассоциировать список потомков с тестом и использовать advice для рекурсивного запуска компонентов в точке соединения, выбранной pointcut recursing(). Этот pointcut указывает составную операцию, которая должна быть рекурсивной:

// в абстрактном аспекте AComposite

/** тэговый интерфейс для объявления в субаспектах */
public interface IComposite {}

/** pointcut для объявления в субаспектах */
protected abstract pointcut recursing(IComposite c);

/** composites имеют потомков */
public ArrayList<IComposite> IComposite.children 
    = new ArrayList<IComposite>();

/** при рекурсии обойти все поддерево */
void around(IComposite c) : recursing(c)  {
  // рекурсия...
}

Вот как вы можете применить аспект к Run:

public aspect CompositeRun extends AComposite {
  declare parents : Run implements IComposite;
  
  public pointcut recursing(IComposite c) : 
    execution(void Run+.walk()) && target(c);
}

Инкапсулирование точек соединения как объектов

Рекурсия с точками соединения? Вот где вещи становятся интересными. В around advice AspectJ вы можете выполнить оставшуюся часть точки соединения, используя proceed(..). Чтобы сделать это рекурсивно, вы закрываете оставшуюся часть точки соединения при помощи инкапсулирования вызова proceed(..) в анонимном классе. Чтобы передать его в рекурсивный метод, анонимный класс должен расширять тип надстройки (wrapper), известный методу. Например, ниже я определил wrapper-интерфейс IClosure, наложил его на proceed(..) в around advice и передал результат в метод recurse(..):

// в аспекте AComposite...


/** используется здесь только при рекурсии */
public interface IClosure {
    public void runNext(IComposite next);
}

/** при рекурсии пройти по всему поддереву */
void around(IComposite c) : recursing(c)  {
  recurseTop(c, new IClosure() {
    // определить closure для дальнейшего вызова
    public void runNext(IComposite next) { 
      proceed(next); 
    }});
}

/** Для клиентов, чтобы найти вершину рекурсии. */
void recurseTop(IComposite targ, IClosure closure) {
    recurse(targ, closure);
}

/** Вызов targ или recurse через потомков targ. */
void recurse(IComposite targ, IClosure closure) {
  List children 
    = (null == targ?null:targ.children);
  if ((null == children) || children.isEmpty()) {
    // если нет потомков – обрабатывается следующий лист 
    closure.runNext(targ);
  } else {
    // если есть потомки – не обрабатывается следующий лист
    for (Object next: children) {
      recurse((IComposite) next, closure);
    }        
  }
}

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

Не удивляйтесь, если закрытие proceed(..) станет привычкой. Если вы инициируете объект IClosure после завершения точки соединения, результаты могут меняться.

Практичность

Аспект RunComposite применяет это составное решение к классу Run, просто устанавливая тэг в классе при помощи интерфейса IComposite и определяя pointcut recursing(). Однако сборка компонентов в древовидную структуру влечет за собой добавление потомков; это значит, что некоторые компоненты должны знать, что Run является IComposite с потомками. Вот компоненты и их взаимосвязи:

Assembler, знает о...
  Компонент Run, и
  Конкретный аспект CompositeRun, который знает о...
    Компонент Run, и
    Абстрактный аспект AComposite

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

Способность к компоновке

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

Composition      # рекурсия
  Configuration  # определение теста, идентичность
    Context      # вывод результатов
      Fixture    # обработка данных теста
        test     # зависимый тест

Pointcut как абстракция проектирования

Этим я завершаю мое обозрение "Junit: Cook's Tour". Все рассмотренные аспектно-ориентированные решения доступны в полном объеме в пакете с исходным кодом, присоединенным к данной статье. Решения имеют следующие характеристики:

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

Для данного Java-шаблона AspectJ предоставляет много путей сделать ту же вещь, иногда с более простой идиомой. Здесь я выбрал подход с использованием pointcut (и меньше всего предполагал возможность повторного использования, главным образом для демонстрации того, как спроектирован AspectJ для инкапсуляции advice и точек соединения для минимизации взаимодействий, облегчая масштабирование поведения в точке соединения. В некоторых случаях может быть более понятней использовать конкретный (не используемый повторно) аспект, комбинировать функции в одном аспекте, или использовать объявления inter-type для реализации соответствующих шаблонов Java. Однако эти решения демонстрируют технику минимизации взаимодействия в точке соединения для облегчения использования pointcut как первоклассных абстракций проектирования.

Pointcut являются всего лишь первым шагом в подходе к проектированию, в котором минимизируется запутанные допущения. Попробуйте по-настоящему силу точек соединения, при использовании которых объекты могут не понадобиться. Когда объекты необходимы, попробуйте использовать объявления inter-type в аспектах для композиции объектов, так чтобы различные (шаблон) роли оставались индивидуальными, даже если определены в одном классе. Так же как и в объектно-ориентированном программировании попробуйте предохранить различные компоненты от знания друг о друге. Если они должны знать друг о друге, конкретный компонент должен знать об абстрактном, а сборщик должен знать о частях. Когда они знают друг о друге, взаимоотношения должны быть узкими, явными, стабильными и возможными.


Полноскоростное АОП

AspectJ1.0 был выпущен более трех лет назад (!). Большинство разработчиков видели или пробовали начальные приложения AspectJ, которые модулизируют пересекающиеся процессы, например, трассировку. Но некоторые разработчики пошли дальше, пробуя то, что я классифицирую как полноскоростное АОП:

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

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

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

Такая мысль аналогична рассуждениям в терминах "is-a" и "has-a" на ранних этапах объектно-ориентированного программирования. В поисках одной задачи (даже если она пересекающаяся) вы упустили взаимосвязи и протоколы, которые, будучи нормализованными в виде шаблонов, формируют суть технологии кодирования.

Еще одно предубеждение:

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

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

И, наконец, самое труднопреодолимое предубеждение:

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

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


Заключение

Новое прочтение "JUnit: A Cook's Tour" было хорошим способом лучше понять, как AspectJ может минимизировать и управлять взаимодействием в точках соединения, что является ключом эффективного использования pointcut в вашем проекте. Плотность шаблонов, которая может сделать развитые объектно-ориентированные системы разработки трудными для изменения, является естественным результатом способа проектирования систем объектно-ориентированными разработчиками. Представленное здесь решение путем использования pointcut вместо объекта, ухода, где только возможно, от взаимодействий или их минимизации (в противном случае) устраняет жесткость JUnit и показывает, как сделать то же самое с вашими собственными проектами. Устраняя издержки проектирования, разработчики доросли до того, чтобы принять АОП. Эти решения показывают, что АОП может быть полезен даже в случае, если код уже выглядит хорошо модулизированным. Я надеюсь, что воодушевил вас на то, чтобы попробовать АОП в большем числе приложений, на полной скорости.


Загрузка

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

Ресурсы

Комментарии

developerWorks: Войти

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


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


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

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

 


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

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

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



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

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

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

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

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

 


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


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=96762
ArticleTitle=AOP@Work: Проектирование с использованием pointcut для устранения плотности шаблонов
publish-date=06142005