依赖注入听起来好像很复杂,但是实际上超级简单,一句话说就是:
本来我接受各种参数来构造一个对象,现在只接受一个参数——已经实例化的对象。
也就是说我对对象的『依赖是注入进来的』,而和它的构造方式解耦了。构造和销毁这些『控制』操作也交给了第三方,也就是控制『反转』。
不举抽象的例子了。一个很实际的例子,比如我们要用 redis 实现一个远程列表。耦合成一坨的代码可以是这样写,其中我们需要自己构造需要用的组件:
class RedisList: def __init__(self, host, port, password): self._client = redis.Redis(host, port, password) def push(self, key, val): self._client.lpush(key, val) l = RedisList(host, port, password)
依赖翻转之后是这样的:
class RedisList: def __init__(self, redis_client) self._client = redis_client def push(self, key, val): self._client.lpush(key, val) redis_client = get_redis_client(...) l = RedisList(redis_client)
看起来好像也没什么区别,但是考虑下面这些因素:
- 线下线上环境可能不一样,
get_redis_client
函数在线上可能要做不少操作来读取到对应的配置,可能并不是不是一个简单的函数。在测试环境可能会返回一个 Mock 的 FakeRedis。 - redis 这个类是一个基础组件,可能好多类都需要用到,每个类都去自己实例化吗?如果需要修改的话,每个类都要改。
- 我们想依赖的是 redis 的 lpush 方法,而不是他的构造函数。
所以把 redis 这个类的实例化由一个单一的函数来做,而其他函数只调用对应的接口是有意义的。
Web 框架中的依赖注入
上面提到的是依赖注入的原始定义,在实际开发过程中,Web 框架领域最喜欢提依赖注入这个 buzz word。由于本人太笨了,一直没学会 Java 和 Spring Framework,这里以 Python 的 FastAPI 为例。我们将会看到,Web 框架领域的依赖注入依然没有脱离它的原始定义。
假设我们有如下三个 API,它们都返回一个列表且支持分页,所以都需要 offset 和 limit 两个参数。
/api/users?offset=100&limit=10 /api/posts?offset=100&limit=10 /api/comments?offset=100&limit=10
我们可以这样实现,其中 handler 函数的参数就是 URL 中的参数:
@app.get("/api/users") def list_users(offset: int, limit: int): return UserModel.filter(offset=offset, limit=limit) @app.get("/api/posts") def list_posts(offset: int, limit: int): return PostModel.filter(offset=offset, limit=limit) @app.get("/api/posts") def list_comments(offset: int, limit: int): return CommentModel.filter(offset=offset, limit=limit)
虽然参数不多,但是这里已经可以嗅到一丝代码重复的味道了。不过更重要的是,假如我们要改一下参数呢?比如说从 limit/offset 改成 page/size,那么所有函数的参数都需要改,难免会有漏掉的。这时候就可以请出我们的老朋友依赖注入了。
# fastapi 中提供了 Depends 用来表示依赖 from fastapi import Depends def get_page_info(offset: int, limit: int): return {"offset": limit, "limit": limit} # list_users 依赖了 get_page_info 函数,而不再负责具体的 offset/limit 参数 @app.get("/api/users") def list_users(page_info: dict = Depends(get_page_info)): return UserModel.filter(**page_info) # posts, comments 等类似
和开篇的一句话类似:list_users 本来接受具体的参数来获取翻页信息,而现在只接受一个已经实例化过后的 page_info 对象了。也就是说 page_info 这个依赖被框架注入到了具体的业务代码中。
假如我们需要把参数变成 page/size,只需要更改依赖就好了,所有依赖它的函数都无需做任何改动。
def get_page_info(page: int, size: int): # page 从 1 开始,offset 从 0 开始 return {"offset": page * limit - limit: ,"limit": size}
再来一个例子,如果我们每个 handler 函数都依赖一个数据库链接:
def get_db(): db = connect(...) try: yield db finally: db.close() @app.get("/api/users") def list_users(db=Depends(get_db)): # use the db ...
这个例子就和最上面的 get_redis_client 几乎一样了,不再赘述。
总而言之,依赖注入在代码上很简单,就是把一坨参数换成了一个实例参数。
设计模式不是发明出来的,而是总结出来的,可能不经意间你早就在用依赖注入了。没必要一写代码就想着我要用这个那个设计模式,只会缚住自己的手脚,当你发现一个项目里有三处雷同的代码,再用合理的设计模式解决这个问题也不迟。