• 使用C# 11的静态接口方法改进 面向约定 的设计


    C# 11带来了一个我期待已久的特性——静态接口方法。我们知道接口是针对契约的定义,但是一直以来它只能定义一组“实例”的契约,而不能定义类型(的静态成员)的契约,因为定义在接口中的方法只能是实例方法。由于缺乏针对“类型契约”的支持,我们在设计一些框架或者类库的时候,只能采用“按照约定”的设计,比如ASP.NET Core Minimal API针对参数的绑定就是一个典型的案例。以如下这个简单的应用为例,我们采用Minimal API的形式注册了一个针对根地址“/”的路由,作为处理器的委托的输出和输出都是我们自定义的Point对象。

    复制代码
    var app = WebApplication.Create();
    app.Map("/", (Point point) => point);
    app.Run();
    
    public class Point
    {
        public double X { get; }
        public double Y { get; }
        public Point(double x, double y)
        {
            X = x;
            Y = y;
        }
    
        public override string ToString() => $"{X},{Y}";
    
        public static bool TryParse(string expression, out Point? result)
        {
            result = default;
            var parts = expression.Split(',');
            if (parts.Length != 2) return false;
            if (!double.TryParse(parts[0], out var x) || !double.TryParse(parts[1], out var y)) return false;
            result = new Point(x, y);
            return true;
        }
    }
    复制代码

    Minimal API的约定,如果我们为Point类型定义了具有如上声明的TryParse方法,该方法就会用来帮助我们绑定处理方法的Point参数,如下的演示结果证实了这一点。

    image

    其实针对参数绑定,我们还可以定义如下这样BindAsync参数来完成。

    复制代码
    public class Point
    {
        ...
        public static ValueTask BindAsync(HttpContext httpContext, ParameterInfo parameter)
        {
            Point? result = default;
            var name = parameter.Name;
            var value = httpContext.GetRouteData().Values.TryGetValue(name!, out var v) ? v : httpContext.Request.Query[name!].SingleOrDefault();
            if (value is string expression && TryParse(expression, out var point))
            {
                result = point;
            }
            return new ValueTask(result);
        }
    }
    复制代码

    对于这种“基于约定”的编程,可以你觉得还不错,但是我想有90%的ASP.NET Core的开发者不知道有这个特性,就从这一点就充分证明了这样的设计还不够好。这样的实现也比较繁琐,我们不得不通过反射检验待绑定参数的类型是否满足约定,并以反射(或者表达式树)的方式调用对应的方法。其实上述两个方法本应该写入“契约”,无奈它们是静态方法,没法定义在接口中。现在我们有了静态接口方法,它们可以定义如下所示的IBindable和IParsable

    复制代码
    public interface IBindable
    {
        abstract static ValueTask BindAsync(HttpContext httpContext, ParameterInfo parameter);
    }
    
    public interface IParsable
    {
        abstract static bool TryParse(string expression, out T? result);
    }
    
    public class Point : IBindable, IParsable
    {
        public double X { get; }
        public double Y { get; }
        public Point(double x, double y)
        {
            X = x;
            Y = y;
        }
    
        public override string ToString() => $"{X},{Y}";
    
        public static bool TryParse(string expression, out Point? result)
        {
            result = default;
            var parts = expression.Split(',');
            if (parts.Length != 2) return false;
            if (!double.TryParse(parts[0], out var x) || !double.TryParse(parts[1], out var y)) return false;
            result = new Point(x, y);
            return true;
        }
    
        public static ValueTask BindAsync(HttpContext httpContext, ParameterInfo parameter)
        {
            Point? result = default;
            var name = parameter.Name;
            var value = httpContext.GetRouteData().Values.TryGetValue(name!, out var v) ? v : httpContext.Request.Query[name!].SingleOrDefault();
            if (value is string expression && TryParse(expression, out var point))
            {
                result = point;
            }
            return new ValueTask(result);
        }
    }
    复制代码

    实际上IParsable已经存在了,它真正的定义是这样的。如果有了这样的接口,确定带绑定参数类型是否满足之前的约定条件只需要确定其是否实现了对应的接口就可以了。

    public interface IParsable where TSelf : IParsable?
    {
        static TSelf Parse(string s, IFormatProvider? provider);
        static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out TSelf result);
    }

    静态接口设计被应用到《用最少的代码打造一个Mini版的gRPC框架》中,我在表示gRPC服务的接口中定义了如下的静态方法Bind将本服务类型中定义的gRPC方法绑定成路由。

    复制代码
    public interface IGrpcService where TService : class
    {
        static abstract void Bind(IServiceBinder binder);
    }
    
    [GrpcService(ServiceName = "Greeter")]
    public class GreeterService: IGrpcService
    {
        public Task SayHelloUnaryAsync(HelloRequest request, ServerCallContext context);
    
        public async Task SayHelloClientStreamingAsync(IAsyncStreamReader reader, ServerCallContext context);
    
        public  async Task SayHelloServerStreamingAsync(Empty request, IServerStreamWriter responseStream, ServerCallContext context);
    
        public async Task SayHelloDuplexStreamingAsync(IAsyncStreamReader reader, IServerStreamWriter writer, ServerCallContext context);
    
        public static void Bind(IServiceBinder binder)
        {
            binder
                .AddUnaryMethod(it =>it.SayHelloUnaryAsync(default!,default!), HelloRequest.Parser)
                .AddClientStreamingMethod(it => it.SayHelloClientStreamingAsync(default!, default!), HelloRequest.Parser)
                .AddServerStreamingMethod(nameof(SayHelloServerStreamingAsync), it => it.SayHelloServerStreamingAsync, Empty.Parser)
                .AddDuplexStreamingMethod(nameof(SayHelloDuplexStreamingAsync), it => it.SayHelloDuplexStreamingAsync, HelloRequest.Parser);
        }
    }
    复制代码
  • 相关阅读:
    1024程序员节:庆祝编程的魅力
    [附源码]计算机毕业设计springboot网文论坛管理系统
    Taobao Proxy Purchasing System
    LeetCode_50_Pow(x,n)
    【报错】Failed to start A high performance web server and a reverse proxy server.
    FPGA - 7系列 FPGA内部结构之Memory Resources -03- 内置纠错功能
    暑假超越计划练习题(2)
    3D Gaussian Splatting for Real-Time Radiance Field Rendering(慢慢啃,还是挺复杂的)
    全景描绘云原生技术图谱,首个《云原生应用引擎技术发展白皮书》发布
    asp毕业设计——基于asp+access的网上远程教育网设计与实现(毕业论文+程序源码)——网上远程教育网
  • 原文地址:https://www.cnblogs.com/artech/p/static-interface-method.html