【AICon】 如何构建高效的 RAG 系统?RAG 技术在实际应用中遇到的挑战及应对策略?>>> 了解详情
写点什么

面向 DSL 设计 API 是否会形成语义的滥用?

  • 2009-11-30
  • 本文字数:3202 字

    阅读完需:约 11 分钟

本月初,博客园的老赵在其博客上发表了一篇文章,谈到了一种在他眼中兼顾性能和可读性的DSL,以此在ASP.NET MVC 应用程序中构造URL。但也有人认为,这种构造方式违反了语言元素原本的语义,让人难以从签名中快速看出它的使用方法,因此是一种不可取的方式。

在使用ASP.NET MVC 框架构建Web 应用程序时,一个很常见的需求便是构造面向某个特定Action 方法及参数的URL。老赵原本在他的 MvcPatch 项目(一个基于 ASP.NET MVC 进行改造以提高生产力的框架)中提供了一种基于表达式树的构造方式,但经过测试之后发现这种方式有很大的性能问题。因此老赵后来又提出一种流畅接口(Fluent Interface):

复制代码
<span>public static class </span><span>UrlHelperExtensions
</span>{
<span>public static </span><span>ActionOf</span><TController> Of<TController>(<span>this </span><span>UrlHelper </span>helper) <span>where </span>TController : <span>new</span>()
{
<span>return new </span><span>ActionOf</span><TController>(helper);
}
}
<span>public class </span><span>ActionOf</span><TController>
{
<span>public string </span>Action<T1, T2>(<span>Func</span><TController, <span>Func</span><T1, T2, <span>ActionResult</span>>> action, T1 arg1, T2 arg2) {...}
}

他认为,这是一种用于构造 URL 的 DSL,可以这样使用:

复制代码
Url.Of<<span>HomeController</span>>().Action(c => c.Post, blog, post)

以上这行代码表示的含义是“URL of HomeController’s action ‘Post’ with parameter ‘blog’ & post.”,它将会生成一个 URL。针对这个 URL 的请求便会被转化为 HomeController.Post 方法的调用,并且提供 DSL 中所指定的 blog 和 post 的值作为参数。老赵认为,这个做法从性能上比原本基于表达式树的构造方式要高出许多,并且充分利用 C#编译器的类型推导能力,拥有较好的可读性。而更关键的是,这种做法可以在编译期对 URL 构造进行静态检查。

不过在随后的评论中,网友装配脑袋提出,这个 API 不适合 VB,不能算是真正优美的解决方案,而且更重要的是,语言元素应该用做原本职责所在的事情(即“委托”应该用于方法调用):

这种 Url.Of().Action(c => c.Post, blog, post) 的语法,真正给别人使用的时候,没有人会从方法签名或类的定义中快速看出它的使用方法。

另外你也应该能从我的代码里看出,在 VB 里,从方法名获得委托需要 AddressOf 运算符,你的代码就会变成 Url.Of(Of HomeController).Action(Function©AddressOf c.Post, blog, post)。这其实代表了委托和方法语义上的差异。用这种语法,将造成滥用语义的倾向。

我觉得这条路彻底走歪了。从方法名获得委托,仅仅在用户明确知道是委托的上下文才有意义。你们不能滥用这个语法。

老赵则解释道:

其实我觉得 API 还是针对特定语言来说可能比较合适,否则的话,就算以前的 Lambda 表达式写法:Url.Action(c => c.Post(blog, post)) 放在 VB 里写起来还是麻烦。但是这种方式,其实微软本身也很推崇,在 ASP.NET MVC 2 里也有类似的 API(不过不是做同样的事情)。

如果思考“非 C#语言”语法的话,很多东西就不好办了,比如 Moq。还比如 F#的 API 也不会考虑 C#的使用方式,语言特性不同么……我现在就好比是在为 80% 占有率的语言设计 API,而只能忽视 20% 了,这是准备中的,除非有更好的设计,否则只能先满足这 80% 了……

