博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
升维打击,设计之道
阅读量:5368 次
发布时间:2019-06-15

本文共 8887 字,大约阅读时间需要 29 分钟。

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

目录

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

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

上周在公司做了一个关于.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
()?.Source; public static void SetInvocationSource(this HttpContext httpContext, string source) => httpContext.Features.Set
(new InvocationSourceFeature(source));}

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

public class FoobarSelector : IFoobar{    private static ConcurrentDictionary
_sources = new ConcurrentDictionary
(); public Task InvokeAsync(HttpContext httpContext) { return httpContext.RequestServices.GetServices
() .FirstOrDefault(it => it != this && GetInvocationSource(it) == httpContext.GetInvocationSource())?.InvokeAsync(httpContext);
string GetInvocationSource(object service)        {            var type = service.GetType();            return _sources.GetOrAdd(type, _ => type.GetCustomAttribute
()?.Source); } }}

我们按照如下的方式对针对IFoobar的三个实现类型进行了注册。由于FoobarSelector作为最后注册的服务,按照“后来居上”的原则,如果我们利用DI容器获取针对IFoobar接口的服务实例,返回的将会是一个FoobarSelector对象。我们在HomeController的构造函数中直接注入IFoobar对象。在Action方法Index中,我们将参数source绑定为应用类型,在调用IFoobar对象的InvokeAsync方法之前,我们调用了扩展方法SetInvocationSource将它应用到当前HttpContext上。

public class Program{    public static void Main(string[] args)    {        new WebHostBuilder()            .UseKestrel()            .ConfigureServices(svcs => svcs                .AddHttpContextAccessor()                .AddSingleton
() .AddSingleton
() .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)??Task.CompletedTask; }}

我们运行这个程序,并利用查询字符串(?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
where T:class{ private static ConcurrentDictionary
_filters = new ConcurrentDictionary
(); private readonly IHttpContextAccessor _httpContextAccessor; protected ServiceSelector(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; protected T GetService() { var httpContext = _httpContextAccessor.HttpContext; return httpContext.RequestServices.GetServices
() .FirstOrDefault(it => it != this && GetFilter(it)?.Match(httpContext) == true); ServiceFilterAttribute GetFilter(object service) { var type = service.GetType(); return _filters.GetOrAdd(type, _ => type.GetCustomAttribute
()); } }}

针对IFoobar的“服务选择器”则需要作相应的改写。如下面的代码片段所示,FoobarSelector 继承自基类ServiceSelector<IFoobar>,在实现的InvokeAsync方法中,在调用基类的GetService方法得到筛选出来的服务实例后,它只需要调用同名的InvokeAsync方法即可。

public class FoobarSelector : ServiceSelector
, 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()                  .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

你可能感兴趣的文章
面向对象的介绍与特性
查看>>
typing-python用于类型注解的库
查看>>
20189215 2018-2019-2 《密码与安全新技术专题》第13周作业
查看>>
第四周作业
查看>>
一、HTML基础
查看>>
蓝牙进阶之路 (002) - HC-05与HC-06的AT指令的区别(转)
查看>>
mysql的limit经典用法及优化
查看>>
C#后台程序与HTML页面中JS方法互调
查看>>
mysql 同一个表中 字段a 的值赋值到字段b
查看>>
linux系统可执行文件添加环境变量使其跨终端和目录执行
查看>>
antiSMASH数据库:微生物次生代谢物合成基因组簇查询和预测
查看>>
UNICODE与ANSI的区别
查看>>
nginx 配置实例
查看>>
Flutter - 创建底部导航栏
查看>>
ASP.NET MVC 教程-MVC简介
查看>>
SQL Server索引 - 聚集索引、非聚集索引、非聚集唯一索引 <第八篇>
查看>>
转载:详解SAP TPM解决方案在快速消费品行业中的应用
查看>>
Android OpenGL ES 开发(N): OpenGL ES 2.0 机型兼容问题整理
查看>>
项目中用到的技术及工具汇总(持续更新)
查看>>
【算法】各种排序算法测试代码
查看>>