DRF類檢視讓你的程式碼DRY起來
阿新 • • 發佈:2020-12-19
剛開始寫`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/