用面向对象方法组织 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)
改造后程序运行效果不变。但应用程序本身的初始化动作,包括各个扩展的初始化,现在有了专门的类去管理,逻辑更清楚了。