本文翻译自 Abstracting Control Flow。
所有的程序员都在持续不断地创造抽象,尽管有时候连他们自己也意识不到。我们平常最常抽象的是运算(写成函数)或者行为(子程序或者类),但其实我们的工作中还有一些其他的重复的模式,特别是异常处理、资源管理与优化。
这些重复的模式通常会引入一些规则,例如「关闭所有你打开的东西」,「释放资源然后抛出异常」,「如果成功了则继续,否则……」,这些代码通常都是重复的 if ... else
或者 try ... catch
。不如把这些控制流也抽象出来?
在那些没人秀技巧的常规代码里,通常使用控制结构来控制流程。但有时候它们并不能完成得太好,于是我们就只能靠自己动手了。这在 Lisp、Ruby 或者 Perl 里面很容易做到,在所有支持高阶函数的语言中也有办法可以做到。
抽象
让我们从头开始吧。创建一个新的抽象我们需要做些什么呢?
- 选择一个功能或者行为。
- 给它命名。
- 实现它。
- 把我们的实现细节隐藏在这个命名之后。
第三点和第四点不一定总是可以做到的。这与你试图抽象的东西和你所使用的语言的灵活性有非常大的关系。
如果你所使用的语言做不到这些,那就忽略实现这一步,仅仅描述实现的方法就好了,然后想办法让它流行起来,从而创造一个新的设计模式。这样你就不会对你将要写的那些重复代码感到糟糕了。
回到现实
这是一段普通的 Python 代码,它是从真实的项目中拿出来的,只做了很少的修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
这段代码实现了好几个功能:迭代 urls
、下载图片、把图片收集到 photos
里、忽略小图片和在下载失败时重试。这一大堆功能全都塞进了这段单独的代码中,尽管它们在这段代码之外也可能被用到。
这其中的有些功能其实已经存在了。比如,迭代后的结果聚合其实就是 map
:
1
|
|
让我们来实现其他的功能。先从忽略小图片开始,它可以写成:
1 2 3 4 5 6 7 8 9 10 11 |
|
看起来不错的样子。但这样写不能轻易地与 map
组合起来使用。让我们暂时忽略它,先来解决网络错误问题。我们可以试着用我们刚刚处理 ignore
的方法来抽象它:
1 2 |
|
只有这个是不能被正确实现的。Python 的 with
语句不能重复地运行它包含的语句块。我们碰到了语言的限制。如果你想要在语法层面之上理解不同语言之间的差别,那么注意到这样类似的情况是很重要的。在 Ruby 和 Perl 的小扩展版本中,我们可以继续操作语句块,在 Lisp 中我们甚至可以操作代码(这可能有点杀伤力过猛了),但是这些特性在 Python 中全失去了,我们应该转而使用高阶函数和它们的简化形式——装饰器:
1 2 3 4 5 6 7 8 9 10 11 |
|
我们可以看到,它甚至自然地就可以和 map
同时工作了。另外,我们还得到了一对非常有用的可复用的工具:retry
和 http_retry
。不幸的是我们的 ignore
contextmanager 不能轻松地在这里添加。它是不可被组合的。让我们来把它也重写成装饰器:
1 2 3 4 5 6 7 8 9 10 11 |
|
这么做好在哪儿?
似乎我们现在为实现同等的功能写了更多的代码。不同的地方就在于这些功能现在没有混乱地耦合在一起了,并且它们是组合起来的,这意味着这几件事情:
- 每个单独的功能都是可见的,
- 它被命名了,
- 它可以很容易地被使用和输出,
- 它是可复用的。
在我们使用函数式的流程控制之后只用了最后 4 行基础代码来实现这些功能,这也许使代码变得更可读了。或者也没有,毕竟这是一种主观判断。我仍然希望这篇文章能帮到一些人写出更好的代码。
P.S. 我把 @decorator
、ignore
和 retry
打包到了一个实际的项目中。
P.P.S 其他控制流程抽象的例子有:underscore.js 中的函数操作,列表推导式和生成器表达式,模式匹配,函数重载,装饰器缓存等等。