简介:
本文是Django restframework官方文档Quickstart Tutorial的学习笔记之一, 对views.py的编码风格进行一些总结与讨论.
总结了django rest开发的几种编码风格, 从最基本的function-based-view到class-based-view再到最终的ViewSet,循序渐进,抽象程度越来越高. 官方文档中并没有对此进行单独的介绍,因此本文结合rest_framework源码,对几个概念进行阐述.
views.py的编码风格主要有6种, 抽象程度依次提高, 大方向是精简views层的代码,完成MVC到MVVM风格的转换:
- 基础的Function-Based-View
- @api_view使用装饰器
- class-base-view, 继承APIView
- 继承mixins
- generic class-based views
- 使用ViewSet
1. 基础的function-base-view
(本文以一个获取一个定义在models.py当中的model的所有对象的功能(List)为例子)
代码风格:
#views.py
def some_obj(request):
if request.method == 'GET':
...
return JsonResponse(data)
if request.method == 'POST':
...
return JsonResponse(data)
#urls.py
from views import some_obj
urlpatterns = [
url(r'^/someobj$', some_obj),
]
总结:
这是最基础的view写法,一个url对应一个函数.熟悉django和flask的同学应该都不陌生,毕竟初学web开发的helloworld就是这种形式.
其中的哲学总结为面向过程编程,一个函数表示了一个动作.使用上述的function based views当然可以完成开发, 但似乎并没有使用到什么restframework的特性,依旧是以django MVC的风格开发RESTful api. 随着views的发展, 本文当中渐渐描述从MVC到MVVM的变化, 也就是C到VM的改变.
2. @api_view使用装饰器
代码风格:
@api_view(['GET', 'POST'])
def some_Obj(request):
if request.method == 'GET':
...
return Response(data)
elif request.method == 'POST':
...
return Response(data)
最明显的区别是不需要再指定django的Response为JSONResponse,那发生这种改变的原理是什么,我们可以从源码中看看api_view装饰器做了什么.
# restframework/decorators.py
WrappedAPIView.renderer_classes = getattr(func, 'renderer_classes', APIView.renderer_classes)
WrappedAPIView.parser_classes = getattr(func, 'parser_classes', APIView.parser_classes)
WrappedAPIView.authentication_classes = getattr(func, 'authentication_classes', APIView.authentication_classes)
WrappedAPIView.throttle_classes = getattr(func, 'throttle_classes', APIView.throttle_classes)
WrappedAPIView.permission_classes = getattr(func, 'permission_classes', APIView.permission_classes)
WrappedAPIView.schema = getattr(func, 'schema', APIView.schema)
不用怀疑,restframework是我读过最简单的源码,api_view确实主要只做了这些微小的工作.一旦使用了api_view装饰器,意味着当前的view是一个RESTful API的view,它便为我们做了一些配置的工作, 如指定Renderer,Parser以及authentication等.也因为Render已经被指定为JSONRenderer,因此不需要再使用JsonResponse.(关于Render, Parser的概念在官方文档中有,待以后补充学习笔记).
至此,因为是function-based-view的面向过程的思想.
3. class-based-view, 继承APIView
结合面向对象编程的思想,class-base-view风格开始有一些不同的封装.
代码风格:
#views.py
class ObjList(APIView):
def get(self, request, format=None):
...
return Response(serializer.data)
def post(self, request, format=None):
...
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
#urls.py
urlpatterns = [
url(r'^Obj/$', views.ObjList.as_view()),
]
特点:
1) 就是直接使用class内部的函数名称指定对不同类型HTTP请求的处理.显然比上面的判断request.method == ‘GET’简洁美观
2) 同样有api_view装饰器的特点,渲染器renderers等不需要指定就可完成JSON格式的response返回.
源码解析:
# restframework/view.py
class APIView(View):
# The following policies may be set at either globally, or per-view.
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
parser_classes = api_settings.DEFAULT_PARSER_CLASSES
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES
content_negotiation_class = api_settings.DEFAULT_CONTENT_NEGOTIATION_CLASS
metadata_class = api_settings.DEFAULT_METADATA_CLASS
versioning_class = api_settings.DEFAULT_VERSIONING_CLASS
...
def dispatch(self, request, *args, **kwargs):
"""
`.dispatch()` is pretty much the same as Django's regular dispatch,
but with extra hooks for startup, finalize, and exception handling.
"""
...
try:
self.initial(request, *args, **kwargs)
# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response = handler(request, *args, **kwargs)
except Exception as exc:
response = self.handle_exception(exc)
self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response
这是和特点相关的两部分代码:
1) 上半部分和api_view基本一样,为该class指定各种RESTful API的配置组件;
2) dispatch函数完成了request.method和内部函数名的匹配,以此将request分配到不同的处理函数当中.
4. 继承mixins
继承APIView已经简洁了不少,但是随着开发的进行,人们依然发现了许多get,post当中所做的事情是一样的.比如创建一个对象无非是调用ORM的create并传入数据,又比如获取一个对象,无非是ORM获取,然后拼接JSON返回响应.
软件开发领域中最经典的口头禅就是“don’t repeat yourself”。 也就是说,任何时候当你的程序中存在高度重复(或者是通过剪切复制)的代码时,都应该想想是否有更好的解决方案。——python3-cookbook
因此使用mixins编写view,就此诞生.
代码风格:
class ObjList(mixins.ListModelMixin,
mixins.CreateModelMixin,
generics.GenericAPIView):
queryset = Obj.objects.all()
serializer_class = ObjSerializer
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
mixins并不是Django特有的概念,它是JAVA当中面向接口编程的思想在python中的体现.python3-cookbook当中做了详细的介绍,具体的用法可以参考: 利用Mixins扩展类功能
总的来说,某个mixin当中都实现了一些开箱即用的方法,使得我们可以只继承mixin,而无需继承整个父类(比如某个model, 或者说对应的某个数据库表, 只查不改或只增不删),这样多继承多个mixins也不会导致父子关系的混乱.
我们查看一下mixins.ListModelMixin的源码即可了解:
# restframework/mixins.py
class ListModelMixin(object):
"""
List a queryset.
"""
def list(self, request, *args, **kwargs):
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
可以看到list方法已经帮我们写好了,我们的view只需要完成get请求与list方法的绑定即可.
mixins并不复杂,总共只有5种:
* CreateModelMixin: 创建
* ListModelMixin: 列出多个对象
* RetrieveModelMixin: 获取单个对象
* UpdateModelMixin: 更改某个对象
* DestroyModelMixin: 删除某个对象
换个角度来说,源码这种写法也给我们启发:
我们平时对model的操作概括为”增删改查”,其实在开发中是不够的,因为对于同一个model可以查单个,也可以是查多个,所以应该是”增删改查列”更能满足功能的需求.
queryset和get_object()
queryset和get_object()是Django Restframework中实现class-based views的重要概念.
queryset:理解为该class当中所需查询对象的最大集合,class中的其他查询都在基础上进行过滤即可实现.如一个微博应用当中, “时间流”上的微博列表仅返回用户所关注的微博,每个用户看到的都不一样, 但在”热门微博”上的微博列表是所有人可见的, 每个用户看到的都一样.如果用restframework来开发的话,就是在queryset的定义上实现该功能.
它有两个主要功能:
1) 为mixins的方法(list/retrieve等)指定操作对象;
2) django内部使用django.cache完成缓存功能, 避免重复查询. 对于django开发者来说, 这点相比JAVA spring等其他框架要方便得多, 不需要在意我们的应用和数据库之间的一层缓存层的问题(如缓存大小, 缓存置换规则, 缓存有效时间等). 但同时也让django开发者离web应用底层又远了一步, 想成为一个后端开发者也应该要对缓存功能有一定了解.
tips: 重写get_queryset()方法也可实现同样效果.
常见的queryset定义有:
def get_queryset(self, request):
return Obj.objects.filter(user=request.user)
queryset仅返回属于当前用户自己的数据.
get_object(): 就是一个更具体的指定操作对象的函数,针对retrieve等对于单个object的操作指定.
现在,我们的view的工作只剩下绑定请求类型(get/post)和处理方法(list/create等), 显然,这样在多个工程当中,重复代码也是很多了, 这一部分也可以由generic class-based views框架完成.
5. generic class-based views
代码风格:
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
ListCreateAPIView源码:
class ListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin,
GenericAPIView):
"""
Concrete view for listing a queryset or creating a model instance.
"""
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
通用类视图generic class-based views非常非常简单,无非就是把我们刚才在使用mixin的例子当中我们手动完成的事情提前写好了罢了. 继承之后,已经可以不需要写一行逻辑代码了, 只需要告诉restframework当前视图对应的Serializer即可.
它帮我们完成的只有两点:
1) 继承所需的mixin,如ListCreateAPIView,则已经继承好了mixins.ListModelMixin,mixins.CreateModelMixin.
2) 绑定get/post到对应的mixin当中提供的方法(list()/create())
提供了多个同个通用视图:
* CreateAPIView
* ListAPIView
* RetrieveAPIView
* DestroyAPIView
* UpdateAPIView
* ListCreateAPIView
* RetrieveUpdateAPIView
* RetrieveDestroyAPIView
* RetrieveUpdateDestroyAPIView
通用视图的原理并不复杂,不再赘述.
但是从框架给我们提供的这几个通用视图,也可以大概看出restframework的开发者认为RESTful开发view的逻辑大体上不会超出这几种.
当然我们没有必要迷信权威,开发过程当中直接去自定义继承一些所需的mixins往往有更高的效率,而不是去通用视图里面找有没有匹配的.
6. ViewSet
如果你到了这里,那么ViewSet已经不是什么复杂的概念,ViewSet可以理解为一系列针对同一个model的views的集合.在这一点上并没有对通用类视图有太大的改进,只是通过绑定ViewSet和Model的方式来实现真正的MVVM的开发.
代码风格:
# views.py
class ObjViewSet(viewsets.ModelViewSet):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions.
Additionally we also provide an extra `highlight` action.
"""
queryset = Obj.objects.all()
serializer_class = ObjSerializer
permission_classes = (permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,)
# urls.py
router = DefaultRouter()
router.register(r'Obj', views.ObjViewSet)
urlpatterns = [
url(r'^', include(router.urls))
]
若使用ViewSet,鼓励使用router完成更简洁的url绑定.
主要用到的ViewSet只有两个:
* ReadOnlyModelViewSet
* ModelViewSet
class ReadOnlyModelViewSet(mixins.RetrieveModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `list()` and `retrieve()` actions.
"""
pass
class ModelViewSet(mixins.CreateModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
"""
A viewset that provides default `create()`, `retrieve()`, `update()`,
`partial_update()`, `destroy()` and `list()` actions.
"""
pass
可以看到只完成了继承关系,并没有额外实现了的功能, 但是对于ReadOnlyModel概念和Model概念的总结还是值得我们对views的设计进一步思考.
实际工程使用:
看了这么多中views风格,你肯定会好奇哪种才更好.
实际工程中views逻辑普遍存在两个问题:
1) 对model的操作不可能只是简单的增删改查,比如增改要验证数据的合法性,比如删要对有关联的数据也一并删除.
2) 不是所有请求都能用增删改查来总结,比如用户请求你完成1+1=?的计算.
因此,使用viewset一劳永逸是很少见的情况.
针对问题1) ,我们可以采用mixins引入所需”增删改查列”的函数,再覆盖重写.
对于问题2) ,则回到我们最初的function-base-view,在viewset当中使用action装饰器功能, 为对象增加”增删改查”以外的操作函数.
因此,mixins风格 + action装饰器是我使用的views编码方式.
总结:
说了这么多,其实初级开发者注重框架风格,中级开发者注重业务逻辑,高级开发者应该注重数据结构和数据库设计.
本文只属于对”初级”的概念进行了介绍和总结,其本质只是以不同的方法去实现增删改查,不必拘泥于形式.
虽然把function-base-view的views.py重构成APIVIEW或者ViewSet风格能让你的代码更加简洁,但是不代表你的开发效率就能因此提高很多,甚至运行效率也不会更快.
记一篇纯粹以好奇心驱使写下的博文.