Привет, Хабр!
Сегодня расскажу о паттерне «Реверсивный Proxy» на примере онлайн‑магазина кормов для котиков. Суть паттерна проста: вместо обычного прокси, который просто передаёт вызовы, мы можем динамически менять логику выполнения методов. Под катом — теория, примеры и объяснение кода.
Теоретические основы паттерна «Реверсивный Proxy»
В классическом варианте Proxy объект‑заместитель принимает вызовы и делегирует их реальному объекту. Такой подход удобен для кэширования, логирования или контроля доступа. Но он статичен — логику менять на лету нельзя. Проблема: при обновлении функционала приходится останавливать сервер или вносить изменения во все компоненты системы.
Реверсивный Proxy перехватывает вызовы и позволяет модифицировать их до передачи реальному объекту. Это может быть:
Добавление проверки параметров (например, замена товара в заказе).
Внедрение нового алгоритма обработки.
Откат к резервной реализации при ошибках.
Главное — всё происходит динамически, без остановки работы системы.
Для реализации динамики используются:
Expression Trees: позволяют формировать и компилировать выражения на ходу.
Reflection.Emit: даёт контроль над генерируемым IL‑кодом.
Пример: магазин кормов для котиков
Предположим, есть интерфейс сервиса, обрабатывающего заказы:
public interface ICatFoodService
{
void ProcessOrder(string orderId, string catFoodType);
}
Стандартная реализация:
public class CatFoodService : ICatFoodService
{
public void ProcessOrder(string orderId, string catFoodType)
{
Console.WriteLine($"[CatFoodService] Заказ {orderId} на корм '{catFoodType}' обработан.");
}
}
Теперь добавим реверсивный прокси, который, например, заменяет заказ «DeluxeCatFood» на «PremiumCatFood».
Реализация с Expression Trees и DispatchProxy
Используем DispatchProxy
для перехвата вызовов. Добавим динамическую проверку и логирование:
using System;
using System.Linq.Expressions;
using System.Reflection;
public static class ReverseProxy
{
public static T Create<T>(T instance) where T : class
{
if (!typeof(T).IsInterface)
throw new ArgumentException("T должен быть интерфейсом");
return DispatchProxyGenerator.CreateProxy<T>(instance, (method, args) =>
{
// Логирование вызова
Console.WriteLine($"[ReverseProxy] Метод: {method.Name}, параметры: {string.Join(", ", args)}");
// Если заказ на корм "DeluxeCatFood" - меняем на "PremiumCatFood"
if (method.Name == nameof(ICatFoodService.ProcessOrder) &&
args.Length >= 2 && args[1] is string catFoodType &&
catFoodType.Equals("DeluxeCatFood", StringComparison.OrdinalIgnoreCase))
{
Console.WriteLine("[ReverseProxy] Замена товара: DeluxeCatFood -> PremiumCatFood");
args[1] = "PremiumCatFood";
}
});
}
}
public class DispatchProxyGenerator : DispatchProxy
{
private object _target;
private Action<MethodInfo, object[]> _interceptor;
public static T CreateProxy<T>(T target, Action<MethodInfo, object[]> interceptor) where T : class
{
object proxy = Create<T, DispatchProxyGenerator>();
((DispatchProxyGenerator)proxy)._target = target;
((DispatchProxyGenerator)proxy)._interceptor = interceptor;
return proxy as T;
}
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
_interceptor?.Invoke(targetMethod, args);
return targetMethod.Invoke(_target, args);
}
}
Метод Create
: проверяет, что тип — интерфейс, и вызываем генератор прокси. Interceptor
: перед вызовом реального метода выполняется функция‑перехватчик, где можно изменить параметры (например, заменить тип корма) или добавить логирование. DispatchProxyGenerator
наследник DispatchProxy
, в котором переопределён метод Invoke, позволяющий вставить перехват вызова.
Реализация с Reflection.Emit
Для тех, кто хочет максимальной гибкости, предлагаю пример с Reflection.Emit
:
using System;
using System.Reflection;
using System.Reflection.Emit;
public static class ReverseProxyEmit
{
public static T Create<T>(T instance) where T : class
{
if (!typeof(T).IsInterface)
throw new ArgumentException("T должен быть интерфейсом");
var assemblyName = new AssemblyName("DynamicProxyAssembly");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
var typeBuilder = moduleBuilder.DefineType(typeof(T).Name + "Proxy",
TypeAttributes.Public, typeof(object), new[] { typeof(T) });
// Поле для хранения реального объекта
var targetField = typeBuilder.DefineField("_target", typeof(T), FieldAttributes.Private);
// Создание конструктора
var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(T) });
var ilCtor = ctor.GetILGenerator();
ilCtor.Emit(OpCodes.Ldarg_0);
ilCtor.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
ilCtor.Emit(OpCodes.Ldarg_0);
ilCtor.Emit(OpCodes.Ldarg_1);
ilCtor.Emit(OpCodes.Stfld, targetField);
ilCtor.Emit(OpCodes.Ret);
// Создание метода для каждого метода интерфейса
foreach (var method in typeof(T).GetMethods())
{
var parameters = method.GetParameters();
var paramTypes = Array.ConvertAll(parameters, p => p.ParameterType);
var methodBuilder = typeBuilder.DefineMethod(
method.Name,
MethodAttributes.Public | MethodAttributes.Virtual,
method.ReturnType,
paramTypes);
var il = methodBuilder.GetILGenerator();
// Логирование вызова
il.Emit(OpCodes.Ldstr, $"[ReverseProxyEmit] Вызов метода: {method.Name}");
il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }));
// Пример: проверка и замена параметра, если метод ProcessOrder и второй параметр равен "DeluxeCatFood"
if (method.Name == nameof(ICatFoodService.ProcessOrder) && paramTypes.Length >= 2)
{
il.Emit(OpCodes.Ldarg_2); // второй аргумент
il.Emit(OpCodes.Ldstr, "DeluxeCatFood");
il.Emit(OpCodes.Call, typeof(string).GetMethod("Equals", new[] { typeof(string) }));
Label labelContinue = il.DefineLabel();
il.Emit(OpCodes.Brfalse_S, labelContinue);
// Если равно, то заменяем на "PremiumCatFood"
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldstr, "PremiumCatFood");
il.Emit(OpCodes.Starg_S, (byte)2);
il.MarkLabel(labelContinue);
}
// Вызов реального метода
il.Emit(OpCodes.Ldarg_0);
il.Emit(OpCodes.Ldfld, targetField);
for (int i = 0; i < paramTypes.Length; i++)
{
il.Emit(OpCodes.Ldarg, i + 1);
}
il.Emit(OpCodes.Callvirt, method);
il.Emit(OpCodes.Ret);
typeBuilder.DefineMethodOverride(methodBuilder, method);
}
var proxyType = typeBuilder.CreateType();
return Activator.CreateInstance(proxyType, instance) as T;
}
}
Создаём сборку и модуль, определяем новый тип, реализующий интерфейс, далее инициализируем поле _target
для хранения реального объекта.
Для каждого метода генерируем IL‑код:
Выводим строку логирования.
Если метод равен
ProcessOrder
, проверяем второй аргумент и, если он равен «DeluxeCatFood», заменяем его на «PremiumCatFood».Загружаем аргументы, вызываем реальный метод и возвращаем результат.
Дополнительные примеры кейсы
Иногда может потребоваться переключаться между двумя реализациями. Приведем пример прокси, который переключается на резервную логику при возникновении исключения:
public class ResilientProxy<T> : DispatchProxy
{
private T _primary;
private T _fallback;
public void Configure(T primary, T fallback)
{
_primary = primary;
_fallback = fallback;
}
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
try
{
Console.WriteLine($"[ResilientProxy] Вызов {targetMethod.Name} с основной логикой.");
return targetMethod.Invoke(_primary, args);
}
catch (Exception ex)
{
Console.WriteLine($"[ResilientProxy] Ошибка: {ex.Message}. Переключение на резервную логику.");
return targetMethod.Invoke(_fallback, args);
}
}
}
Применение:
// Основная реализация
public class CatFoodServicePrimary : ICatFoodService
{
public void ProcessOrder(string orderId, string catFoodType)
{
// Симулируем ошибку для демонстрации
if (catFoodType == "FaultyCatFood")
throw new Exception("Ошибка в логике заказа");
Console.WriteLine($"[Primary] Заказ {orderId} на '{catFoodType}' обработан.");
}
}
// Резервная реализация
public class CatFoodServiceFallback : ICatFoodService
{
public void ProcessOrder(string orderId, string catFoodType)
{
Console.WriteLine($"[Fallback] Заказ {orderId} на '{catFoodType}' обработан резервной логикой.");
}
}
// Конфигурация прокси
var primaryService = new CatFoodServicePrimary();
var fallbackService = new CatFoodServiceFallback();
var proxy = DispatchProxy.Create<ICatFoodService, ResilientProxy<ICatFoodService>>();
((ResilientProxy<ICatFoodService>)proxy).Configure(primaryService, fallbackService);
// Тестируем: вызовется резервная логика, если основная падает
proxy.ProcessOrder("123", "FaultyCatFood");
Можно создать прокси, который динамически корректирует входные данные в зависимости от бизнес‑правил:
public static class OrderCorrectionProxy
{
public static ICatFoodService Create(ICatFoodService instance)
{
return DispatchProxy.Create<ICatFoodService, OrderCorrectionProxyImpl>()
.Setup(instance);
}
}
public class OrderCorrectionProxyImpl : DispatchProxy
{
private ICatFoodService _target;
public OrderCorrectionProxyImpl Setup(ICatFoodService target)
{
_target = target;
return this;
}
protected override object Invoke(MethodInfo targetMethod, object[] args)
{
// Если второй параметр (тип корма) содержит ошибку в написании, исправляем
if (targetMethod.Name == nameof(ICatFoodService.ProcessOrder) &&
args.Length >= 2 && args[1] is string type)
{
if (type.Contains("Delux"))
{
Console.WriteLine("[OrderCorrection] Исправление типа корма с 'Delux' на 'DeluxeCatFood'");
args[1] = "DeluxeCatFood";
}
}
return targetMethod.Invoke(_target, args);
}
}
Применение:
var realService = new CatFoodService();
var correctedService = OrderCorrectionProxy.Create(realService);
correctedService.ProcessOrder("456", "DeluxCatFood"); // Выведет исправление и затем вызов метода
А вы применяли этот паттерн? Пишите в комментариях. Удачи и до новых встреч!
P.S. Рекомендую обратить внимание на открытые уроки, которые скоро пройдут в Otus: