《三体》让我们了解了什么是“降维打击”,在软件设计领域很多时候需要反其道而行。对于某个问题,如果不能有效的解决,可以考虑是否可以上升一个维度,从高维视角审视问题往往可以找到捷径。软件设计是抽象的艺术,“升维打击”实际上就是“维度”层面的抽象罢了。(本文实例从这里下载)

目录
一、源起:一个接口,多个实现
二、根据当前上下文来过滤目标服务
三、将这个方案做得更加通用一点
四、我们是否走错了方向?

一、源起:一个接口,多个实现

上周在公司做了一个关于.NET Core依赖注入的培训,有人提到一个问题:如果同一个服务接口,需要注册多个服务实现类型,在消费该服务会根据当前上下文动态对选择对应的实现。这个问题我会被经常问到,我们不妨使用一个简单的例子来描述一下这个问题。假设我们需要采用ASP.NET Core MVC开发一个供前端应用消费的微服务,其中某个功能比较特殊,它需要针对消费者应用类型而采用不同的处理逻辑。我们将这个功能抽象成接口IFoobar,具体的功能实现在InvokeAsync方法中。

publicinterface IFoobar{ Task InvokeAsync(HttpContext httpContext);}

假设对于来源于App和小程序的请求,这个功能具有不同的处理逻辑,为此将它们实现在对应的实现类型Foo和Bar中。

