Django工程的分層結構

昀溪發表於2019-07-18

前言

傳統上我們都知道在Django中的MTV模式,具體內容含義我們再來回顧一下:

  • M:是Model的簡稱,它的目標就是通過定義模型來處理和資料庫進行互動,有了這一層或者這種型別的物件,我們就可以通過物件來運算元據。

  • V:是View的簡稱,它的工作很少,就是接受使用者請求換句話說就是通過HTTP請求接受使用者的輸入;另外把輸入資訊傳送給處理程並獲取結果;最後把結果傳送給使用者,當然最後這一步還可以使用模板來修飾資料。

  • T:是Template的簡稱,這裡主要是通過標記語言來定義頁面,另外還可以嵌入模板語言讓引擎來渲染動態資料。

這時候我們看到網上大多數的列子包括有些視訊課程裡面只講MVT以及語法和其他功能實現等,但大家有沒有想過一個問題,你的業務邏輯放在哪裡?課程中的邏輯通常放在了View裡面,就像下面:

# urls.py
path('hello/', Hello),
path('helloworld/', HelloWorld.as_view())

# View
from django.views import View

# FVB
def Hello(request):
    if request.method == "GET":
        return HttpResponse("Hello world")

# CVB
class HelloWorld(View):
    def get(self, request):
        pass
    def post(self, request):
        pass

無論是FBV還是CBV,當使用者請求進來並通過URL路由找到對應的方法或者類,然後對請求進行處理,比如可以直接返回模型資料、驗證使用者輸入或者校驗使用者名稱和密碼等。在學習階段或者功能非常簡單的時候使用這種寫法沒問題,但是對於相對大一點的專案來說你很多具體的處理流程開始出現,而這些東西都寫到View裡顯然你自己都看不下去。

FBV全名Function-based views,基於函式的檢視;CBV全名Class-based views,基於類的檢視

所以View,它就是一個控制器,它不應該包含業務邏輯,事實上它應該是一個很薄的層。

業務邏輯到底放哪裡

網上也有很多文章回答了這個問題,提到了Form層,這個其實是用於驗證使用者輸入資料的格式,比如郵件地址是否正確、是否填寫了使用者名稱和密碼,至於這個使用者名稱或者郵箱到底在資料庫中是否真實存在則不是它應該關心的,它只是一個資料格式驗證器。所以業務邏輯到底放哪裡呢?顯然要引入另外一層。

關於這一層的名稱有些人叫做UseCase,也有些人叫做Service,至於什麼名字無所謂只要是大家一看就明白的名稱就好。如果我們使用UseCase這個名字,那麼我們的Djaong工程架構就變成了MUVT,如果是Service那麼就MSVT。

這一層的目標是什麼呢?它專注於具體業務邏輯,也就是不同用例的具體操作,比如使用者註冊、登陸和登出都一個用例。所有模型都只是工作流程的一部分並且這一層也知道模型有哪些API。這麼說有些空洞,我們用一個例子來說明:

場景是使用者註冊:

  1. 資訊填寫規範且使用者不存在則註冊成功併傳送賬戶啟用郵件

  2. 如果使用者已存在則程式引發錯誤,然後傳遞到上層並進行告知使用者名稱已被佔用

Django 2.2.1、Python 3.7

下圖是整個工程的結構

Django工程的分層結構

Models層

models.py

from django.db import models
from django.utils.translation import gettext as _

# Create your models here.

from django.contrib.auth.models import AbstractUser, UserManager, User

class UserAccountManager(UserManager):
    # 管理器
    def find_by_username(self, username):
        queryset = self.get_queryset()
        return queryset.filter(username=username)


class UserAccount(AbstractUser):
    # 擴充套件一個欄位,家庭住址
    home_address = models.CharField(_('home address'), max_length=150, blank=True)
    # 賬戶是否被啟用,與users表裡預設的is_active不是一回事
    is_activated = models.BooleanField(_('activatition'), default=False, help_text=_('新賬戶註冊後是否通過郵件驗證啟用。'),)

    # 指定該模型的manager類
    objects = UserAccountManager()

