Python API 设计(2):无绪和一致性

无绪: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 程序员吧。(笑)

 

 

 

 

 

 

 

 

 

 

 

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值