掃盲
首先帶大家瞭解一下什麼是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 等可獲取相關技術文章和資料,同時有任何問題都可以在公眾號後臺留言~