如何编写可重用代码

看了PyCon 2015的一个演讲,名为"How to Write Reusable Code",其中有几个观点,很有意思。

fewer classes, more functions

很多从Java,C#等强面向对象语言过来的人,在使用Python时,不管做什么事,下意识先建一个类,再写方法。每一个函数必须放到一个类里,这是一个陋习。比如:

1
2
3
4
5
6
7
8
class ThingLoader(object):
def __init__(self, host, id):
self.host = host
self.id = id
def load(self):
# connect to host
# load thing identified by id

ThingLoader类只做一件事,就是根据id,到hostload一个东西。很简单的事情,用类来写,啰嗦得很。一个更好的版本是:

1
2
3
def load_thing(host, id):
# connect to host
# load thing identified by id

简单,明了。记住:

如无必要,勿增实体。

但尽量减少使用类,也不是什么事都用函数来做,比如:

1
2
3
4
5
6
7
8
9
10
11
def is_draft(conn, article_id):
'''return true if the specified article is unpublished'''
def is_published(conn, article_id):
'''return true if the specified article is published'''
def load_article(conn, article_id):
'''load article from database over conn, returning body'''
def save_article(conn, article_id, body):
'''write article to database over conn'''

上面几个函数,做的事情相近,可能还公用一些变量,这样零散的写成函数,不如整合成一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ArticleStore(object):
def __init__(self, conn):
self.conn = conn
def is_draft(self, article_id):
'''return true if the specified article is unpublished'''
def is_published(self, article_id):
'''return true if the specified article is published'''
def load_article(self, article_id):
'''load article from database, returning body'''
def save_article(self, article_id, body):
'''write article to database'''

如何判断该用类,还是用函数呢?

如果方法之间有共享状态(shared state),就用类;反之,用函数。

Python中,类最主要的作用,是给多个方法提供共享状态,这些状态的集合,就是对象。

Java的类,除了状态共享,还有封装的作用,Python里如何体现呢?

别忘了,Python封装体系中,除了package和class之外,还有一层module,这是很多人忽略的。一个函数,放在module中,并非没有封装,这个module,就是一层封装。这也是为什么Pythonic的代码中,基本不会出现static mathod——因为所有静态方法,都可以提取到module作为函数存在。

functions ≠ procedures

在古老的Pascal中,代码块分成函数(function)和过程(procedure),两者的区别,在于副作用。没有副作用的,比如计算、查询等,叫做function;有副作用的,比如打印一行文字、向文件追加一段内容等,叫做procedure。

现代语言对两者并不区分,这不是个好事。

写出可重用的代码,是希望让其他人使用,如果函数职责混乱,既做计算,又做输出,对于另一位程序员——代码的用户——来说,很容易不知所措。例如:

1
2
3
4
5
6
7
8
9
def get_foo(self, foo=None):
'''Query the server for foo values.
Return a dict mapping hostname to foo value.
foo must be a dict or None; if supplied, the
foo values will additionally be stored there
by hostname, and foo will be returned instead
of a new dict.
'''

上面这个函数在参数fooNone时,返回一个dict,而不是None时,将查询的值写进foo。这么做可以避免创建一个新的dict,是对性能的一种优化。但对于用户来说,函数的职责不够明确。

Greg在演讲中给出一种做法,在Python代码中区分两者,即:

如果函数有返回值,就不该有副作用;如果函数有副作用,就不要有返回值。

上面那段代码,可以改成:

1
2
3
4
def get_foo(self):
'''Query the server for foo values.
Return a dict mapping hostname to foo value.
'''

不仅仅是函数变简单,连注释都更简洁清楚。事实上,通过给API函数写注释文档,可以检验函数的设计是否满足单一指责。如果很难解释一个函数在做什么,说明设计有缺陷。

这其实就是命令查询分离(command–query separation, CQS)模式,或者通俗一点,叫做读写分离。一个函数,要么只做查询类操作,没有副作用,返回查询结果;要么只做命令操作,具有副作用,不返回结果。

命令操作不返回结果,如何判断命令执行是否成功呢?一种方法是使用异常,如果出错,抛出异常;不抛出异常,说明执行成功。

函数式编程思想非常强调无副作用。尽管Python不是纯函数式编程语言,依然可以通过分离读写操作,写出函数式风格的代码。

CQS模式还可以更进一步,即命令查询职责隔离(Command Query Responsibility Segregation, CQRS)。

fewer frameworks, more libraries

库,还是框架?

库,是一组可重用的代码,其他程序员可以选择自己需要的部分使用。

框架则不同,用户必须遵照框架的要求写代码,而且不能按照自己的需要对框架进行取舍。

我曾经想在一个项目中使用django的数据库关系影射,但项目本身不是web项目,不需要完整的django框架。但django的组件无法独立使用,最后只好作罢。

所以,可重用代码最好通过库的方式提供,有助于更多的人使用。