我們知道Django會為我們自動建立一個叫做auth_user的表,也就是它自己的認證內容,這個user表本身就是一個模型,它就是繼承了AbstractUser類,而這個類有繼承了AbstractBaseUser,而這個類繼承了models.Model,所以我們這裡就是一個模型。再說回AbstractUser類,這個類裡面定義了一些username、first_name、email、is_active等使用者屬性相關的欄位,如果你覺得不夠用還可以自己擴充套件。

為了讓Django使用我們擴充套件的使用者模型,所以需要在settings.py中新增如下內容:

AUTH_USER_MODEL = "users.UserAccount"

工具類

這個檔案主要是放一些通用工具,比如傳送郵件這種公共會呼叫的功能,utils.py內容如下:

from django.core.mail import send_mail
from django.contrib.sites.shortcuts import get_current_site
from django.contrib.auth.tokens import PasswordResetTokenGenerator
from django.utils import six
from django.template.loader import render_to_string
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from mysite import settings


class TokenGenerator(PasswordResetTokenGenerator):
    def __init__(self):
        super(TokenGenerator, self).__init__()

    # def _make_hash_value(self, user, timestamp):
    #     return (
    #         six.text_type(user.pk) + six.text_type(timestamp) + six.text_type(user.is_active)
    #     )


class WelcomeEmail:
    subject = 'Activate Your Account'

    @classmethod
    def send_to(cls, request, user_account):
        try:
            current_site = get_current_site(request)
            account_activation_token = TokenGenerator()
            message = render_to_string('activate_account.html', {
                'username': user_account.username,
                'domain': current_site.domain,
                'uid': urlsafe_base64_encode(force_bytes(user_account.id)),
                'token': account_activation_token.make_token(user_account),
            })

            send_mail(
                subject=cls.subject,
                message=message,
                from_email=settings.EMAIL_HOST_USER,
                recipient_list=[user_account.email]
            )
        except Exception as err:
            print(err)

TokenGenerator這個東西使用還是它父類本身的功能,之所以這樣做是為了在必要的時候可以重寫一些功能。父類PasswordResetTokenGenerator的功能主要是根據使用者主鍵來生成token,之後還會根據傳遞的token和使用者主鍵去檢查傳遞的token是否一致。

針對郵件傳送我這裡使用Django提供的封裝,你需要在settings.py中新增如下內容:

# 郵件設定
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_USE_SSL = True
EMAIL_HOST = 'smtp.163.com'
EMAIL_PORT = 465
EMAIL_HOST_USER = ''   # 發件人郵箱地址
EMAIL_HOST_PASSWORD = ''  # 發件人郵箱密碼
DEFAULT_FROM_EMAIL = EMAIL_HOST_USER

Services層

這層主要是根據用例來實現業務邏輯,比如註冊使用者賬號和啟用使用者賬號。

"""
Service層,針對不同用例實現的業務邏輯程式碼
"""
from django.utils.translation import gettext as _
from django.shortcuts import render
from .utils import (
    WelcomeEmail,
    TokenGenerator,
)
from users.models import (
    UserAccount
)


class UsernameAlreadyExistError(Exception):
    pass


class UserIdIsNotExistError(Exception):
    """
    使用者ID,主鍵不存在
    """
    pass


class ActivatitionTokenError(Exception):
    pass


class RegisterUserAccount:
    def __init__(self, request, username, password, confirm_password, email):
        self._username = username
        self._password = password
        self._email = email
        self._request = request

    def valid_data(self):
        """
        檢查使用者名稱是否已經被註冊
        :return:
        """
        user_query_set = UserAccount.objects.find_by_username(username=self._username).first()
        if user_query_set:
            error_msg = ('使用者名稱 {} 已被註冊,請更換。'.format(self._username))
            raise UsernameAlreadyExistError(_(error_msg))
        return True

    def _send_welcome_email_to(self, user_account):
        """
        註冊成功後傳送電子郵件
        :param user_account:
        :return:
        """
        WelcomeEmail.send_to(self._request, user_account)

    def execute(self):
        self.valid_data()
        user_account = self._factory_user_account()
        self._send_welcome_email_to(user_account)
        return user_account

    def _factory_user_account(self):
        """
        這裡是建立使用者
        :return:
        """

        # 這樣建立需要呼叫save()
        # ua = UserAccount(username=self._username, password=self._password, email=self._email)
        # ua.save()
        # return ua

        # 直接通過create_user則不需要呼叫save()
        return UserAccount.objects.create_user(
            self._username,
            self._email,
            self._password,
        )


