👴 让 DRF Views 支持依赖注入

date
May 24, 2021
slug
make-serializer-as-dependency-injector.html
status
Published
tags
python
django
tech
Dependency injection
summary
老瓶装新酒
type
Post

起因

Django 和 Django REST framework 是 Python 开发者常用的框架组合,通常来说,一个典型的 DRF 式 API 可能长这个样子:
from rest_framework.generics import ListAPIView


class ProfileViewSet(ListAPIView):
    def login(self, request):
        serializer = LoginSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        validated_data = serializer.validated_data

        ...
        return Response(data=ProfileSerializer(results, many=True).data)
 
这样写在逻辑上是简单的,可以让开发者对用户请求处理有一个清晰的脉络,但同时也会带来问题:Serializer 的逻辑和主逻辑混杂,使单元测试构造困难。
同时,输入输出的代码在多个 API 中是有一定程度重复的, D.R.Y 重度患者无法接受。
 

启发

新贵框架 FastAPI依赖注入特性 就能够很好的解决以上两点:
from fastapi import Depends, FastAPI

app = FastAPI()

@app.get("/items/")
async def read_items(commons: dict = Depends(common_parameters)):
    return commons
 
然而,现实中的工程中切换框架往往是成本高昂的,仅仅为了依赖注入而切换框架显得有些小题大做。所以,如果能在 Django & DRF 中实现类似依赖注入的功能,会较大程度提高 views 的可读性并降低 TDD 的门槛,间接提高代码质量。
 
同时我们需要满足几个条件:
  • 能够兼容当前的 ViewSet 类
  • 能够复用 Serializer
  • (可选)能够复用 drf-yasg
 
综上,我写了一个 简单的文件 ,你可以将它 Copy 到你的 DRF 项目中就可以改造原来的 ViewSet
(当前需求是比较简单的,封装成 SDK 然后安装依赖的成本反而高于直接复制粘贴,这样大家可以一起偷懒
 

最后的效果:

 
原来的 ViewSet (包含 drf-yasg 的 schema 生成)
class ProfileViewSet(ListAPIView):
    @swagger_auto_schema(
        request_body=LoginSerializer,
        responses={status.HTTP_200_OK: ProfileSerializer()},
    )
    def login(self, request):
        serializer = LoginSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        validated_data = serializer.validated_data

        ...
        return Response(data=ProfileSerializer(results, many=True).data)
 
改造之后的效果:
from some_path.inject import serializer_inject


class ProfileViewSet(ListAPIView):
    @serializer_inject(
        in_cls=LoginSerializer,
        out_cls=ProfileSerializer,
        # 选择去掉原有的 request 依赖
        config={"remain_request": False},
        out_params={"many": True},
    )
    def login(self, validated_data: dict):
        # 原来的逻辑部分
        ...
        return results
(可以通过 gist 评论 获取更多的例子)
 
这样的改造我们得到了一些好处:
  • 仅需要简单改造原来的 ViewSet
  • 完全继承原来的 Serializer
  • 完整支持 drf-yasg
  • 在原来主干逻辑没有依赖 request 对象的情况下,单元测试的用例构造被简化成了 dict
 
当然仍旧还有不完美的地方:
  • 没有使用 Type Annotation ,在声明上较 FastAPI 更为冗余
  • 对于返回值使用了 contextSerializer 需要通过 inject.ResponseParams 类来包装一次,显得不那么纯粹,暂时也没有更好的思路,有空再慢慢改(咕咕🐦)。
 

© bluesyu 2019 - 2023

powered by nobelium