Это вторая часть из моей серии статей о внутреннем устройства Spring.NET AOP. В первой статье я описал первый шаг в разделении поведения и то, как мы можем использовать шаблон "Декоратор", чтобы сделать наш код более легким в сопровождении. Однако мы до сих пор имеем несколько важных проблем:
- Изменение интерфейса нашего сервиса требует изменение всех декораторов
- Невозможность использования данных декораторов для других сервисов
Более гибкий подход: перехватчик
Как было сказано ранее, мы можем улучшить нашу реализацию. Представьте, что мы имеем другие сервисы в нашем приложении. И мы не хотим реализовывать кеширование или операцию повтора каждый раз для каждого метода. Дублирование кода - это дорога в ад. Таким образом нам необходимо улучшить наше решение. Во-первых, давайте сделаем рефакторинг нашего кода операции повтора в отдельный класс, который может быть использован для оборачивания вызова любого метода. Мы введем новый специальный интерфейс для добавления нужного поведения и назовем его перехватчиком (от англ. interceptor):
public interface IMethodInterceptor
{
object InvokeMethod(Func<object> invokeNext);
}
Реализация перехватчика принимает делегат в качестве параметра. Данный делегат может быть как обычным методом, так и другим перехватчиком. Метод, который мы оборачиваем дополнительным поведением, обычно называют целевым методом (от англ. target method). В том случае, если делегат - это другой перехватчик, то это называется цепочкой перехватчиков (от англ. interceptor chain). В таком случае каждый перехватчик добавляет свое собственное поведение перед вызовом целевого метода. Наш Retry-interceptor будет выглядить следующим образом:
public class RetryInterceptor : IMethodInterceptor
{
public object InvokeMethod(Func<object> invokeNext)
{
int retries = 0;
while (true)
{
try
{
return invokeNext();
}
catch (Exception ex)
{
retries++;
if (retries >= 3)
{
throw; // retry threshold exceeded, giving up
}
Thread.Sleep(1000); // wait a second
}
}
}
}
Отметьте, что нашему RetryInterceptor
абсолютно неважен вид целевого метода или его аргументы. Он всего лишь принимает делегат, который может быть следующим перехватчиком в цепочке или же вызовом реального метода.
Теперь мы можем реализовать нашу цепочку перехватчиков еще более гибким
способом. Мы создадим класс, который назовем CalculatorProxy
, и передадим ему реальный метод и список необходимых перехватчиков. Вызов метода в данном случае несколько неочевиден, но с этим вполне можно справиться:
public class CalculatorProxy : ICalculator
{
private ICalculator target;
private IMethodInterceptor[] interceptors;
public CalculatorProxy(ICalculator target, IMethodInterceptor[] interceptors)
{
this.target = target;
this.interceptors = interceptors;
}
public int Add(int x, int y)
{
return (int) InvokeInterceptorAtIndex(0, ()=>target.Add(x, y));
}
private object InvokeInterceptorAtIndex(int interceptorIndex, Func<object> targetMethod)
{
if (interceptorIndex >= interceptors.Length)
{
return targetMethod();
}
return interceptors[interceptorIndex].InvokeMethod(
() => InvokeInterceptorAtIndex(interceptorIndex + 1, targetMethod)
);
}
}
Прокси принимает все внешние вызовы ICalculator.Add()
и отдает их на выполнение первому перехватчику в цепочке. Прокси инициализируется следующим образом:
ICalculator calc = new CalculatorProxy(
new CalculatorWebService(),
new IMethodInterceptor[] { new RetryInterceptor() });
int sum = calc.Add(2, 5);
Каждый перехватчик будет вызывать следующего перехватчика в цепочке или целевой метод, если цепочка закончилась:
Генерация прокси - Spring's ProxyFactory
Конечно на данный момент мы реализовали наш механизм перехватывания только для одного метода. Теперь представим, что мы хотим расширить функциональность нашего калькулятора и добавить к нему метод Divide()
. Взгляните на то, как будет выглядеть реализация этого метода в proxy:
public double Divide(double dividend, double divisor)
{
return (int)InvokeNext(0, () => target.Divide(dividend, divisor));
}
Сравнив это с реализацией метода Add()
, мы можем заметить очевидное сходство. Оба метода передают контроль первому перехватчику в цепочке, а так же передают ему вызов реального метода. Независимо от того, сколько методов мы добавим, код всегда будет выглядеть одинаково. Давайте выделим общий код вызова цепочки перехватчиков в в базовый класс AbstractProxy
:
public abstract class AbstractProxy
{
private IMethodInterceptor[] interceptors;
public AbstractProxy(IMethodInterceptor[] interceptors)
{
this.interceptors = interceptors;
}
protected object InvokeInterceptorAtIndex(int interceptorIndex, Func<object> targetMethod)
{
if (interceptorIndex >= interceptors.Length)
{
return targetMethod();
}
return interceptors[interceptorIndex].InvokeMethod(
() => InvokeInterceptorAtIndex(interceptorIndex + 1, targetMethod)
);
}
}
Тогда наш CalculatorProxy
примет вид:
public class CalculatorProxy : AbstractProxy, ICalculator
{
private readonly ICalculator target;
public CalculatorProxy(ICalculator target, IMethodInterceptor[] interceptors)
: base(interceptors)
{
this.target = target;
}
public int Add(int x, int y)
{
return (int)InvokeInterceptorAtIndex(0, () => target.Add(x, y));
}
public double Divide(double dividend, double divisor)
{
return (int)InvokeInterceptorAtIndex(0, () => target.Divide(dividend, divisor));
}
}
Даже, если мы реализуем другие интерфейсы, эти строки не изменятся. К счастью, Castle.DynamicProxy, LinFu и конечно Spring.NET уже реализуют возможность генерации этого кода в run-time. Далее я покажу, как вы можете использовать спринговский класс ProxyFactory
для этой цели.