Привет, Хабр!
Сегодня расскажу о паттерне «Реверсивный 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:
