DRF類檢視讓你的程式碼DRY起來

dongfanger發表於2020-12-19

剛開始寫views.py模組的程式碼,一般都是用def定義的函式檢視,不過DRF更推薦使用class定義的類檢視,這能讓我們的程式碼更符合DRY(Don't Repeat Yourself)設計原則:

DRF類檢視讓你的程式碼DRY起來

使用APIView

rest_framework.views.APIView是DRF封裝的API檢視,繼承了django.views.generic.base.View

DRF類檢視讓你的程式碼DRY起來

我們用它把函式檢視改寫成類檢視,編輯snippets/views.py

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from django.http import Http404
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status


class SnippetList(APIView):
    """
    List all snippets, or create a new snippet.
    """
    def get(self, request, format=None):
        snippets = Snippet.objects.all()
        serializer = SnippetSerializer(snippets, many=True)
        return Response(serializer.data)

    def post(self, request, format=None):
        serializer = SnippetSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
    
    
class SnippetDetail(APIView):
    """
    Retrieve, update or delete a snippet instance.
    """
    def get_object(self, pk):
        try:
            return Snippet.objects.get(pk=pk)
        except Snippet.DoesNotExist:
            raise Http404

    def get(self, request, pk, format=None):
        snippet = self.get_object(pk)
        serializer = SnippetSerializer(snippet)
        return Response(serializer.data)

    def put(self, request, pk, format=None):
        snippet = self.get_object(pk)
        serializer = SnippetSerializer(snippet, data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data)
        return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

    def delete(self, request, pk, format=None):
        snippet = self.get_object(pk)
        snippet.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)

類檢視的程式碼跟函式檢視是非常類似的,區別在於GETPOST等方法是用的函式而不是if語句,可以更好的解耦程式碼。

改了views.py程式碼後,需要同時修改snippets/urls.py

from django.urls import path
from rest_framework.urlpatterns import format_suffix_patterns
from snippets import views

urlpatterns = [
    path('snippets/', views.SnippetList.as_view()),
    path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
]

urlpatterns = format_suffix_patterns(urlpatterns)

為什麼要加個as_view()方法?

因為path()的引數必須是可呼叫的,在原始碼中能看到elif callable(view)

def _path(route, view, kwargs=None, name=None, Pattern=None):
    if isinstance(view, (list, tuple)):
        # For include(...) processing.
        pattern = Pattern(route, is_endpoint=False)
        urlconf_module, app_name, namespace = view
        return URLResolver(
            pattern,
            urlconf_module,
            kwargs,
            app_name=app_name,
            namespace=namespace,
        )
    # callable判斷
    elif callable(view):
        pattern = Pattern(route, name=name, is_endpoint=True)
        return URLPattern(pattern, view, kwargs, name)
    else:
        raise TypeError('view must be a callable or a list/tuple in the case of include().')

as_view()方法返回了一個內部定義的可呼叫函式:

@classonlymethod
def as_view(cls, **initkwargs):
    """Main entry point for a request-response process."""
    for key in initkwargs:
        if key in cls.http_method_names:
            raise TypeError(
                'The method name %s is not accepted as a keyword argument '
                'to %s().' % (key, cls.__name__)
            )
        if not hasattr(cls, key):
            raise TypeError("%s() received an invalid keyword %r. as_view "
                            "only accepts arguments that are already "
                            "attributes of the class." % (cls.__name__, key))

    # 內部定義了可呼叫函式
    def view(request, *args, **kwargs):
        self = cls(**initkwargs)
        self.setup(request, *args, **kwargs)
        if not hasattr(self, 'request'):
            raise AttributeError(
                "%s instance has no 'request' attribute. Did you override "
                "setup() and forget to call super()?" % cls.__name__
            )
        return self.dispatch(request, *args, **kwargs)
    view.view_class = cls
    view.view_initkwargs = initkwargs

    # take name and docstring from class
    update_wrapper(view, cls, updated=())

    # and possible attributes set by decorators
    # like csrf_exempt from dispatch
    update_wrapper(view, cls.dispatch, assigned=())
    return view

使用mixins

DRF提供了rest_framework.mixins模組,封裝了類檢視常用的增刪改查方法:

DRF類檢視讓你的程式碼DRY起來

比如新增CreateModelMixin

class CreateModelMixin:
    """
    Create a model instance.
    """
    def create(self, request, *args, **kwargs):
        serializer = self.get_serializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        self.perform_create(serializer)
        headers = self.get_success_headers(serializer.data)
        return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)

    def perform_create(self, serializer):
        serializer.save()

    def get_success_headers(self, data):
        try:
            return {'Location': str(data[api_settings.URL_FIELD_NAME])}
        except (TypeError, KeyError):
            return {}

類檢視繼承了Mixin後,可以直接使用它的.create()方法,類似的還有.list().retrieve().update().destroy()。我們按照這個思路來簡化snippets/views.py程式碼:

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import mixins
from rest_framework import generics

class SnippetList(mixins.ListModelMixin,
                  mixins.CreateModelMixin,
                  generics.GenericAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

    def get(self, request, *args, **kwargs):
        return self.list(request, *args, **kwargs)

    def post(self, request, *args, **kwargs):
        return self.create(request, *args, **kwargs)


class SnippetDetail(mixins.RetrieveModelMixin,
                    mixins.UpdateModelMixin,
                    mixins.DestroyModelMixin,
                    generics.GenericAPIView):
    queryset = Snippet.objects.all()
    serializer_class = SnippetSerializer

    def get(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

    def put(self, request, *args, **kwargs):
        return self.update(request, *args, **kwargs)

    def delete(self, request, *args, **kwargs):
        return self.destroy(request, *args, **kwargs)

瞬間少了好多程式碼,真夠DRY的。

什麼是mixin?

維基百科的解釋:

In object-oriented programming languages, a mixin (or mix-in) is a class that contains methods for use by other classes without having to be the parent class of those other classes.

不太好理解。

換句話說,mixin類提供了一些方法,我們不會直接用這些方法,而是把它新增到其他類來使用。

還是有點抽象。

再簡單點說,mixin只不過是實現多重繼承的一個技巧而已。

這下應該清楚了。

使用generics

如果仔細看snippets/views.py的程式碼,就會發現我們用到了from rest_framework import generics

DRF類檢視讓你的程式碼DRY起來

generics.GenericAPIView

DRF類檢視讓你的程式碼DRY起來

這是DRF提供的通用API類檢視,mixins只提供了處理方法,views.py中的類要成為檢視,還需要繼承GenericAPIViewGenericAPIView繼承了本文第一小節提到的rest_framework.views.APIView。除了GenericAPIView,我們還可以用其他的類檢視進一步簡化程式碼:

from snippets.models import Snippet
from snippets.serializers import SnippetSerializer
from rest_framework import generics


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)

真DRY!

東方說

學到這裡,已經開始感受到了Django REST framework的強大之處了,我覺得學一個框架,不僅要看如何使用,還需要了解它的設計思路和底層實現,這樣才能更好的總結為自己的程式設計思想,寫出更漂亮的程式碼。

參考資料:

https://www.django-rest-framework.org/tutorial/3-class-based-views/#tutorial-3-class-based-views

https://stackoverflow.com/questions/533631/what-is-a-mixin-and-why-are-they-useful

https://www.zhihu.com/question/20778853

相關文章