另一网友 Ivony 则认为这似乎也不能算是走歪了:

委托不仅仅是方法的调用包装(透过委托可以调用方法),同样也可以是方法的信息包装(透过委托找到方法)。我们一般用前者,但不见得后者就歪了呀。

老赵又作了补充:

其实,是否可以从方法的签名中得出使用方式,这个我也不太在意。尤其是在视图中,我一般都把这些东西当作是 DSL 来看待,所以我会设计出“url of HomeController’s action Post with…”这种语义的 API。而使用的时候只要记住“用法”就行了,不关心“签名”究竟如何,就像使用不动点组合子来生成递归形式的 HTML ,从签名也实在看不出来。

的确,如果可以从签名看出来那自然最为理想,但是在 DSL 面前,这点还是让步吧,比如在 FsTest 里:

复制代码
"foo" |> should equal "foo"
true |> should be True

这种东西,看 should 函数,be 函数,not 函数(的签名)……都是搞不懂该怎么用的,但就是为了让别人看明白写出来的代码,让别人知道该怎么写。

装配脑袋也进一步阐述了他眼中这种 API 的坏处:

用户不会想到这里是要写 c => c.Detail,你没法限制用户第一层必须使用 Lambda 表达式,所以用户不可能思考到诸如 Func<…,Func<>> 是做这个目的的。

至于 DSL,我觉得大部分尝试真的是玩具。论坛上好多人也热衷于折腾。但如果你想让你的类库有实用性,做 API 的时候正的必须要好好考虑所用语法的语义。

用奇技淫巧来写代码的时代已经过去了,C++ 的同僚们都开始清醒了……

Ivony 认为,这种 API 虽然有问题,但其实也并非完全不可接受:

老实说我是没觉得这样优美的……不过我觉得这样也不算是特别的那啥。我明白你的意思了,如果这个东西作为 API 提供的确是不太合适的。的确是不能“自然”的直接悟出使用方式。

好吧,我的确没用 API 的高度去约束它。但实际上现在.NET 的一些 API 的风格也在变,也有一些不是那么严谨的开始出现。我觉得这个度还是很难去把握。也同意你的观点。

接着,对于老赵眼中“较好的可读性”,装配脑袋也有自己的看法:

读着很通顺并不等于很容易地理解其行为。用户一般视方法为动作,每一步应该有每一步的语义。

Url.Of…这一步你想让用户看出什么语义呢?将 Url 转化为??? 类型吗?如果想让这个稍微有点语义的话,我看只能设计成这样:Url.CreateForType

对此,老赵解释道:

我是看整句的:“Url of HomeController’s action Post with paramters blog, post…” 其中只有“'s”和“with…”是我补充上去的,这也是我选择用 Of 作为方法名的原因。就像 FsTest 中:true |> should be True,它是作为一句话看代,而不是认为 should 作了一件事情,be 又做了另一件事情。

还有比如 Fluent NHibernate 中:Reference().Not.LazyLoad(),我觉得这也是在从整句进行考虑,而不是一个方法便是一个步骤。

最后,Ivony 也系统阐述了他对于 API 设计的思路:

嗯,其实我觉得应该说只是有分歧没矛盾。

你要我说,我欣赏老赵这样的语法么?老实说我不欣赏,虽然部分方法虽然是我提出的,但这种语法要我接受我还需要一些时间。我在考虑这个方法的时候,也过分的追求 Action(c.Post)( blog, post ) 而不是 Action( c.Post, blog, post ) 的形式,因为我觉得前者才像是函数调用,后者看起来就不像是函数调用了,这样会造成一些阅读障碍。对前种形式过分的追求还使得我竟然没发现后种形式是成立的(真是个低级错误)。

