Python後端日常操作之在Django中「強行」使用MVVM設計模式

畫星星高手發表於2020-07-25

掃盲

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

相關文章