用面向对象方法组织 Flask 应用程序 (一)

版权声明:所有博客文章除特殊声明外均为原创,允许转载,但要求注明出处。

Flask

Flask 是著名的 Python Web 微框架,而 《Flask Web 开发——基于 Python 的 Web 应用开发实战》(OReilly出版社出品,以一只大狗作为封面,所以也有人戏称“狗书”)则是这一框架的经典书籍。特别是该书的第七章,描述如何将网站划分为多个模块,很多 Flask 网站都是参照该例子的形式进行规划的。

我的 个人主页 也用了 Flask 框架来开发,网站结构在很大程度上参考了该书的示例。但在开发过程中,我也感觉到该方式也有一些不够合理的地方,主要表现在:

Flask App 和 Blueprint 存在循环引用问题

这是最先引起我警觉的地方,因为一般来讲,循环引用意味着设计上很可能出现了问题。出现这种困境的原因在于,定义视图函数需要先有app/blueprint 来声明装饰器,而app/blueprint又需要视图函数才能定义路由规则。书上的例子用分离导入的方法来解决此问题:

from flask import Blueprint

main = Blueprint('main', __name__)

from . import views, errors

通过小心组织 import 的顺序,确实可以避免出现错误。但我并不认为这是个好办法,因为它并不直观,并且破坏了正常的代码组织逻辑。

太多全局变量

所有 Flask 插件,全部以变量的形式定义在最顶层模块。这样虽然方便了一些辅助函数的编写,但和 Javascript 的问题类似,污染全局命名空间从长远来说是糟糕的设计。

路由定义过于分散

@app.route(...) 的定义方式第一眼看上去确实亮眼。然而程序逐渐变大以后,我意识到:如果让路由定义散落在各个函数中,读者在遍历整个文件之前,很难弄清楚整个网站整体的导航结构。为了让 URL 设计意图更加明确,集中式的路由定义或许更好。

当然,在实践上,你可以在程序跑起来以后查看 app.url_map 获得完整的路由定义,所以这个问题并不是那么严重。集中式的定义可以对整个路由结构一览无余,但是要引用所有函数,写起来要麻烦一点。我的观点是,这个问题可改可不改。

配置文件成了大杂烩

以前在 Django 程序中见过类似的问题,现在 Flask 程序中再次出现:app.config 是一个平面的字典,所有业务都在往里插入自己的配置项,config.py 慢慢变成了一个庞大的垃圾堆,没有组织,没有层次。为了避免名称重复,许多键值长得让人没有看的欲望。

虽然说了那么多问题,不过 Flask 的好处是,你不需要全盘照搬别人的设计,而是可以按照自己的意图重新组织代码。经过思考,我确定了重构的主要目标:

  • 用面向对象的方式重新组织各个函数,提高内聚性,消除大多数全部变量;
  • 取消装饰器,而是用add_url_rule方法,将路由规则的分散定义改为统一定义,以提高可读性。

首先要动手术的是全局的app对象和各个插件。我把原例子的工厂方法 create_app 改造成类继承的方式,这样方便定义和分解各项初始化工作。同时,需要用到的插件也统一定义到 app.extensions,不再用全局变量。以下是经过改造的程序类代码,删除了一些次要内容:

from flask import Flask

class App(Flask):
def __init__(self):
super(App, self).__init__(__name__)
self.plugins = PluginRegistry(self)
self.init_config()
self.register_blueprints()

def register_blueprints(self):
... # omit for now

def init_config(self):
from config import Config
self.config.from_object(Config())

def add_manager_command(self, name, fn):
from falsk_script import Command
self.plugins.manager.add_command(name, Command(fn))


class PluginRegistry:
def __init__(self, app):
self.app = app
self.register_manager()
... # register other extensions

def register_manager(self):
from flask_script import Manager
self.manager = Manager(self.app)

其中的Blueprint是另一个话题,这里暂且不展开了。另外注意 App 类定义了一个 add_manager_command 方法。这是因为某些外壳命令——比如 tests ——其定义位于 app 之外,一般认为引用自身位置之外的代码是一种不好的做法。所以这里只提供注册的接口,由最外部的 manage.py 来管理。这也是面向对象的主要观点:由合适的类来完成合适的职责。

经过改造后,manage.py 也简单了一些:

from app import App
import tests

app = App()
app.add_manager_command('test', tests.run_all)

if __name__ == '__main__':
app.plugins.manager.run()

这里为了方便测试加入了一个外壳命令,可以用 ./manage.py test 来执行。Test 基本框架如下:

import unittest


def run_all():
"""Run all unit tests."""
suite = unittest.TestSuite()
loader = unittest.TestLoader()
suite.addTest(loader.loadTestsFromTestCase(...))
unittest.TextTestRunner(verbosity=2).run(suite)

改造后程序运行效果不变。但应用程序本身的初始化动作,包括各个扩展的初始化,现在有了专门的类去管理,逻辑更清楚了。