1. 程式人生 > >DRF類檢視讓你的程式碼DRY起來

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

剛開始寫`views.py`模組的程式碼,一般都是用`def`定義的函式檢視,不過DRF更推薦使用`class`定義的類檢視,這能讓我們的程式碼更符合DRY(Don't Repeat Yourself)設計原則:
# 使用APIView `rest_framework.views.APIView`是DRF封裝的API檢視,繼承了`django.views.generic.base.View`:
我們用它把函式檢視改寫成類檢視,編輯`snippets/views.py`: ```python 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) ``` 類檢視的程式碼跟函式檢視是非常類似的,區別在於`GET`、`POST`等方法是用的函式而不是`if`語句,可以更好的解耦程式碼。 改了`views.py`程式碼後,需要同時修改`snippets/urls.py`: ```python 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//', views.SnippetDetail.as_view()), ] urlpatterns = format_suffix_patterns(urlpatterns) ``` **為什麼要加個`as_view()`方法?** 因為`path()`的引數必須是可呼叫的,在原始碼中能看到`elif callable(view)`: ```python 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()`方法返回了一個內部定義的可呼叫函式: ```python @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`模組,封裝了類檢視常用的增刪改查方法:
比如新增`CreateModelMixin`: ```python 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`程式碼: ```python 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`:
和`generics.GenericAPIView`:
這是DRF提供的通用API類檢視,`mixins`只提供了處理方法,`views.py`中的類要成為檢視,還需要繼承`GenericAPIView`,`GenericAPIView`繼承了本文第一小節提到的`rest_framework.views.APIView`。除了`GenericAPIView`,我們還可以用其他的類檢視進一步簡化程式碼: ```python 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`的原始碼: ```python 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/