无绪:API 设计的终极目标
在《软件框架设计的艺术》这本书里面,提到一个 API 设计原则,称之为无绪(cluelessness)。
无绪是这样一个概念: API 的设计应该尽可能地『自解释』,让客户端程序员(也即是使用者)通过少量学习甚至不学习的情况下,就能使用该 API 。
举个例子,购买过电子产品的朋友可能就有过这样的经历:一个设计得好的产品,它的操作应该是完全直观、流畅、开箱即用的,你可以在不看一页产品说明书的情况下,将整个产品的功能弄懂。
而如果不幸碰上了设计得不好的产品,你就会发现自己在尝试操作的过程中频繁碰壁,最后只好求助于说明书,客服或者互联网。
那么,很明显,设计无绪 API 的目标,就是造出不用看说明手册(文档)就能使用的 API —— 这听上去有点唬人,作为程序员,我们为了学习某样知识,通常需要花费大量的时间阅读各种各样的文档、书本和博客 —— 要学习某样东西,你就必须去读那该死的文档(RTFM),对我们已经成为了一种习惯,那么,自然地,一种不用读文档(或者源码)也能使用的 API ,对程序员来说肯定是非常有诱惑力,同时,这也让我们心生疑惑:这种 API 可能存在吗,如果存在,这种 API 是什么样子?又或者,所谓的『无绪』只是书里面捏造出来的一个子虚乌有的新名词?
为了证实无绪 API 的确存在,我在脑海中搜索自己学习各种语言和库 API 的经历,很快,我找到了自己一次使用无绪 API 的经历,那次经历的确是让我印象深刻,只是当时还没有意识到『无绪』的存在,好吧,现在,就让我来说说这事。
一次使用无绪 API 的经历
Django 是最常用的 Python 框架之一,绝大部分 Python 的使用者都有学习它的经历,它和很多大而全的框架一样,有各种各样不尽如人意的小问题,其中一个就是它的模板的解释速度非常慢——这种慢说来有点夸张,几乎不用计时测试,你直接用肉眼就会发现 Django 的模板解释速度非常慢,因此,大部分 Django 的使用者学习 Django 之后的第一件事不是用 Django 去写程序,而是给 Django 换一个模板引擎(笑)。
在我学习 Django 的时候,我也遇到了这样的问题,于是,我搜索 Google ,查看各种 Python 模板引擎的信息,经过一番筛选之后,我将目标锁定在 Jinja 和 Mako 两个模板引擎上面,因为网上对它们的反响都不错,而且在很多计时测试当中,它们基本都排在前两位,这样就能保证速度不再是程序的瓶颈。
其中,Mako 的特点是非常快,它使用的语法基于 Python 自身,但是增加了一些额外的关键字,整体语法比较复杂。
另一方面,Jinja 也比 Django 的模板快不少,但它比起 Mako 还是稍慢一些,但是,我发现 Jinja 的语法完全模仿了 Django 的语法, Jinja 用起来就像 DJango 的模板一样,如果我使用 Jinja 代替 Django 的模板的话,那么我连一行文档读不用看,就可以直接用 Jinja 写程序,因为它的语法和 Django 模板的一样,而在此之前,我已经通过文档学习过 Django 模板的语法。
发现了 Jinja 的这个好处之后,我就马上决定使用 Jinja ,而不是 Mako ,原因很简单,我不想使用一种慢得离谱的模板引擎,同时,在足够快的 Jinja 和 非常快的 Mako 之间,我更喜欢让我不再读更多文档、马上就能使用的 Jinja ,如果将来连足够快的 Jinja 都不能满足我的需求的话,我再来学习怎么用 Mako 好了 。
有趣的是,很多 Python 程序员和我的想法一样,通过一些帖子我了解到,在很多时候,大家都喜欢使用 Jinja ,只有当性能真正成为问题的时候,才转向 Mako 。
我认为 Jinja 受欢迎的一个很重要的原因是它模仿了 Django 模板的语法,这使得它对那些为数众多的使学习过 Django 的人可以马上”学会“ Jinja ,期间不需要多读一行文档——因此,在这里我们可以说,Jinja 的 API 符合我们上面所说的『无绪』的原则,至少对于那些学习过 Django 模板的人来说是这样,而这样人并不在少数,最终,这种无绪在实践中成为 Jinja 的一种巨大的优势。
无绪实战(1)
好的,通过 Django 和 Jinja 的例子,我们已经见识到了无绪 API 的好处,但是,我们还没具体地知道, 无绪 API 到底是什么样子,还有,该怎么去实现一个无绪 API ?在以下的内容,我就继续以 OORedis 作为例子,从理论转向实战,看看 OORedis 是如何在实践中应用无绪原则的。
在前一篇文章中, OORedis 的第二版 API 基本就是 Redis 命令的直接翻译,比如对 Redis 的 Hash 结构,就有对应的类 Hash 及方法 get 和 set :
huangz = Hash('profile')
huangz.hset('name', 'huangz')
huangz.hget('name')
# => 'huangz'
以上代码等同于以下 Redis 命令:
hset 'profile' 'name' 'huangz'
hget 'profile' 'name'
这种形式的 API 可以满足基本需求,任何学习过 Redis 的人都可以使用这种 API 来操作 Redis, 这一点是毋庸置疑的——事实上,很多『驱动型』的库 API 都是以这种『将函数的接口直接翻译成类和方法』的风格被写出来的,在 Python 的包发布和管理网站 PYPI 上,这种『翻译』风格的 API 不在少数,一抓一大把。
但是,这种『翻译式』的 API 并没有真正发挥语言的的威力,因为这些 API 都是简单直接地『翻译』过来的,它们没有经过详细的思考,这些 API 虽然可以使用,但做得并不够好,也不够『无绪』。
比如说,上面展示的 OORedis 的第二版 API 就假定使用者一定学习过 Redis ,当这种假设不成立时,它们就不好用了——我想找一种足够无绪的方式,写出的 API 不仅仅是学习过 Redis 的人能用,它甚至可以让那些没有学习过 Redis 的人也一样可以通过 OORedis 来操作 Redis 。
无绪实战(2)
『让不会用 Redis 的程序员通过 OORedis 来使用 Redis ,而且不用看一行文档』,这想法初听上去有点疯狂,但这并不是没有可能的。
比如在前面,我们就看到 Jinja 通过模仿 Django 模板的语法,来让所有 Django 模板的使用者不用学习 Jinja 就能使用 Jinja ,这样看来,要让一个库 API 『无绪』起来,最简单的办法似乎就是让这个库去模仿另一个人尽皆知的库,这样就可以减少这个库的学习和使用成本。
那么, 看回来, OORedis 应该去模仿哪一个库呢?
模仿一个 SQL 库?嗯,这看上去不是一个好主意,因为 SQL 的功能要比 Redis 复杂得多,用 SQL 库的 API 去操作 Redis ,绝对是牛刀杀鸡。
又或者,可以尝试将 OORedis 写成 ORM , 这样的话,Django 的 ORM 就是一个很好的学习标本。但是,这也不是一个好主意,因为 OORedis 定位的目标是成为一个比 redis-py 更好的 Redis 通用库,它的抽象应该处于 redis-py 之上, ORM 之下,因此,它也不打算成为一个 ORM 。
嗯,如果常用库没有很好的模仿对象,那么是否可以尝试去模仿 Python 的标准库?事实上,这的确可以。
Python 语言内置有列表(list)、字典(dict)和集合(set)等结构,它们和 Redis 中的列表(list)、哈希表(hash)和集合(set)等数据结构非常相似。
对于 Python 的这些内置数据结构,在标准库中有一簇专门的 API 来操作这些内置的数据结构,很明显,OORedis 也可以照猫画虎,模仿标准库中的 API 去构建一套操作 Redis 的 API ,而这些 API 使用起来就像和操作 Python 内置的数据结构别无二致。
这样一来, OORedis 的 API 就变成以下的样子(以哈希表为例):
# 第二版
profile = Hash('profile')
profile.hset('name', 'huangz')
profile.hget('name) # => 'haungz'
profile.hexists('name') # => true
profile.hkeys() # => ['name']
profile.hvals() # => ['huangz']
profile.hlen() # => 1
profile.hdel('name')
profile.hexists('name') # => false
# 第三版
profile = Hash('profile')
profile['name'] = 'huangz'
profile['name'] # => 'haungz'
'name' in profile # => true
profile.keys() # => ['name']
profile.values() # => ['huangz']
len(profile) # => 1
del profile['name']
'name' in profile # => false
可以看到,第三版 OORedis 的 API 比第二版的更简洁易明,也更有 Python 味(Pythonic) ,而且,第三版还是一个无绪的 API —— 现在通过 OORedis ,任何 Python 程序员都可以像操作内置数据结构一样操作 Redis ,甚至不必学习任何一条 Redis 命令。
嗯,这真的很酷,不是么。
极速无绪
Python 提供了一簇名字有点特殊(或者说,奇怪)的方法,称为魔法方法,通过实现这些方法,可以实现一些 Python 的特殊功能。
比如说,如果某个类实现了 __getitem__ 和 __setitem__ 它就可以获得通过键来操作类的能力:
dict_like_object[key] = value
dict_like_object[key] # => value
另一方面,如果实现了 __len__ 方法,就可以通过函数 len 获取对象的某个值的数量:
list_like_object.append(item_1)
list_like_object.append(item_2)
len(list_like_object) # => 2
而要实现一个『无绪化』的、模仿 Python 内置数据结构操作方式的 OORedis API ,我们就要一个不漏地实现相应的魔法方法,但是,这样做起来很麻烦,原因有两个:
1. 要实现的魔法方法不少
2. 就算人工地实现了所有所需的魔法方法,但还是很难跟 Python 内置数据结构保持一致,比如说,在异常抛出问题上,人工实现的 API 的异常就很容易和 Python 内置数据结构抛出的异常不同,造成这个问题的主要原因是在人工的 API 和 Python 内置数据结构的 API 之间,没有一个统一的接口
幸好,Python 为这类问题提供了一组 ABC 类(abstract base classes),通过继承这些 ABC 类,我们只需要编写一簇魔法方法的最小子集合,就可以获得一整个功能完整的类,而且,这些类的行为几乎完全和相应的 Python 内置数据结构的行为一致。
比如说,只要继承 collections.MutableMapping 类,然后实现 __setitem__ 、 __getitem__ 、 __delitem__ 、 __len__ 和 __iter__ 五个魔法方法,我们就可以获得一个和 Python 内置的字典类(dict)功能上别无二致的类,这些类一共有十多二十个常用方法,而真正要写的只有五个 —— OORedis 中的 Dict 类就是这样子实现的,通过五个魔法方法,每个方法平均五六行代码,就这样简单地将 Redis 的哈希表结构的大部分功能完整地实现了。
一致性
在上面的例子中,我们看到 JInja 通过模范 Django 模板的语法,减少了对新手的学习成本,从而使得更多人特别是那些使用过 Django 模板的人更倾向于使用 Jinja 。
我们还看到, OORedis 通过继承 ABC 类,高效地模仿了 Python 的内置数据结构的标准库行为,让操作 Redis 数据结构变得像操作 Python 的内置数据结构一样简单。
我们说这些 API 是无绪的,因为它们都非常简单易用,只需很少的学习甚至无须学习就可以使用。
但是这里还有几个问题要澄清:
首先,前文所说的『模仿』在 API 相关术语中还有一个更专业的名词,就是『一致性』,比如我们说 OORedis 『模仿』了 Python 内置数据结构的标准库行为,换种更专业一点的说法,我们可以说 OORedis 的 API 和 Python 内置数据结构的标准库 API 『保持一致』。
其次,虽然上面的无绪的例子都是一些关于『一致性』的案例,但是实现无绪并不是只有『保持一致性』这一种方式,比如说,你可以将 API 写得非常简单易用,连小学生都能学会;或者将 API 写得非常通用,通用得 就像 SQL 语句似的;又或者,遵循一种特定的方式来写你的代码,比如 Erlang 的 OTP 和 jQuery 的 callback 函数就是一个例子。诸如此类。
总而言之,有很多方法可以让 API (和代码)变得更无绪,但『保持一致性』的确是一种达到『无绪化』目标的快速有效的手段:如果你要写一个库,但是你不确定 API 该怎么写,那么最简单的最方便的无绪方法就是模仿一个人尽皆知的库的 API ,那样的话,你的 API 的学习曲线就会非常的低,最起码对学习过你所模仿的那个库的人来说是这样子的。
第三,有时候,有多于一个 API 可供模仿,比如你发现你的库无论是模仿标准库还是外部库都非常适合,这时候,就有一个优先级列表(从先到后,从高到低):
1. 和 API 自身保持一致:这是最重要的,无论你模仿的是什么库,关键是内部要保持一种统一的风格。
2. 和语言自身或者标准库保持一致:这种一致性非常强大,如果你能做到,你就无敌了——因为所有能熟练使用这种语言(及其标准库)API 的人都可以很容易地使用你的 API 。
另外,语言自身一般会提供一些帮助来让你达到这个层次的一致性的目的,比如之前说的 Python 的 ABC 类和 Ruby 的 Mixin 机制。
3. 和常用库保持一直:每一些语言都有一些非常热门的库,这些库的 API 的使用者为数众多,以致于人们对这些库的熟悉程度和标准库相差无几。
比如,如果你使用 Python ,那么模仿 Django 的 API 就是一种不错的选择。如果你使用 Ruby ,那么模仿 Rails 中的库的 API 也不错。又或者,对于 Erlang 程序员, 无论是直接使用或者模仿, OTP 库都是一个很好的对象。
4. 到了这一步,其实就没有什么库是值得模仿的了,和一些小众的库保持一致并不一定能减少你的库的学习难度,不过如果某个库设计得很好,并且和你所写的库相差无几,那么学习一下也无妨。
总结
『无绪』、『一致性』是两个非常强大的技术,它们对于写出让人更容易理解和学习的 API 方面非常有帮助,想一想我们开篇的那个电子产品的例子,你是希望写一个要让人仔细看说明文档才能会用的 API ,还是要写一个上手即用的 API ?
如果你的选择是后者的话,那么请谨记, 『无绪』和『一致性』是你的好朋友,下次写 API 的时候,不妨先想想,我要怎么让这个库易用易学起来?怎么让它『无绪』化?
如果你真的这样做了的话,我相信你最终完成的 API 比起一个直接『翻译』过来的 API 会更好用,写出的代码也会更漂亮。
待续
函数式编程(functional programming)已经成为一个越来越热的话题了,越来越多的编程语言都在增加各种各样的函数式语言的机制,争取让整个语言变得更健壮,更好用。
实际上,不但编程语言可以通过学习函数式编程来提高自身的质量,一个库的 API 也可以。
在本系列的下篇文章中,我将讲述一些例子,说明怎么通过将 API 变得更『函数式』化,来让 API 变得更简单、易用和通用,并且,这一切只需要使用 Python 就能做到。
脚注
关于Jinja 和 Django
Jinja 的作者 Armin Ronacher 写了一篇文章,分析和比较了 Mako 和 Jinja 以及 Django 的模板: http://lucumr.pocoo.org/2008/1/1/python-template-engine-comparison/
Jinja 项目上的文档,可以看到,Jinja 非常容易学习,其中和 Django 的差别只有很少: http://jinja.pocoo.org/docs/switching/
StackOverFlow 上对 Jinja 和 Mako 的比较: http://stackoverflow.com/questions/3435972/mako-or-jinja2
关于 Mako 和 Jinja 速度的比较,在这个文章中,新版的 Jinja 速度已经超过了 Mako: http://techspot.zzzeek.org/2010/11/19/quick-mako-vs.-jinja-speed-test/
关于 Python 的魔法方法和 ABC 类
Python 的 collections 库中的 ABC 类: http://docs.python.org/py3k/library/collections.html#abcs-abstract-base-classes
Python 的数据模型的描述: http://docs.python.org/py3k/reference/datamodel.html
关于 OORedis
OORedis 的项目主页: https://github.com/huangz1990/ooredis
Dict 类的实现代码: https://github.com/huangz1990/ooredis/blob/master/ooredis/mix/dict.py
关于《软件框架设计的艺术》
这本书我个人的感觉其实写得一般,我看完整本书获得唯一深刻的概念就是『无绪』两个字了,仅此而已,也许是因为我不是一个 JAVA 程序员吧。(笑)
分享到:
相关推荐
图形化a+b,可以锻炼你的记忆力和算数速度
柔性输送线sw18可编辑全套技术资料100%好用.zip
本汽车票网上预订系统管理员和用户。管理员功能有个人中心,用户管理,汽车票管理,订单管理,退票管理,换票管理,反馈管理,留言板管理,系统管理等。用户功能有个人中心,汽车票管理,订单管理,退票管理,换票管理,反馈管理等。 内含文档,可轻松上手。
自动锁螺丝机细化完全step全套技术资料100%好用.zip
【创新无忧】基于matlab龙格库塔算法RUN优化极限学习机KELM故障诊断【含Matlab源码 10715期】.zip
pll电荷泵锁相环 cppll(已流片)仿真环境搭建好了 电路到版图都已流片验证,另外送PLL书籍电子版和对应工艺库。 另加50就可以得到完整版图 三阶二型锁相环 参考频率50-100MHz 分频比可调 锁定频率600M-2GHz 锁定时间4us 环形振荡器 ring vco 鉴频鉴相器PFD模块 分频器DIV模块 ,ps counter 电荷泵CP模块
智慧社区有管理员和客户两个角色。客户功能有车位信息,社区信息,周边服务,问卷调查,爱心助老,通知公告,留言反馈,个人中心,客服中心,在线报修管理,投诉建议管理,车位租买管理,社区信息管理,参与答卷管理,我的收藏管理。管理员功能有个人中心,客户管理,在线报修管理,投诉建议管理,车位信息管理,车位租买管理,社区信息管理,周边服务管理,问卷调查管理,参与答卷管理,爱心助老管理,留言板管理,系统管理。 内含文档,可轻松上手。
本科生课程设计封面.doc
展示PRD文档的关键要素编写具体示例。同时提供了一份模板,方便撰写PRD文档。
基于matlab的用于分析弧齿锥齿轮啮合轨迹的程序,输出齿轮啮合轨迹及传递误差。 程序已调通,可直接运行。 程序保证可直接运行。
【创新无忧】基于matlab向量加权平均算法INFO优化极限学习机KELM故障诊断【含Matlab源码 10732期】.zip
仓库管理系统(一个毕设) 毕业设计项目《仓库管理系统(manager_sys)》的概述和指南: 项目标题 《基于Spring MVC和Vue.js的仓库管理系统设计与实现 —— 毕业设计项目》 项目概述 本项目是一个基于Spring MVC、Spring Security、Spring、MyBatis、PageHelper和Vue.js框架的仓库管理系统。系统旨在提供高效、安全的库存管理解决方案,包括权限管理、商品管理、订单处理和库存预警等功能。 系统特点 权限管理:利用Spring Security实现基于角色的访问控制(RBAC),动态分配权限。 业务流程:涵盖商品、订单、库存的完整操作流程,确保库存管理的准确性。 日志记录:通过Spring AOP实现操作日志的记录,便于追踪和审计。 数据统计:首页展示商品销量统计图和每日销售统计图,直观展示业务状况。 系统预览 登录和首页:用户登录后进入系统首页,查看统计信息。 产品管理:管理商品信息,包括添加、修改、删除等操作。 订单管理:处理订单,包括创建订单、更新库存等。 权限管理:管理用户角色和权限。 日志管理:查看系统操作日志。 运
A星算法 A*算法 自己研究编写的Matlab路径规划算法 Astar算法走迷宫 可自行设置起始点,目标点,自由更地图。 ——————————————————— 可以和人工势场法融合 动态障碍物
《MATLAB神经网络原理与实例精解》是一本深度学习初学者的理想教程,它全面涵盖了神经网络的基础理论以及MATLAB实现方法。这本书旨在帮助读者理解神经网络的工作原理,并通过具体的MATLAB实例,让读者能够动手实践,从而深入掌握神经网络在实际问题中的应用。 神经网络是一种模仿人脑神经元结构的计算模型,它由大量的处理单元——神经元组成,通过权重连接形成复杂的网络结构。在深度学习领域,神经网络被广泛用于图像识别、语音识别、自然语言处理等任务,因其强大的非线性建模能力而备受青睐。 MATLAB作为一个强大的数值计算和数据可视化环境,为构建和训练神经网络提供了便利的工具箱。MATLAB神经网络工具箱(Neural Network Toolbox)包含了各种类型的神经网络模型,如前馈网络、卷积网络、递归网络等,以及训练算法,如反向传播、遗传算法等。通过这些工具,用户可以快速构建网络结构,调整参数,进行训练和验证,并将模型应用于实际数据。 本书首先会介绍神经网络的基本概念,包括感知机、多层前馈网络和反向传播算法。然后,将详细讲解如何在MATLAB中搭建这些网络,包括网络结构的设计、权重初始
Matlab领域上传的视频是由对应的完整代码运行得来的,完整代码皆可运行,亲测可用,适合小白; 1、从视频里可见完整代码的内容 主函数:main.m; 调用函数:其他m文件;无需运行 运行结果效果图; 2、代码运行版本 Matlab 2019b;若运行有误,根据提示修改;若不会,私信博主; 3、运行操作步骤 步骤一:将所有文件放到Matlab的当前文件夹中; 步骤二:双击打开main.m文件; 步骤三:点击运行,等程序运行完得到结果; 4、仿真咨询 如需其他服务,可私信博主; 4.1 博客或资源的完整代码提供 4.2 期刊或参考文献复现 4.3 Matlab程序定制 4.4 科研合作
ABAQUS动,静力学模型;车辆-轨道耦合动力学;钢轨不平顺程序;批量非线性弹簧;单向弹簧(收拉不受压或受压不受拉),温度耦合等。 轨道检算(超高,超限,出报告);土木建筑有限元建模分析。
教学督导检查情况表.docx
基于springboot的逍遥大药房管理系统--论文.zip
win32汇编环境,理解BeginPaint函数与GetDC函数的区别
调试过可以运行。 开发语言:Java 框架:springboot JDK版本:JDK1.8 服务器:tomcat7 数据库:mysql 5.7(一定要5.7版本) 数据库工具:Navicat11 开发软件:eclipse/myeclipse/idea Maven包:Maven3.3.9