升维打击,设计之道

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

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

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

上周在公司做了一个关于.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对象的InvokeAsync方法之前,我们调用了扩展方法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)??Task.CompletedTask;
    }
}

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

image

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

我们可以将上述这个方案做得更加通用一点。由于“服务过滤”的目的就是确定目标服务类型是否与当前请求上下文是否匹配,所以我们可以定义如下这个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;
    }
}

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

image

posted @ 2019-06-17 06:51 Artech 阅读(...) 评论(...) 编辑 收藏