至于老赵所用的这样两个方法的配合,你要我说,我真的说现阶段在 API 我还是不能完全认同。……但我也觉得,虽然现在还难以接受,却也真的不是有什么强有力的根据说这种方式有多么不可取。诚然,这种方法如果按照传统的从方法签名参数含义来阐述,基本上 100 个人 99 个不知道怎么用。不给 Demo 几乎没办法正确的理解使用方式。

但是尽管我觉得别扭,却也不得不承认这样的方案不错。我并没有放弃语义更明晰的方案的探索和寻找,却也真找不出什么特别有份量的理由否决这种方式。

……

所以这种事情是个见仁见智的问题,接受是需要一些时间的,老实说我是比较传统的程序员。我坦白第一次看到 XElement 的构造函数的时候,我都觉得不是很舒服。这种事情怎么说呢,可以说现在双方的观点我都认同,所以说我觉得没有矛盾,只有分歧。从这个角度来说,A 是合理的,从另一个角度来说,B 是合理的。不存在一个角度 A 和 B 存在非此即彼的关系。

API 的设计是个永远的话题。尽管语言不断增强,但对于人们对于优秀 API 依旧进行着不断地追求。对于这个话题,您能给出自己的见解吗?

2009-11-30 05:543413
用户头像

发布了 157 篇内容, 共 52.1 次阅读, 收获喜欢 6 次。

关注

评论

发布
暂无评论
发现更多内容

背完这套Java面试八股文,自动解锁面试牛逼症被动技能

北游学Java

Java 数据库 面试 算法 开发框架

模块二作业

TIEDPAG

Vue进阶(九十二):应用 postMessage 实现窗口通信

No Silver Bullet

Vue 9月日更

如何判断一个项目是否值得投资?

石云升

9月日更

在用户故事中应该包含多细的细节?(译)——来自Mike Cohn

Bruce Talk

敏捷开发 译文 Agile User Story Product Owner

微信朋友圈的高性能复杂度

michael

#架构实战营

《计算机网络》读后感

codists

【LeetCode】二叉树的深度Java题解

Albert

算法 LeetCode 9月日更

架构实战营-模块二作业

南山先生

架构训练营

黑灯瞎火搞什么?搞智造!

脑极体

博客升级之在线代码编辑器

devpoint

编辑器 9月日更

【Flutter 专题】56 图解自定义 BubbleWidget 气泡插件

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 9月日更

网络攻防学习笔记 Day127

穿过生命散发芬芳

9月日更 办公网安全

极客时间-架构实战营2期-模块二作业

Dylan TANG

极客时间 第二周作业 极客时间架构师二期 架构实战营

10个步骤成为K8S云原生工程师

云原生

Kubernetes 云原生 K8S工程师

架构训练营模块二作业

guangbao

架构作业 - 模块二

Leo Zhao

架构训练营

架构实战营 模块七作业

孫影

架构实战营 #架构实战营

微信朋友圈高性能复杂度分析

一叶知秋

架构实战营

LeetCode刷题977-简单-有序数组的平方

ベ布小禅

9月日更

微信朋友圈架构设计

白开水又一杯

#架构实战营

大厂offer任你挑!覆盖面试中近98%Java高频面试手册

Java 编程 架构 面试 大厂

上线几小时下载量破百万!无价的这份阿里并发编程图册就这么强势

Java 编程 架构 面试 程序人生

大开眼界!数字人民币原来还有这些新玩法

CECBC

学习心得 - 架构训练营 - 第二课

Fm

0基础架构入门 - 2(架构设计复杂度模型和应对之道)

felix

架构实战营 0基础架构入门

【VueRouter 源码学习】第三篇 - 路由插件 install 的实现

Brave

源码 vue-router 9月日更

中国、女性与自然的鸣奏曲

脑极体

架构实战营模块二

WolvesLeader

「架构实战营」

架构实战课程 模块二作业

Frank

微信朋友圈复杂度分析

Nico

面向DSL设计API是否会形成语义的滥用?_.NET_赵劼_InfoQ精选文章