《三体》让我们了解了什么是 "降维打击", 在软件设计领域很多时候需要反其道而行. 对于某个问题, 如果不能有效的解决, 可以考虑是否可以上升一个维度, 从高维视角审视问题往往可以找到捷径. 软件设计是抽象的艺术,"升维打击" 实际上就是 "维度" 层面的抽象罢了.
目录
一, 源起: 一个接口, 多个实现
二, 根据当前上下文来过滤目标服务
三, 将这个方案做得更加通用一点
四, 我们是否走错了方向?
一, 源起: 一个接口, 多个实现
上周在公司做了一个关于. NET Core 依赖注入的培训, 有人提到一个问题: 如果同一个服务接口, 需要注册多个服务实现类型, 在消费该服务会根据当前上下文动态对选择对应的实现. 这个问题我会被经常问到, 我们不妨使用一个简单的例子来描述一下这个问题. 假设我们需要采用 ASP.NET Core MVC 开发一个供前端应用消费的微服务, 其中某个功能比较特殊, 它需要针对消费者应用类型而采用不同的处理逻辑. 我们将这个功能抽象成接口 IFoobar, 具体的功能实现在 InvokeAsync 方法中.
- public interface IFoobar
- {
- Task InvokeAsync(HttpContext httpContext);
- }
假设对于来源于 App 和小程序的请求, 这个功能具有不同的处理逻辑, 为此将它们实现在对应的实现类型 Foo 和 Bar 中.
- public class Foo : IFoobar
- {
- public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");
- }
- public class Bar : IFoobar
- {
- public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for MiniApp");
- }
二, 根据当前上下文来过滤目标服务
服务调用的请求会携带应用类型 (App 或者 MiniApp) 的信息, 现在我们需要解决的是: 如何根据提供的应用类型选择出对应的服务(Foo 或者 Bar). 为了让服务类型和应用类型之间实现映射, 我们选择在 Foo 和 Bar 类型上应用如下这个 InvocationSourceAttribute, 它的 Source 属性表示调用源的应用类型.
- [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
- public class InvocationSourceAttribute : Attribute
- {
- public string Source { get; }
- public InvocationSourceAttribute(string source) => Source = source;
- }
- [InvocationSource("App")]
- public class Foo : IFoobar
- {
- public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");
- }
- [InvocationSource("MiniApp")]
- public class Bar : IFoobar
- {
- public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for MiniApp");
- }
那么如何针对当前请求上下文设置和获取应用类型呢? 这可以在表示当前请求的 HttpContext 对象上附加一个对应的 Feature 来实现. 为此我们定义了如下这个 IInvocationSourceFeature 接口, InvocationSourceFeature 为默认的实现类型. IInvocationSourceFeature 的属性成员 Source 代表调用源的应用类型. 针对 HttpContext 的扩展方法 GetInvocationSource 和 SetInvocationSource 利用这个 Feature 获取和设置应用类型.
- public interface IInvocationSourceFeature
- {
- string Source { get; }
- }
- public class InvocationSourceFeature : IInvocationSourceFeature
- {
- public string Source { get; }
- public InvocationSourceFeature(string source) => Source = source;
- }
- public static class HttpContextExtensions
- {
- public static string GetInvocationSource(this HttpContext httpContext)
- => httpContext.Features.Get<IInvocationSourceFeature>()?.Source;
- public static void SetInvocationSource(this HttpContext httpContext, string source)
- => httpContext.Features.Set<IInvocationSourceFeature>(new InvocationSourceFeature(source));
- }
现在我们将 "服务选择" 实现在如下一个同样实现了 IFoobar 接口的 FoobarSelector 类型上. 如下面的代码片段所示, FoobarSelector 实现的 InvokeAsync 方法会先调用上面定义的 GetInvocationSource 扩展方法获取应用类型, 然后利用作为 DI 容器的 IServiceProvider 得到所有实现了 IFoobar 接口的服务实例. 接下来的任务就是通过分析应用在服务类型上的 InvocationSourceAttribute 特性来选择目标服务了.
- public class FoobarSelector : IFoobar
- {
- private static ConcurrentDictionary<Type, string> _sources = new ConcurrentDictionary<Type, string>();
- public Task InvokeAsync(HttpContext httpContext)
- {
- return httpContext.RequestServices.GetServices<IFoobar>()
- .FirstOrDefault(it => it != this && GetInvocationSource(it) == httpContext.GetInvocationSource())
- ?.InvokeAsync(httpContext);
- string GetInvocationSource(object service)
- {
- var type = service.GetType();
- return _sources.GetOrAdd(type, _ => type.GetCustomAttribute<InvocationSourceAttribute>()?.Source);
- }
- }
- }
我们按照如下的方式对针对 IFoobar 的三个实现类型进行了注册. 由于 FoobarSelector 作为最后注册的服务, 按照 "后来居上" 的原则, 如果我们利用 DI 容器获取针对 IFoobar 接口的服务实例, 返回的将会是一个 FoobarSelector 对象. 我们在 HomeController 的构造函数中直接注入 IFoobar 对象. 在 Action 方法 Index 中, 我们将参数 source 绑定为应用类型, 在调用 IFoobar 对象的 InvokeAsyncfan 方法之前, 我们调用了扩展方法 SetInvocationSource 将它应用到当前 HttpContext 上.
- public class Program
- {
- public static void Main(string[] args)
- {
- new webHostBuilder()
- .UseKestrel()
- .ConfigureServices(svcs => svcs
- .AddHttpContextAccessor()
- .AddSingleton<IFoobar, Foo>()
- .AddSingleton<IFoobar, Bar>()
- .AddSingleton<IFoobar, FoobarSelector>()
- .AddMvc())
- .Configure(App => App.UseMvc())
- .Build()
- .Run();
- }
- }
- public class HomeController: Controller
- {
- private readonly IFoobar _foobar;
- public HomeController(IFoobar foobar) => _foobar = foobar;
- [HttpGet("/")]
- public Task Index(string source)
- {
- HttpContext.SetInvocationSource(source);
- return _foobar.InvokeAsync(HttpContext);
- }
- }
我们运行这个程序, 并利用查询字符串 (?source=App) 的形式来指定应用类型, 可以得到我们希望的结果.
三, 将这个方案做得更加通用一点
我们可以将上述这个方案做得更加通用一点. 由于 "服务过滤" 的目的就是确定目标服务类型是否与当前请求上下文是否匹配, 所以我们可以定义如下这个 ServiceFilterAttribute 特性. 具体的过滤实现在 ServiceFilterAttribute 的 Match 方法上. 派生于这个抽象类的 InvocationSourceAttribute 特性帮助我们完成针对应用类型的服务过滤. 如果需要针对其他元素的过滤逻辑, 定义相应的派生类即可.
- public abstract class ServiceFilterAttribute: Attribute
- {
- public abstract bool Match(HttpContext httpContext);
- }
- [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
- public sealed class InvocationSourceAttribute : ServiceFilterAttribute
- {
- public string Source { get; }
- public InvocationSourceAttribute(string source) => Source = source;
- public override bool Match(HttpContext httpContext)=> httpContext.GetInvocationSource() == Source;
- }
我们依然采用注册一个额外的 "选择服务" 的方式来完成针对匹配服务实例的调用, 并为这样的服务定义了如下这个基类 ServiceSelector<T>. 这个基类提供的 GetService 方法会帮助我们根据当前 HttpContext 选择出匹配的服务实例.
- public abstract class ServiceSelector<T> where T:class
- {
- private static ConcurrentDictionary<Type, ServiceFilterAttribute> _filters = new ConcurrentDictionary<Type, ServiceFilterAttribute>();
- private readonly IHttpContextAccessor _httpContextAccessor;
- protected ServiceSelector(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;
- protected T GetService()
- {
- var httpContext = _httpContextAccessor.HttpContext;
- return httpContext.RequestServices.GetServices<T>()
- .FirstOrDefault(it => it != this && GetFilter(it)?.Match(httpContext) == true);
- ServiceFilterAttribute GetFilter(object service)
- {
- var type = service.GetType();
- return _filters.GetOrAdd(type, _ => type.GetCustomAttribute<ServiceFilterAttribute>());
- }
- }
- }
针对 IFoobar 的 "服务选择器" 则需要作相应的改写. 如下面的代码片段所示, FoobarSelector 继承自基类 ServiceSelector<IFoobar>, 在实现的 InvokeAsync 方法中, 在调用基类的 GetService 方法得到筛选出来的服务实例后, 它只需要调用同名的 InvokeAsync 方法即可.
- public class FoobarSelector : ServiceSelector<IFoobar>, IFoobar
- {
- public FoobarSelector(IHttpContextAccessor httpContextAccessor) : base(httpContextAccessor) { }
- public Task InvokeAsync(HttpContext httpContext) => GetService()?.InvokeAsync(httpContext);
- }
四, 我们是否走错了方向?
我们甚至可以将上面解决方案做到极致: 比如我们可以采用如下的形式在实现类型上应用的 InvocationSourceAttribute 加上服务注册的信息(服务类型和生命周期), 那么就可以批量完成针对这些类型的服务注册. 我们还可以采用 IL Emit 的方式动态生成对应的服务选择器类型(比如上面的 FoobarSelector), 并将它注册到依赖注入框架, 这样应用程序就不需要编写任何服务注册的代码了.
- [InvocationSource("App", ServiceLifetime.Singleton, typeof(IFoobar))]
- public class Foo : IFoobar
- {
- public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");
- }
- [InvocationSource("MiniApp", ServiceLifetime.Singleton, typeof(IFoobar))]
- public class Bar : IFoobar
- {
- public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for MiniApp");
- }
到目前为止, 我们的解决方案貌似还不错(除了需要创建所有服务实例之外), 扩展灵活, 编程优雅, 但是我觉得我们走错了方向. 由于我们自始自终关注的维度只有 IFoobar 代表的目标服务, 所以我们脑子里想的始终是: 如何利用 DI 容器提供目标服务实例. 但是我们面临的核心问题其实是: 如何根据当前上下文提供与之匹配的服务实例, 这是一个关于 "服务实例的提供" 维度的问题."维度提升" 之后, 对应的解决思路就很清晰了: 既然要解决的是针对 IFoobar 实例的提供问题, 我们只需要定义如下 IFoobarProvider, 并利用它的 GetService 方法提供我们希望的服务实例就可以了. FoobarProvider 表示对该接口的默认实现.
- public interface IFoobarProvider
- {
- IFoobar GetService();
- }
- public sealed class FoobarProvider : IFoobarProvider
- {
- private readonly IHttpContextAccessor _httpContextAccessor;
- public FoobarProvider(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor;
- public IFoobar GetService()
- {
- switch (_httpContextAccessor.HttpContext.GetInvocationSource())
- {
- case "App": return new Foo();
- case "MiniApp": return new Bar();
- default: return null;
- }
- }
- }
采用用来提供所需服务实例的 IFoobarProvider, 我们的程序同样会很简单.
- public class Program
- {
- public static void Main(string[] args)
- {
- new WebHostBuilder()
- .UseKestrel()
- .ConfigureServices(svcs => svcs
- .AddHttpContextAccessor()
- .AddSingleton<IFoobarProvider, FoobarProvider>()
- .AddMvc())
- .Configure(App => App.UseMvc())
- .Build()
- .Run();
- }
- }
- public class HomeController: Controller
- {
- private readonly IFoobarProvider _foobarProvider;
- public HomeController(IFoobarProvider foobarProvider)=> _foobarProvider = foobarProvider;
- [HttpGet("/")]
- public Task Index(string source)
- {
- HttpContext.SetInvocationSource(source);
- return _foobarProvider.GetService()?.InvokeAsync(HttpContext)??Task.CompletedTask;
- }
- }
《三体》让我们了解了什么是 "降维打击", 在软件设计领域则需要反其道而行. 对于某个问题, 如果不能有效的解决, 可以考虑是否可以上升一个维度, 从高维视角审视问题往往可以找到捷径. 软件设计是抽象的艺术,"升维打击" 实际上就是 "维度" 层面的抽象罢了.
来源: https://www.cnblogs.com/artech/p/upgrade-degree.html