起因
Django 和 Django REST framework 是 Python 开发者常用的框架组合,通常来说,一个典型的 DRF 式 API 可能长这个样子:
代码语言:javascript复制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 的 依赖注入特性 就能够很好的解决以上两点:
代码语言:javascript复制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)
改造之后的效果:
代码语言:javascript复制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
更为冗余 - 对于返回值使用了
context
的Serializer
需要通过inject.ResponseParams
类来包装一次,显得不那么纯粹,暂时也没有更好的思路,有空再慢慢改(咕咕