无知的 tonyseek

Yet Another Seeker

让 Web 控制器更有条理

这里的“Web 控制器”指的的确是 MVC 中的 Controller,在 Django 等 Python Web 框架中也被称为“视图”。

说到本文,源起 Python Web 框架中对于“控制器”有两种不同的表达方法。其中一种是类似于 Rails 的 class-based,另一种是 类似于 Sinatra 的 function-based(当然 Sinatra 的实际是 block-based)。tornado、web.py 采取了前者,而 Flask、Bottle、Django 采取了后者。

在组织简单代码的时候,两种方式仅仅是风格上的区别,这时候往往 function-based 会显得更加简洁。但对于更加复杂一点的情况,class-based 的控制器有一些代码组织上的优势,可能 function-based 的需要花一些脑筋才能达到相同效果。

举个例子,在 tornado 中如果要实现 Google 的 OpenID 登录,可以将和 OpenID 相关的逻辑分离成一个 GoogleMixin 类,然后混入到控制器中。这样组织出来的代码更加有条理。事实上,tornado 也自带了 GoogleMixin 类, 官网文档 中有个非常好的使用范例:

from tornado import web, auth

class GoogleHandler(web.RequestHandler, auth.GoogleMixin):
    @web.asynchronous
    def get(self):
        if self.get_argument("openid.mode", None):
            callback = self.async_callback(self._on_auth)
            self.get_authenticated_user(callback)
            return
        self.authenticate_redirect()

    def _on_auth(self, user):
        if not user:
            raise web.HTTPError(500, "Google auth failed")
        # Save the user with, e.g., set_secure_cookie()

从范例可以看到,这种 class-based 的控制器不仅能将某一个主关注点的代码分离,还能将细节步骤以类成员函数的形式写出来,代码条理性非常好。但是如果是 function-based 的控制器呢?恐怕就不像写 hello, world 的时候那么舒服了。

from flask import Blueprint

app = Blueprint("app", __name__)

@app.route("/")
def home():
    # 这里还很舒服
    return "hello, world"

@app.route("/oauth2/google")
def oauth2_google():
    # 这里应该怎么组织呢?
    return "= =!"

分析一下造成这种差异主要的主要原因:

  • 类可以以继承的方式复用代码,函数不行
  • 类可以包含多个成员函数组织代码,函数只能借助全局作用域的其他函数

所以,现在要做的是提出对策:

  • 函数可以以装饰器的形式复用代码
  • 装饰器可以写成类的形式,以求分离成员函数,提高代码条理性

用伪代码表达这两个对策就是:

import functools

class ComplexFlow(object):
    """Decorator Class."""

    def __init__(self, view_function):
        self.view_function = view_function
        functools.update_wrapper(self, view_function)

    def prepare(self):
        pass  # complex process

    def on_unauthenticated(self):
        pass  # complex process

    def result_handle(self, raw_result):
        return raw_result  # complex process

    def __call__(self, *args, **kwargs):
        self.prepare()
        try:
            result = self.view_function(*args, **kwargs)
            return self.result_handle(result)
        except Unauthenticated:
            self.on_authenticated()
            return abort(401)

complex_flow = ComplexFlow


# -----
# Usage
# -----

from flask import Blueprint

app = Blueprint("app", __name__)

@app.route("/mywork")
@complex_flow
def mywork():
    return "= =!"

这么一来代码就更加有清晰了,实现了“复用”和“分离”两个目标。如果结合比 Class Request Context 更灵活的 LocalThread Context,完全可以将复杂的 function-based Web 控制器组织得比 class-based 的更有条理。

Comments