从Function-Based-View到ViewSet——Django Restframework关于views的总结与源码分析

独自空忆成欢 提交于 2019-12-06 00:39:30

简介:

本文是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风格能让你的代码更加简洁,但是不代表你的开发效率就能因此提高很多,甚至运行效率也不会更快.
记一篇纯粹以好奇心驱使写下的博文.

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!