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

时间:2022-12-07 10:08:44

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参数,如下的演示结果证实了这一点。

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

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

public class Point
{
    ...
    public static ValueTask<Point?> 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<Point?>(result);
    }
}

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

public interface IBindable<T>
{
    abstract static ValueTask<T?> BindAsync(HttpContext httpContext, ParameterInfo parameter);
}

public interface IParsable<T>
{
    abstract static bool TryParse(string expression, out T? result);
}

public class Point : IBindable<Point>, IParsable<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;
    }

    public static ValueTask<Point?> 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<Point?>(result);
    }
}

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

public interface IParsable<TSelf> where TSelf : IParsable<TSelf>?
{
    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<TService> where TService : class
{
    static abstract void Bind(IServiceBinder<TService> binder);
}

[GrpcService(ServiceName = "Greeter")]
public class GreeterService: IGrpcService<GreeterService>
{
    public Task<HelloReply> SayHelloUnaryAsync(HelloRequest request, ServerCallContext context);

    public async Task<HelloReply> SayHelloClientStreamingAsync(IAsyncStreamReader<HelloRequest> reader, ServerCallContext context);

    public  async Task SayHelloServerStreamingAsync(Empty request, IServerStreamWriter<HelloReply> responseStream, ServerCallContext context);

    public async Task SayHelloDuplexStreamingAsync(IAsyncStreamReader<HelloRequest> reader, IServerStreamWriter<HelloReply> writer, ServerCallContext context);

    public static void Bind(IServiceBinder<GreeterService> binder)
    {
        binder
            .AddUnaryMethod<HelloRequest, HelloReply>(it =>it.SayHelloUnaryAsync(default!,default!), HelloRequest.Parser)
            .AddClientStreamingMethod<HelloRequest, HelloReply>(it => it.SayHelloClientStreamingAsync(default!, default!), HelloRequest.Parser)
            .AddServerStreamingMethod<Empty, HelloReply>(nameof(SayHelloServerStreamingAsync), it => it.SayHelloServerStreamingAsync, Empty.Parser)
            .AddDuplexStreamingMethod<HelloRequest, HelloReply>(nameof(SayHelloDuplexStreamingAsync), it => it.SayHelloDuplexStreamingAsync, HelloRequest.Parser);
    }
}