大家好,这是我在公众号上发的第一篇编程文章,希望大家喜欢
前几天在给一位同学上课,中途我们讲到了一个叫做依赖注入(Dependency Injection)的设计范式。然后我布置作业,说将后端代码从“直接使用全局变量”重构成“依赖注入”。
第二天这位同学说并没有搞懂怎么弄。所以我觉得有必要在这里给出具体例子,让大家可以一目了然。
首先我们来看具体的问题。
我们有一个简单的 Python Flask 应用,大致长这个样子。
fromflaskimportFlask
app = Flask("example")
classDAO:
def__init__(self):
self.data = []
dao = DAO()
@app.route("/")
defm():
returndao.data
if__name__=="__main__":
app.run()
DAO 是 Data Access Object, 在一个真正的后端应用中,多半是一个数据库的连接点。它的职责就是提供不同数据的CRUD(Create,Read,Update,Delete)。当然,这位同学课上的应用比这个例子有更多的 REST 路由和 DAO。这里为了简便,将代码大量简化了。
一切都运作良好,唯一有个小问题:dao 是个全局变量。
为什么在这里使用全局变量会是一个问题呢?我们可以从测试与功能拓展的容易程度来看。
首先,从测试的角度,使用全局变量让单元测试(Unit Test)的繁琐程度与难度直线上升。
通常情况下,我仅仅是想测试 REST API handler ,并不想测试 dao 本身的逻辑。使用全局变量让这几乎办不到了。因为每个 handler 直接使用了 dao 这个变量。也就是说,每次运行一个 handler,都会运行 dao 本身的逻辑。如果 dao 是一个真正的数据库连接,那么每个测试都会与数据库进行交互。这就变成了整合测试(Integration Test)。有些时候,我们的确需要这样做。但这不是单元测试的目的。
其次,从功能拓展的角度,如果 dao 的接口被改动了,那么因为每个 handler 都直接使用那个全局变量,所以很有可能造成多个 handler 的代码也得跟着改。这就是耦合程度过高。牵一发而动全身是最糟糕的软件工程。
如何解决?
1. Monkey Patch (糟糕的方法)
因为 Python 是一个超级反射、超级元编程的语言,所以很多人会使用一种叫做 monkey patch 的方法。简单地说,就是在一个作用域(scope)中,将目标对象的某些成员替换掉,然后在离开这个 scope 时,将之前被替换掉的成员替换回来。
因为 Python 的文件就是模组,而一个模组也是一个对象,所以可以在每个单元测试中将 dao 这个全局变量给替换成测试所用的实现。
这是一个糟糕的方法。首先 monkey patch 总是要写很多不必要的代码。而且 monkey patch 要求测试的作者必须 100% 记住被测试对象的源代码依赖关系,不然几乎就要写错。Type 信息也更有可能丢失。因为 monkey patch 发生在 runtime,而不是 load time,所以 IDE 几乎也给不出很多提示。
不管从 OO Design 的角度还是实践的角度,monkey patch 都是一种劣等的策略。我只在没有办法的时候,才使用 monkey patch。(比如代码库已经太耦合)
(没有看懂 monkey patch 的同学不用担心,因为它根本不中用。如果你好奇,我也许可以单独做一个 monkey patch 的视频)
2. Dependency Injection 依赖注入 (优等的方法)
在讲解之前,我们直接来看修改后的代码吧。
fromflaskimportFlask
classDAO:
def__init__(self):
self.data = []
defApp(dao):
app = Flask("example")
@app.route("/")
defm():
returndao.data
returnapp
if__name__=="__main__":
app = App(DAO())
app.run()
这里我们做的改动其实很简单。定义了一个 App 函数,其作用是初始化我们的 Flask app。dao 作为一个参数传进来。这让整个代码的耦合性直线下降了。那么在测试中,每个单元测试可以单独调用一次 App 函数,得到一个单独的 Flask 实例,然后 App(dao) 的参数 dao 可以是单独实现的。
这样第一不用管 Flask 的 handler 和 DAO Class 的源代码依赖,因为他们之间没有依赖了。handler 现在只依赖于一个有 data 成员的对象,而不是 DAO Class 的实例。
第二,因为测试没有依赖于同一个全局变量,所以每个测试也是相互独立的。不用担心自己对数据的操作会影响其他测试。
结语
Dependency Injection 还有很多可以讲的。这里只是给出一个简单的 Flask 例子而已。本文并没有对 Flask 或者 Python 的细节做出具体解释。如果你有任何疑惑,欢迎留言!
也欢迎大家关注我的 B 站:https://space.bilibili.com/16696495
代码在https://gist.github.com/CreatCodeBuild/11e80e20be0e9aee85e644aa2ff09cd5
关于我:我是一名在硅谷打工的码农,平时就写写代码,教教课,才能维持生活这样子。
我教课的内容包括 Go、Python、GraphQL、JavaScript、前端。
想学编程的读者可以给公众号发消息。
领取专属 10元无门槛券
私享最新 技术干货