Django工程的分层结构详解

yipeiwu_com5年前Python基础

前言

传统上我们都知道在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。这么说有些空洞,我们用一个例子来说明:

场景是用户注册:

  • 信息填写规范且用户不存在则注册成功并发送账户激活邮件
  • 如果用户已存在则程序引发错误,然后传递到上层并进行告知用户名已被占用

Django 2.2.1、Python 3.7

下图是整个工程的结构

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模板,我就不放在这里了

下载全部的代码

页面效果

激活邮件内容

点击后就会跳转到登陆页。下面我们从Django admin中查看,2个用户是激活状态的。

相关文章

浅析PyTorch中nn.Linear的使用

浅析PyTorch中nn.Linear的使用

查看源码 Linear 的初始化部分: class Linear(Module): ... __constants__ = ['bias'] def __init__(s...

python PIL/cv2/base64相互转换实例

PIL和cv2是python中两个常用的图像处理库,PIL一般是anaconda自带的,cv2是opencv的python版本。base64在网络传输图片的时候经常用到。 ##PIL...

10种检测Python程序运行时间、CPU和内存占用的方法

10种检测Python程序运行时间、CPU和内存占用的方法

在运行复杂的Python程序时,执行时间会很长,这时也许想提高程序的执行效率。但该怎么做呢? 首先,要有个工具能够检测代码中的瓶颈,例如,找到哪一部分执行时间比较长。接着,就针对这一部分...

Python实现的字典值比较功能示例

Python实现的字典值比较功能示例

本文实例讲述了Python实现的字典值比较功能。分享给大家供大家参考,具体如下: #coding=utf8 import logging import os from Lib.Dea...

用python wxpy管理微信公众号并利用微信获取自己的开源数据

用python wxpy管理微信公众号并利用微信获取自己的开源数据

之前了解到itchat 乃至于 wxpy时 是利用tuling聊天机器人的接口。调用接口并保存双方的问答结果可以作为自己的问答词库的一个数据库累计。这些数据可以用于自己训练。 而最近希望...