В разнообразных форумах я часто вижу один и тот же вопрос: "Могу ли я создать advice для XY?". В серии последующих статей я поясню предпосылки AOP (Aspect Oriented Programming) и его реализацию в spring.net (а так же в Castle.DynamicProxy и LinFu), чтобы вы лучше понимали данную тему и смогли сами ответить на данный вопрос.
Пример: повторение операции в случае неуспеха
Вместо привычного примера с логгированием я хочу показать вам другой полезный прием: повторение операции в случае неуспеха. Чтобы не усложнять, давайте предположим, что мы вызываем метод web-сервиса, складывающий два целых числа:
CalculatorWebService calc = new CalculatorWebService("http:/...");
int sum = calc.Add(2, 5);
Т.к. к web-сервисам обычно обращаются по сети, то эти вызовы небезопасны и могут быть неуспешными по самым разным причинам. В нашем примере мы хотим сделать три попытки обращения к методу web-сервиса с секундным интервалом прежде чем сдаться.
Существует пара вариантов реализации требуемого. Вероятно самым прямым подходом будет унаследовать наш класс от класса клиента web-сервиса:
public class RetryingCalculatorWebService : CalculatorWebService
{
public RetryingCalculatorWebService(string url):base(url) {}
public override int Add(int x, int y)
{
int retries = 0;
while (true)
{
try
{
return base.Add(x, y);
}
catch (Exception ex)
{
retries++;
if (retries >= 3)
{
throw; // retry threshold exceeded, giving up// wait a second
}
}
}
}
}
И, конечно, с этим подходом есть несколько проблем. Наиболее значимые среди них:
1. Код операции повтора довольно сильно увеличивает общий объем кода, что затрудняет осознание цели метода.
public class RetryingCalculatorWebService : CalculatorWebService
{
public RetryingCalculatorWebService(string url):base(url) {}
public override int Add(int x, int y)
{
int retries = 0;
while(true)
{
try
{
return base.Add(x, y);
}
catch(Exception ex)
{
retries++;
if (retries >= 3)
{
throw; // retry threshold exceeded, giving up
}
Thread.Sleep(1000); // wait a second
}
}
}
}
2. Подобная реализация повтора для всех методов web-сервиса приведет не только дублированию одного и того же кода, но и потребует изменения значительной части сервиса при изменении требования.
Собственное решение
Более структурированный путь решения данной проблемы описан в книге банды четырех - шаблон "Декоратор". Основная его идея в "обертывании" целевого метода в дополнительный код. Таким образом вам не придется изменять существующий код. Вместо этого вы создаете "матрешку" из включающих друг друга оберточных методов, каждый из которых реализован в собственном классе. В наши дни больший приоритет отдают композиции (вместо наследования, которое применяется в подходе, описанном в книге банды четырех). Поэтому давайте создадим интерфейс для облегчения объединения требуемых поведений в одну цепочку:
public interface ICalculator
{
int Add(int x, int y);
}
ICalculator calc = ...;
int sum = calc.Add(2, 5);
Теперь мы можем легко реализовать нашу бизнес-логику, а так же дополнительное поведение раздельно. В последствии мы можем объединить их так, как нам необходимо:
public class CalculatorWebService : ICalculator
{
public CalculatorWebService(string url) { ... }
public int Add(int x, int y)
{
// осуществляем вызов метода web-сервиса
return ...
}
}
public class CalculatorRetryDecorator : ICalculator
{
private ICalculator next;
public CalculatorRetryDecorator(ICalculator next) { this.next = next; }
public int Add(int x, int y)
{
int retries = 0;
while (true)
{
try
{
return next.Add(x, y);
}
catch (Exception ex)
{
retries++;
if (retries >= 3)
{
throw; // retry threshold exceeded, giving up
}
Thread.Sleep(1000); // wait a second
}
}
}
}
Заметьте, как CalculatorRetryDecorator
делегирует реальную работу следующему калькулятору в цепочке. Теперь, если внешние условия заставляют нас применить операцию повтора, мы с легкостью объединяем поведения:
ICalculator calc = new CalculatorRetryDecorator( new CalculatorWebService( "http://..." ) );
int sum = calc.Add(2, 5);
Также, в случае ухудшения производительности, мы можем реализовать декоратор кеширования результата вызова нашего метода и аналогичным образом добавить его в цепочку:
ICalculator calc = new CalculatorCacheDecorator( new CalculatorRetryDecorator( new CalculatorWebService( "http://..." ) ) );
int sum = calc.Add(2, 5);
Когда мы вызываем метод Add(), граф вызовов выглядит так:
И это только начало
Мы уже достигли много в разделении поведения на различные классы, но наше решение по-прежнему имеет несколько довольно сильных недостатков:
- Когда мы добавим новый метод в
ICalculator
, мы будем вынуждены расширить каждый из наших декораторов. - Мы хотим повторно использовать реализованное в декораторах поведение для других сервисов.
Свою следующую статью я посвящу этим вопросам и покажу, как реализовать более гибкое решение.
Тут вы можете скачать исходный код для данной статьи.
А как же PostSharp? Впрочем, C#5 сделает большинство попыток реализации АОР нерелевантными =)
Ну PostSharp — это пост процессинг, а в Spring всё работает через динамические прокси.