旧瓶装新酒——动态作用域
背景
在项目 X 中,有一个函数get_db_reader
,顾名思义,它可以返回一个只读的数据库连接,其实现代码可能是下面这样的
DB_READER_CONF = {}
"""数据库只读账号的连接参数。"""
def connect(**kwargs):
"""基于配置获取数据库连接。"""
pass
def get_db_reader():
"""获取只读的数据库连接。"""
return connect(**DB_READER_CONF)
这个函数在整个项目中大量使用。例如,在获取用户信息的接口中,需要查询用户的账号、个人资料,以及不同类型的小红点数据,需要多次调用函数get_db_reader
。
随着业务的发展,项目的数据库实例增加了从库,经过评估后,认为可以将获取用户的接口中的查询动作都从主库切换到从库中。此时代码可以有多种的修改方案:
- 将原本调用
get_db_reader
的代码都替换为调用新函数get_db_slave_reader
; - 函数
get_db_reader
新增一个参数use_slave
,传入True
表示使用从库。然后,在原本调用get_db_reader
的地方都补充参数use_slave=True
; - 在接口的入口位置,修改全局变量
DB_READER_CONF
为指向从库的配置。等到接口的业务逻辑处理完毕后,再恢复DB_READER_CONF
原本的配置。
动态作用域就可以优雅地实现这里的第 3 个方案。
动态作用域
由于 Python 不支持动态作用域,因此接下来的例子需要请出本站的老熟人 Common Lisp 来做演示。先将前文中的示例代码改下为 CL
(defparameter *db-reader-conf* (list
:host "主库")
"数据库只读账号的连接参数。")
(defun connect (&rest args &key host)
(declare (ignorable args))
"基于配置获取数据库连接。"
(format t "host = ~S~%" host))
(defun get-db-reader ()
"获取只读的数据库连接。"
(apply #'connect *db-reader-conf*))
上述代码中由语法defparameter
定义的变量*db-reader-conf*
就是一个动态作用域的变量。在函数get-db-reader
中引用这个变量的时候,它的值取决于运行时赋予了它什么值。例如,如果直接调用函数get-db-reader
,那么访问的就是默认的主库
如果使用let
给*db-reader-conf*
一个临时的绑定关系,那么在函数get-db-reader
内获取到的就会是这个临时的值,如下列代码所示
(defun main ()
;; 默认获取的是主库的连接。
(get-db-reader)
;; 在不改变实现代码的前提下,切换到从库。
(let ((*db-reader-conf* (list
:host "从库")))
(get-db-reader)))
运行效果如下图所示
词法作用域
前面的例子看起来平平无奇,因为变量*db-reader-conf*
就像是其它语言中平平无奇的全局变量一样。在别的不具备动态作用域特性的语言中,也可以在调用get-db-reader
前修改*db-reader-conf*
的值,然后在函数返回后再恢复原来的值。因此,这里再看看如果*db-reader-conf*
其实是一个词法作用域的变量时的效果。
为了将其定义为词法作用域的变量,接下来不能再使用defparameter
,而必须用let
来定义这个顶层的变量
(defun connect (&rest args &key host)
(declare (ignorable args))
"基于配置获取数据库连接。"
(format t "host = ~S~%" host))
(let ((*db-reader-conf* (list
:host "主库")))
(defun get-db-reader ()
"获取只读的数据库连接。"
(apply #'connect *db-reader-conf*)))
这样一来,*db-reader-conf*
就只能被let
范围内的代码访问到,也就是函数get-db-reader
。这时候再运行上面的main
函数,就丝毫无法影响到get-db-reader
的运行逻辑
全文完。