如何用最简单的方式解释依赖注入?

2023-03-06 14:40:28 浏览数 (2)

依赖注入听起来好像很复杂,但是实际上超级简单,一句话说就是:

本来我接受各种参数来构造一个对象,现在只接受一个参数——已经实例化的对象。

也就是说我对对象的『依赖是注入进来的』,而和它的构造方式解耦了。构造和销毁这些『控制』操作也交给了第三方,也就是控制『反转』。

不举抽象的例子了。一个很实际的例子,比如我们要用 redis 实现一个远程列表。耦合成一坨的代码可以是这样写,其中我们需要自己构造需要用的组件:

代码语言:javascript复制
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)

依赖翻转之后是这样的:

代码语言:javascript复制
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)

看起来好像也没什么区别,但是考虑下面这些因素:

  1. 线下线上环境可能不一样,get_redis_client 函数在线上可能要做不少操作来读取到对应的配置,可能并不是不是一个简单的函数。在测试环境可能会返回一个 Mock 的 FakeRedis。
  2. redis 这个类是一个基础组件,可能好多类都需要用到,每个类都去自己实例化吗?如果需要修改的话,每个类都要改。
  3. 我们想依赖的是 redis 的 lpush 方法,而不是他的构造函数。

所以把 redis 这个类的实例化由一个单一的函数来做,而其他函数只调用对应的接口是有意义的。

Web 框架中的依赖注入

上面提到的是依赖注入的原始定义,在实际开发过程中,Web 框架领域最喜欢提依赖注入这个 buzz word。由于本人太笨了,一直没学会 Java 和 Spring Framework,这里以 Python 的 FastAPI 为例。我们将会看到,Web 框架领域的依赖注入依然没有脱离它的原始定义。

假设我们有如下三个 API,它们都返回一个列表且支持分页,所以都需要 offset 和 limit 两个参数。

代码语言:javascript复制
/api/users?offset=100&limit=10
/api/posts?offset=100&limit=10
/api/comments?offset=100&limit=10

我们可以这样实现,其中 handler 函数的参数就是 URL 中的参数:

代码语言:javascript复制
@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,那么所有函数的参数都需要改,难免会有漏掉的。这时候就可以请出我们的老朋友依赖注入了。

代码语言:javascript复制
# 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,只需要更改依赖就好了,所有依赖它的函数都无需做任何改动。

代码语言:javascript复制
def get_page_info(page: int, size: int):
    # page 从 1 开始,offset 从 0 开始
    return {"offset": page * limit - limit: ,"limit": size}

再来一个例子,如果我们每个 handler 函数都依赖一个数据库链接:

代码语言:javascript复制
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 几乎一样了,不再赘述。

总而言之,依赖注入在代码上很简单,就是把一坨参数换成了一个实例参数。

设计模式不是发明出来的,而是总结出来的,可能不经意间你早就在用依赖注入了。没必要一写代码就想着我要用这个那个设计模式,只会缚住自己的手脚,当你发现一个项目里有三处雷同的代码,再用合理的设计模式解决这个问题也不迟。

0 人点赞