Python後端日常操作之在Django中「強行」使用MVVM設計模式
- 2020 年 7 月 25 日
- 筆記
- [Python]Django
掃盲
首先帶大家了解一下什麼是MVVM模式:
什麼是MVVM?MVVM是Model-View-ViewModel的縮寫。
MVVM是MVC的增強版,實質上和MVC沒有本質區別,只是代碼的位置變動而已
從名字上看,MVVM比MVC架構中多了一個ViewModel,沒錯,就是這個ViewModel,他是MVVM相對於MVC改進的核心思想。在開發過程中,由於需求的變更或添加,項目的複雜度越來越高,代碼量越來越大,此時我們會發現MVC維護起來有些吃力,首先被人吐槽的最多的就是MVC的簡寫變成了Massive-View-Controller(意為沉重的Controller)由於Controller主要用來處理各種邏輯和數據轉化,複雜業務邏輯界面的Controller非常龐大,維護困難,所以有人想到把Controller的數據和邏輯處理部分從中抽離出來,用一個專門的對象去管理,這個對象就是ViewModel,是Model和Controller之間的一座橋樑。當人們去嘗試這種方式時,發現Controller中的代碼變得非常少,變得易於測試和維護,只需要Controller和ViewModel做數據綁定即可,這也就催生了MVVM的熱潮。
引言
大家都知道Django是MVT模式,Model就是View和Template/Interface之間的數據傳遞的「信使」,這種模式存在一個問題,就是當我們的業務不斷擴大之後需要在接口返回出model里不包含的數據時該怎麼辦?例如一個商店,我們要動態計算它距離我們當前位置有多遠,那麼這個距離肯定是不包含在Model裏面的,數據庫也不可能實時存儲這類數據。
那麼這時候我們就需要在Model上,再加上一層ViewModel,顧名思義,視圖模型,是用來在視圖裡傳遞和處理數據的模型。
簡單實現
在App包下面創建一個view_models
文件,內容如下:
from rest_framework.request import Request
from core.models import Store
from core.serializers import StoreSerializer
class StoreViewModel:
def __init__(self, store: Store, distance=0.0, request: Request = None):
self.store = store
self.distance = distance
self.request = request
@property
def serialize_data(self):
return StoreSerializer(self.store, context={
'distance': self.distance,
'request': self.request,
}).data
上面的代碼定義了一個商店的視圖模型,構造方法中除了我們的Model對象,還有Model中不包括的distance參數,還有一個request用來傳遞請求的context,這個在Drf中是很重要的,如果不處理好context的傳遞,會導致Drf在序列化一些文件或者鏈接類字段的時候丟失前半部分的域名。
接下來看看serialize_data
這個屬性,它做的工作很簡單,就是把Model對象傳給序列化器,然後在context中存入我們的額外參數distance和request。
再來看看序列化器要如何改造以適應ViewModel模型。
class StoreSerializer(serializers.ModelSerializer):
distance = serializers.SerializerMethodField()
class Meta:
model = models.Store
fields = '__all__'
def get_distance(self, obj: models.Store):
return self.context.get('distance', 0)
這裡可以看到序列化器中,我是把額外的distance字段處理成SerializerMethodField
,然後在get_distance
方法中實現,通過self.context
屬性可以獲取到我們在ViewModel中傳入的context,這樣就實現額外參數的序列化。
最後我們在看看在View,也就是控制器,看看如何將ViewModel和原本的分頁,權限各類功能結合在一起。
class StoreViewSet(viewsets.ReadOnlyModelViewSet):
"""商家相關功能"""
serializer_class = serializers.StoreSerializer
queryset = models.Store.objects.all()
@action(detail=False)
def location(self, request):
"""根據地理位置篩選商家"""
city = request.GET.get('city')
town = request.GET.get('town')
lat = request.GET.get('lat')
lng = request.GET.get('lng')
# 根據城市、區鎮篩選商店
queryset = models.Store.objects.filter(city=city, town=town)
# 調用接口計算所有商店距離當前位置的距離,該接口返回ViewModel
store_view_models = tencent_map.stores_distance(from_lat=lat, from_lng=lng, queryset=queryset, request=request)
# 對ViewModelSet進行排序,按照距離
store_view_models.sort(key=lambda store_view_model: store_view_model.distance)
# 使用列表生成器,對每個ViewModel進行序列化
stores_data = [store_vm.serialize_data for store_vm in store_view_models]
# 對結果數據進行分頁
page = self.paginate_queryset(stores_data)
return self.get_paginated_response(page)
上面的代碼目前在開發環境運行良好,我已經寫了詳細的注釋了,可以看到用ViewModel模式是可以和原本的ViewSet很好的結合在一起的,包括分頁這些功能都可以正常使用。
小結
標題中我用了「強行」這個詞,就是覺得我這樣實現好像很不優雅,但又不至於hack,因為這個需求很簡單,只要實現了就行,我也還沒有去搜索其他的解決方案,在本文中提出了我的ViewModel與Django結合解決方案,如果大家有更好的解決方案可以留言一起探討~
歡迎交流
我整理了一系列的技術文章和資料,在公眾號「程序設計實驗室」後台回復 linux、flutter、c#、netcore、android、java、python 等可獲取相關技術文章和資料,同時有任何問題都可以在公眾號後台留言~