使用C# 11的静态接口方法改进 面向约定 的设计
<p>C# 11带来了一个我期待已久的特性——静态接口方法。我们知道接口是针对契约的定义,但是一直以来它只能定义一组“实例”的契约,而不能定义类型(的静态成员)的契约,因为定义在接口中的方法只能是实例方法。由于缺乏针对“类型契约”的支持,我们在设计一些框架或者类库的时候,只能采用“按照约定”的设计,比如ASP.NET Core Minimal API针对参数的绑定就是一个典型的案例。以如下这个简单的应用为例,我们采用Minimal API的形式注册了一个针对根地址“/”的路由,作为处理器的委托的输出和输出都是我们自定义的Point对象。</p><div class="cnblogs_code"><pre>var app = WebApplication.Create();app.Map("<span style="color: rgba(139, 0, 0, 1)">/</span>", (Point point) => point);
app.Run();
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span> Point
{
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">double</span> X { <span style="color: rgba(0, 0, 255, 1)">get</span>; }
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">double</span> Y { <span style="color: rgba(0, 0, 255, 1)">get</span>; }
<span style="color: rgba(0, 0, 255, 1)">public</span> Point(<span style="color: rgba(0, 0, 255, 1)">double</span> x, <span style="color: rgba(0, 0, 255, 1)">double</span> y)
{
X = x;
Y = y;
}
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">override</span> <span style="color: rgba(0, 0, 255, 1)">string</span> ToString() => $"<span style="color: rgba(139, 0, 0, 1)">{X},{Y}</span>";
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">bool</span> TryParse(<span style="color: rgba(0, 0, 255, 1)">string</span> expression, <span style="color: rgba(0, 0, 255, 1)">out</span> Point? result)
{
result = <span style="color: rgba(0, 0, 255, 1)">default</span>;
var parts = expression.Split(',');
<span style="color: rgba(0, 0, 255, 1)">if</span> (parts.Length != 2) <span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span>;
<span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 255, 1)">double</span>.TryParse(parts, <span style="color: rgba(0, 0, 255, 1)">out</span> var x) || !<span style="color: rgba(0, 0, 255, 1)">double</span>.TryParse(parts, <span style="color: rgba(0, 0, 255, 1)">out</span> var y)) <span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span>;
result = <span style="color: rgba(0, 0, 255, 1)">new</span> Point(x, y);
<span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span>;
}
}</pre></div><p>Minimal API的约定,如果我们为Point类型定义了具有如上声明的TryParse方法,该方法就会用来帮助我们绑定处理方法的Point参数,如下的演示结果证实了这一点。</p><p><img width="240" height="98" title="image" style="border: 0 currentColor; border-image: none; display: inline; background-image: none" alt="image" src="https://img2023.cnblogs.com/blog/19327/202212/19327-20221207094711553-173933617.png" border="0"></p><p>其实针对参数绑定,我们还可以定义如下这样BindAsync参数来完成。</p><div class="cnblogs_code"><pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span> Point
{
...
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> ValueTask<Point?> BindAsync(HttpContext httpContext, ParameterInfo parameter)
{
Point? result = <span style="color: rgba(0, 0, 255, 1)">default</span>;
var name = parameter.Name;
var <span style="color: rgba(0, 0, 255, 1)">value</span> = httpContext.GetRouteData().Values.TryGetValue(name!, <span style="color: rgba(0, 0, 255, 1)">out</span> var v) ? v : httpContext.Request.Query.SingleOrDefault();
<span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">value</span> <span style="color: rgba(0, 0, 255, 1)">is</span> <span style="color: rgba(0, 0, 255, 1)">string</span> expression && TryParse(expression, <span style="color: rgba(0, 0, 255, 1)">out</span> var point))
{
result = point;
}
<span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span> ValueTask<Point?>(result);
}
}</pre></div><p>对于这种“基于约定”的编程,可以你觉得还不错,但是我想有90%的ASP.NET Core的开发者不知道有这个特性,就从这一点就充分证明了这样的设计还不够好。这样的实现也比较繁琐,我们不得不通过反射检验待绑定参数的类型是否满足约定,并以反射(或者表达式树)的方式调用对应的方法。其实上述两个方法本应该写入“契约”,无奈它们是静态方法,没法定义在接口中。现在我们有了静态接口方法,它们可以定义如下所示的IBindable<T>和IParsable<T>。</p><div class="cnblogs_code"><pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">interface</span> IBindable<T>
{
<span style="color: rgba(0, 0, 255, 1)">abstract</span> <span style="color: rgba(0, 0, 255, 1)">static</span> ValueTask<T?> BindAsync(HttpContext httpContext, ParameterInfo parameter);
}
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">interface</span> IParsable<T>
{
<span style="color: rgba(0, 0, 255, 1)">abstract</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">bool</span> TryParse(<span style="color: rgba(0, 0, 255, 1)">string</span> expression, <span style="color: rgba(0, 0, 255, 1)">out</span> T? result);
}
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span> Point : IBindable<Point>, IParsable<Point>
{
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">double</span> X { <span style="color: rgba(0, 0, 255, 1)">get</span>; }
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">double</span> Y { <span style="color: rgba(0, 0, 255, 1)">get</span>; }
<span style="color: rgba(0, 0, 255, 1)">public</span> Point(<span style="color: rgba(0, 0, 255, 1)">double</span> x, <span style="color: rgba(0, 0, 255, 1)">double</span> y)
{
X = x;
Y = y;
}
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">override</span> <span style="color: rgba(0, 0, 255, 1)">string</span> ToString() => $"<span style="color: rgba(139, 0, 0, 1)">{X},{Y}</span>";
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">bool</span> TryParse(<span style="color: rgba(0, 0, 255, 1)">string</span> expression, <span style="color: rgba(0, 0, 255, 1)">out</span> Point? result)
{
result = <span style="color: rgba(0, 0, 255, 1)">default</span>;
var parts = expression.Split(',');
<span style="color: rgba(0, 0, 255, 1)">if</span> (parts.Length != 2) <span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span>;
<span style="color: rgba(0, 0, 255, 1)">if</span> (!<span style="color: rgba(0, 0, 255, 1)">double</span>.TryParse(parts, <span style="color: rgba(0, 0, 255, 1)">out</span> var x) || !<span style="color: rgba(0, 0, 255, 1)">double</span>.TryParse(parts, <span style="color: rgba(0, 0, 255, 1)">out</span> var y)) <span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">false</span>;
result = <span style="color: rgba(0, 0, 255, 1)">new</span> Point(x, y);
<span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">true</span>;
}
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> ValueTask<Point?> BindAsync(HttpContext httpContext, ParameterInfo parameter)
{
Point? result = <span style="color: rgba(0, 0, 255, 1)">default</span>;
var name = parameter.Name;
var <span style="color: rgba(0, 0, 255, 1)">value</span> = httpContext.GetRouteData().Values.TryGetValue(name!, <span style="color: rgba(0, 0, 255, 1)">out</span> var v) ? v : httpContext.Request.Query.SingleOrDefault();
<span style="color: rgba(0, 0, 255, 1)">if</span> (<span style="color: rgba(0, 0, 255, 1)">value</span> <span style="color: rgba(0, 0, 255, 1)">is</span> <span style="color: rgba(0, 0, 255, 1)">string</span> expression && TryParse(expression, <span style="color: rgba(0, 0, 255, 1)">out</span> var point))
{
result = point;
}
<span style="color: rgba(0, 0, 255, 1)">return</span> <span style="color: rgba(0, 0, 255, 1)">new</span> ValueTask<Point?>(result);
}
}</pre></div><p>实际上IParsable<T>已经存在了,它真正的定义是这样的。如果有了这样的接口,确定带绑定参数类型是否满足之前的约定条件只需要确定其是否实现了对应的接口就可以了。</p><div class="cnblogs_code"><pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">interface</span> IParsable<TSelf> where TSelf : IParsable<TSelf>?
{
<span style="color: rgba(0, 0, 255, 1)">static</span> TSelf Parse(<span style="color: rgba(0, 0, 255, 1)">string</span> s, IFormatProvider? provider);
<span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">bool</span> TryParse( <span style="color: rgba(0, 0, 255, 1)">string</span>? s, IFormatProvider? provider, <span style="color: rgba(0, 0, 255, 1)">out</span> TSelf result);
}</pre></div><p>静态接口设计被应用到《用最少的代码打造一个Mini版的gRPC框架》中,我在表示gRPC服务的接口中定义了如下的静态方法Bind将本服务类型中定义的gRPC方法绑定成路由。</p><div class="cnblogs_code"><pre><span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">interface</span> IGrpcService<TService> where TService : <span style="color: rgba(0, 0, 255, 1)">class</span>
{
<span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">abstract</span> <span style="color: rgba(0, 0, 255, 1)">void</span> Bind(IServiceBinder<TService> binder);
}
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">class</span> GreeterService: IGrpcService<GreeterService>
{
<span style="color: rgba(0, 0, 255, 1)">public</span> Task<HelloReply> SayHelloUnaryAsync(HelloRequest request, ServerCallContext context);
<span style="color: rgba(0, 0, 255, 1)">public</span> async Task<HelloReply> SayHelloClientStreamingAsync(IAsyncStreamReader<HelloRequest> reader, ServerCallContext context);
<span style="color: rgba(0, 0, 255, 1)">public</span>async Task SayHelloServerStreamingAsync(Empty request, IServerStreamWriter<HelloReply> responseStream, ServerCallContext context);
<span style="color: rgba(0, 0, 255, 1)">public</span> async Task SayHelloDuplexStreamingAsync(IAsyncStreamReader<HelloRequest> reader, IServerStreamWriter<HelloReply> writer, ServerCallContext context);
<span style="color: rgba(0, 0, 255, 1)">public</span> <span style="color: rgba(0, 0, 255, 1)">static</span> <span style="color: rgba(0, 0, 255, 1)">void</span> Bind(IServiceBinder<GreeterService> binder)
{
binder
.AddUnaryMethod<HelloRequest, HelloReply>(it =>it.SayHelloUnaryAsync(<span style="color: rgba(0, 0, 255, 1)">default</span>!,<span style="color: rgba(0, 0, 255, 1)">default</span>!), HelloRequest.Parser)
.AddClientStreamingMethod<HelloRequest, HelloReply>(it => it.SayHelloClientStreamingAsync(<span style="color: rgba(0, 0, 255, 1)">default</span>!, <span style="color: rgba(0, 0, 255, 1)">default</span>!), HelloRequest.Parser)
.AddServerStreamingMethod<Empty, HelloReply>(nameof(SayHelloServerStreamingAsync), it => it.SayHelloServerStreamingAsync, Empty.Parser)
.AddDuplexStreamingMethod<HelloRequest, HelloReply>(nameof(SayHelloDuplexStreamingAsync), it => it.SayHelloDuplexStreamingAsync, HelloRequest.Parser);
}
}</pre></div><br><br>
来源:https://www.cnblogs.com/artech/p/static-interface-method.html
頁:
[1]