zoukankan      html  css  js  c++  java
  • 一个接口,多个实现

    一个接口,多个实现

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

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

    上周在公司做了一个关于.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

    作者:蒋金楠 
    微信公众账号:大内老A
  • 相关阅读:
    笔记:npm常见错误
    微信小程序ES6方法Promise封装接口
    vuex的Store简单使用过程
    vue-cli输入命令vue ui没效果
    uni-app项目导入第三方组件库muse-ui
    记录:拷贝gitblit里的项目使用git命令clone、pull、push等,出现一直在加载,卡住没反应的问题
    html2canvas脚本实现将html内容转换canvas内容
    记录解决phpStudy报出403Forbidden问题的方法
    gulp报错task function must be specified
    随想
  • 原文地址:https://www.cnblogs.com/Leo_wl/p/11070543.html
Copyright © 2011-2022 走看看