publicclass Foo : IFoobar{ public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");}publicclass 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)]publicclass InvocationSourceAttribute : Attribute{ publicstring Source { get; } publicInvocationSourceAttribute(string source) => Source =source;}[InvocationSource("App")]publicclass Foo : IFoobar{ public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");}[InvocationSource("MiniApp")]publicclass Bar : IFoobar{ public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for MiniApp");}

那么如何针对当前请求上下文设置和获取应用类型呢?这可以在表示当前请求的HttpContext对象上附加一个对应的Feature来实现。为此我们定义了如下这个IInvocationSourceFeature接口,InvocationSourceFeature为默认的实现类型。IInvocationSourceFeature的属性成员Source代表调用源的应用类型。针对HttpContext的扩展方法GetInvocationSource和SetInvocationSource利用这个Feature获取和设置应用类型。

publicinterfaceIInvocationSourceFeature{string Source { get; }}publicclass InvocationSourceFeature : IInvocationSourceFeature{ publicstring Source { get; } publicInvocationSourceFeature(string source) => Source = source; }publicstaticclassHttpContextExtensions{publicstaticstringGetInvocationSource(this HttpContext httpContext) => httpContext.Features.Get<IInvocationSourceFeature>()?.Source;publicstaticvoidSetInvocationSource(this HttpContext httpContext, stringsource)=> httpContext.Features.Set<IInvocationSourceFeature>(newInvocationSourceFeature(source));}

现在我们将“服务选择”实现在如下一个同样实现了IFoobar接口的FoobarSelector 类型上。如下面的代码片段所示,FoobarSelector 实现的InvokeAsync方法会先调用上面定义的GetInvocationSource扩展方法获取应用类型,然后利用作为DI容器的IServiceProvider得到所有实现了IFoobar接口的服务实例。接下来的任务就是通过分析应用在服务类型上的InvocationSourceAttribute特性来选择目标服务了。

publicclass FoobarSelector : IFoobar{ privatestaticConcurrentDictionary<Type,string> _sources = newConcurrentDictionary<Type,string>();public Task InvokeAsync(HttpContext httpContext) { returnhttpContext.RequestServices.GetServices<IFoobar>() .FirstOrDefault(it => it != this && GetInvocationSource(it) ==httpContext.GetInvocationSource())?.InvokeAsync(httpContext);
stringGetInvocationSource(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对象的InvokeAsync方法之前,我们调用了扩展方法SetInvocationSource将它应用到当前HttpContext上。

publicclassProgram{publicstaticvoidMain(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(); }}publicclass HomeController: Controller{ privatereadonly IFoobar _foobar; public HomeController(IFoobar foobar) => _foobar = foobar; [HttpGet("/")]public Task Index(string source) { HttpContext.SetInvocationSource(source); return _foobar.InvokeAsync(HttpContext)??Task.CompletedTask; }}

我们运行这个程序,并利用查询字符串(?source=App)的形式来指定应用类型,可以得到我们希望的结果。

三、将这个方案做得更加通用一点

我们可以将上述这个方案做得更加通用一点。由于“服务过滤”的目的就是确定目标服务类型是否与当前请求上下文是否匹配,所以我们可以定义如下这个ServiceFilterAttribute特性。具体的过滤实现在ServiceFilterAttribute的Match方法上。派生于这个抽象类的InvocationSourceAttribute 特性帮助我们完成针对应用类型的服务过滤。如果需要针对其他元素的过滤逻辑,定义相应的派生类即可。

publicabstractclass ServiceFilterAttribute: Attribute{ publicabstractbool Match(HttpContext httpContext);}[AttributeUsage(AttributeTargets.Class, AllowMultiple =false)]publicsealedclass InvocationSourceAttribute : ServiceFilterAttribute{ publicstring Source { get; } publicInvocationSourceAttribute(string source) => Source =source;publicoverridebool Match(HttpContext httpContext)=> httpContext.GetInvocationSource() ==Source;}

我们依然采用注册一个额外的“选择服务”的方式来完成针对匹配服务实例的调用,并为这样的服务定义了如下这个基类ServiceSelector<T>。这个基类提供的GetService方法会帮助我们根据当前HttpContext选择出匹配的服务实例。

publicabstractclassServiceSelector<T>whereT:class{privatestatic ConcurrentDictionary<Type, ServiceFilterAttribute> _filters = new ConcurrentDictionary<Type, ServiceFilterAttribute>();privatereadonly IHttpContextAccessor _httpContextAccessor; protected ServiceSelector(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor =httpContextAccessor;protected T GetService() { var httpContext =_httpContextAccessor.HttpContext;returnhttpContext.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方法即可。

publicclass 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))]publicclass Foo : IFoobar{ public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for App");}[InvocationSource("MiniApp", ServiceLifetime.Singleton, typeof(IFoobar))]publicclass Bar : IFoobar{ public Task InvokeAsync(HttpContext httpContext) => httpContext.Response.WriteAsync("Process for MiniApp");}

到目前为止,我们的解决方案貌似还不错(除了需要创建所有服务实例之外),扩展灵活,编程优雅,但是我觉得我们走错了方向。由于我们自始自终关注的维度只有IFoobar代表的目标服务,所以我们脑子里想的始终是:如何利用DI容器提供目标服务实例。但是我们面临的核心问题其实是:如何根据当前上下文提供与之匹配的服务实例,这是一个关于“服务实例的提供”维度的问题。“维度提升”之后,对应的解决思路就很清晰了:既然要解决的是针对IFoobar实例的提供问题,我们只需要定义如下IFoobarProvider,并利用它的GetService方法提供我们希望的服务实例就可以了。FoobarProvider表示对该接口的默认实现。

publicinterface IFoobarProvider{ IFoobar GetService();}publicsealedclass FoobarProvider : IFoobarProvider{ privatereadonly IHttpContextAccessor _httpContextAccessor; public FoobarProvider(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor =httpContextAccessor;public IFoobar GetService() { switch (_httpContextAccessor.HttpContext.GetInvocationSource()) { case"App":returnnewFoo();case"MiniApp":returnnewBar();default:returnnull; } }}

采用用来提供所需服务实例的IFoobarProvider,我们的程序同样会很简单。

publicclassProgram{publicstaticvoidMain(string[] args) { new WebHostBuilder() .UseKestrel() .ConfigureServices(svcs => svcs .AddHttpContextAccessor()  .AddSingleton<IFoobarProvider, FoobarProvider>() .AddMvc()) .Configure(app => app.UseMvc()) .Build() .Run(); }}publicclass HomeController: Controller{ privatereadonly IFoobarProvider _foobarProvider; public HomeController(IFoobarProvider foobarProvider)=> _foobarProvider = foobarProvider; [HttpGet("/")]public Task Index(string source) { HttpContext.SetInvocationSource(source); return_foobarProvider.GetService()?.InvokeAsync(HttpContext)??Task.CompletedTask; }}

《三体》让我们了解了什么是“降维打击”,在软件设计领域则需要反其道而行。对于某个问题,如果不能有效的解决,可以考虑是否可以上升一个维度,从高维视角审视问题往往可以找到捷径。软件设计是抽象的艺术,“升维打击”实际上就是“维度”层面的抽象罢了。