class ActivateUserAccount:
    def __init__(self, uid, token):
        self._uid = uid
        self._token = token

    def _account_valid(self):
        """
        驗證使用者是否存在
        :return: 模型物件或者None
        """
        return UserAccount.objects.all().get(id=self._uid)

    def execute(self):
        # 查詢是否有使用者
        user_account = self._account_valid()
        account_activation_token = TokenGenerator()
        if user_account is None:
            error_msg = ('啟用使用者失敗,提供的使用者標識 {} 不正確,無此使用者。'.format(self._uid))
            raise UserIdIsNotExistError(_(error_msg))

        if not account_activation_token.check_token(user_account, self._token):
            error_msg = ('啟用使用者失敗,提供的Token {} 不正確。'.format(self._token))
            raise ActivatitionTokenError(_(error_msg))

        user_account.is_activated = True
        user_account.save()
        return True

這裡定義的異常類比如UsernameAlreadyExistError等裡面的內容就是空的,目的是raise異常到自定義的異常中,這樣呼叫方通過try就可以捕獲,有些時候程式碼執行的結果影響呼叫方後續的處理,通常大家可能認為需要通過返回值來判斷,比如True或者False,但通常這不是一個好辦法或者說在有些時候不是,因為那樣會造成程式碼冗長,比如下面的程式碼:

這是上面程式碼中的一部分,

    def valid_data(self):
        """
        檢查使用者名稱是否已經被註冊
        :return:
        """
        user_query_set = UserAccount.objects.find_by_username(username=self._username).first()
        if user_query_set:
            error_msg = ('使用者名稱 {} 已被註冊,請更換。'.format(self._username))
            raise UsernameAlreadyExistError(_(error_msg))
        return True

    def execute(self):
        self.valid_data()
        user_account = self._factory_user_account()
        self._send_welcome_email_to(user_account)
        return user_account

execute函式會執行valid_data()函式,如果執行成功我才會向下執行,可是你看我在execute函式中並沒有這樣的語句,比如:

def execute(self):
    if self.valid_data():
        user_account = self._factory_user_account()
        self._send_welcome_email_to(user_account)
        return user_account
    else:
        pass

換句話說你的每個函式都可能有返回值,如果每一個你都這樣寫程式碼就太囉嗦了。其實你可以看到在valid_data函式中我的確返回了True,但是我希望你也應該注意,如果使用者存在的話我並沒有返回False,而是raise一個異常,這樣這個異常就會被呼叫方獲取而且還能獲取錯誤資訊,這種方式將是一個很好的處理方式,具體你可以通過views.py中看到。

Forms表單驗證

這裡是對於使用者輸入做檢查

"""
表單驗證功能
"""
from django import forms
from django.utils.translation import gettext as _


class RegisterAccountForm(forms.Form):
    username = forms.CharField(max_length=50, required=True, error_messages={
        'max_length': '使用者名稱不能超過50個字元',
        'required': '使用者名稱不能為空',
    })
    
    email = forms.EmailField(required=True)
    password = forms.CharField(min_length=6, max_length=20, required=True, widget=forms.PasswordInput())
    confirm_password = forms.CharField(min_length=6, max_length=20, required=True, widget=forms.PasswordInput())

    def clean_confirm_password(self) -> str:  # -> str 表示的含義是函式返回值型別是str,在列印函式annotation的時候回顯示。
        """
        clean_XXXX XXXX是欄位名
        比如這個方法是判斷兩次密碼是否一致,密碼框輸入的密碼就算符合規則但是也不代表兩個密碼一致,所以需要自己來進行檢測
        :return:
        """
        password = self.cleaned_data.get('password')
        confirm_password = self.cleaned_data.get('confirm_password')

        if confirm_password != password:
            raise forms.ValidationError(message='Password and confirmation do not match each other')

        return confirm_password

前端可以實現輸入驗證,但是也很容易被跳過,所以後端肯定也需要進行操作,當然我這裡並沒有做預防XSS攻擊的措施,因為這個不是我們今天要討論的主要內容。

Views

from django.shortcuts import render, HttpResponse, HttpResponseRedirect
from rest_framework.views import APIView
from django.utils.http import urlsafe_base64_encode, urlsafe_base64_decode
from django.utils.encoding import force_bytes, force_text
from .forms import (
    RegisterAccountForm,
)
from .services import (
    RegisterUserAccount,
    UsernameAlreadyExistError,
    ActivateUserAccount,
    ActivatitionTokenError,
    UserIdIsNotExistError,
)
# Create your views here.


class Register(APIView):
    def get(self, request):
        return render(request, 'register.html')

    def post(self, request):
        # print("request.data 的內容: ", request.data)
        # print("request.POST 的內容: ", request.POST)

        # 針對資料輸入做檢查,是否符合規則
        ra_form = RegisterAccountForm(request.POST)
        if ra_form.is_valid():
            # print("驗證過的資料:", ra_form.cleaned_data)
            rua = RegisterUserAccount(request=request, **ra_form.cleaned_data)
            try:
                rua.execute()
            except UsernameAlreadyExistError as err:
                # 這裡就是捕獲自定義異常,並給form物件新增一個錯誤資訊,並通過模板渲染然後返回前端頁面
                ra_form.add_error('username', str(err))
                return render(request, 'register.html', {'info': ra_form.errors})

            return HttpResponse('We have sent you an email, please confirm your email address to complete registration')
            # return HttpResponseRedirect("/account/login/")
        else:
            return render(request, 'register.html', {'info': ra_form.errors})


class Login(APIView):
    def get(self, request):
        return render(request, 'login.html')

    def post(self, request):
        print("request.data 的內容: ", request.data)
        print("request.POST 的內容: ", request.POST)
        pass


class ActivateAccount(APIView):
    # 使用者啟用賬戶
    def get(self, request, uidb64, token):
        try:
            # 獲取URL中的使用者ID
            uid = force_bytes(urlsafe_base64_decode(uidb64))
            # 啟用使用者
            aua = ActivateUserAccount(uid, token)
            aua.execute()
            return render(request, 'login.html')
        except(ActivatitionTokenError, UserIdIsNotExistError) as err:
            return HttpResponse('Activation is failed.')

這裡就是檢視層不同URL由不同的類來處理,這裡只做基本的接收輸入和返回輸出功能,至於接收到的輸入該如何處理則有其他元件來完成,針對輸入格式規範則由forms中的類來處理,針對資料驗證過後的具體業務邏輯則由services中的類來處理。

Urls

from django.urls import path, re_path, include
from .views import (
    Register,
    Login,
    ActivateAccount,
)


app_name = 'users'
urlpatterns = [
    re_path(r'^register/$', Register.as_view(), name='register'),
    re_path(r'^login/$', Login.as_view(), name='login'),
    re_path(r'^activate/(?P[0-9A-Za-z_\-]+)/(?P[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$',
            ActivateAccount.as_view(), name='activate'),
]

Templates

是我用到的html模板,我就不放在這裡了,大家可以去這裡https://files.cnblogs.com/files/rexcheny/mysite.zip

下載全部的程式碼

頁面效果

Django工程的分層結構

Django工程的分層結構

啟用郵件內容

Django工程的分層結構

點選後就會跳轉到登陸頁。下面我們從Django admin中檢視,2個使用者是啟用狀態的。

Django工程的分層結構

相關文章