跳转至主要内容

Python编程

Python/Django中设计模式的应用

Sprite
发表于 2024年10月26日

1. Don’t Repeat Yourself(不要重复你自己)

在 Django 开发中,我们很容易陷入“重复代码”的陷阱,比如多个视图或模型中有类似的逻辑。为了避免这样的情况,我们可以使用 DRY(Don’t Repeat Yourself)原则,减少重复的代码块,使代码更简洁且易于维护。

我们看下面这个例子,我们有两个视图,它们都需要从数据库中获取用户信息并进行验证。一个是展示用户信息的视图,另一个是用户编辑视图。没有使用 DRY 原则之前,代码可能会长这样:

# views.py
from django.shortcuts import render, get_object_or_404
from .models import User

def user_detail(request, user_id):
    user = get_object_or_404(User, id=user_id)
    if not user.is_active:
        return render(request, 'error.html', {'message''用户未激活'})
    return render(request, 'user_detail.html', {'user': user})

def user_edit(request, user_id):
    user = get_object_or_404(User, id=user_id)
    if not user.is_active:
        return render(request, 'error.html', {'message''用户未激活'})
    return render(request, 'user_edit.html', {'user': user})

你会发现,这里 user_detailuser_edit 两个视图的代码几乎完全相同,尤其是用户获取和验证的部分。这样的重复代码不仅显得繁琐,还增加了将来维护时出错的风险。现在我们用 DRY 原则来优化一下。

使用 DRY 原则改写

我们可以将获取用户的逻辑提取成一个单独的函数,这样一来,当我们需要修改这个逻辑时,只需改动一个地方即可:

# utils.py
from django.shortcuts import get_object_or_404
from .models import User

def get_active_user(user_id):
    user = get_object_or_404(User, id=user_id)
    if not user.is_active:
        return None
    return user

# views.py
from django.shortcuts import render
from .utils import get_active_user

def user_detail(request, user_id):
    user = get_active_user(user_id)
    if not user:
        return render(request, 'error.html', {'message''用户未激活'})
    return render(request, 'user_detail.html', {'user': user})

def user_edit(request, user_id):
    user = get_active_user(user_id)
    if not user:
        return render(request, 'error.html', {'message''用户未激活'})
    return render(request, 'user_edit.html', {'user': user})

代码优化后,user_detailuser_edit 视图现在都引用了 get_active_user 函数,这样,如果我们想更改“用户未激活”的处理逻辑,只需要在 get_active_user 中修改一次即可,而不需要在每个视图中都进行修改。这样不仅提升了代码复用性,还降低了出错的可能性。

2. Keep It Simple, Stupid(保持简单,愚蠢)

KISS 原则强调代码要保持简单和直观,不要刻意复杂化,避免那些看起来很“聪明”但难以理解的写法。Django 本身就提供了很多简化开发的工具,所以在实现某些功能时,没必要“自造轮子”。在 Django 项目里,我们可以通过合理使用框架提供的工具来保持代码简单。

我们要实现一个“获取最新文章列表”的视图。可能有的开发者为了显示技术水平,会写一个复杂的 SQL 查询语句,手动从数据库中提取最新的文章,甚至处理时间格式等。但在 Django 中,我们可以直接使用 ORM 的简洁方法来实现。

没有使用 KISS 原则时的代码可能如下:

# views.py
from django.shortcuts import render
from django.db import connection

def latest_articles(request):
    with connection.cursor() as cursor:
        cursor.execute("""
            SELECT * FROM articles_article
            WHERE published_date <= NOW()
            ORDER BY published_date DESC LIMIT 10
        """
)
        articles = cursor.fetchall()
    return render(request, 'latest_articles.html', {'articles': articles})

这样的代码虽然能实现需求,但涉及了数据库的底层操作,复杂且难以维护,容易导致 SQL 注入等安全问题。

使用 KISS/Keep It Simple, Stupid 原则改写

在 Django 中,我们可以直接使用 ORM 来实现相同功能,这样的代码更加直观、简洁,并且更具可读性:

# views.py
from django.shortcuts import render
from .models import Article

def latest_articles(request):
    articles = Article.objects.filter(published_date__lte=timezone.now()).order_by('-published_date')[:10]
    return render(request, 'latest_articles.html', {'articles': articles})

改写后,代码不仅更短,更重要的是更加易懂。任何熟悉 Django ORM 的人都可以轻松理解这段代码的功能。保持代码简单,不仅帮助他人更快地理解你的逻辑,也为自己将来的维护工作减少了麻烦。

3. YAGNI/You Ain’t Gonna Need It(你不会需要它)

YAGNI 原则的核心在于:不要提前写不需要的功能。Django 项目中,有时我们会为了“以防万一”而编写许多未使用的代码或复杂的抽象逻辑,结果不仅浪费时间,还会增加代码维护难度。YAGNI 提醒我们,只在真正需要某个功能时才去实现它,避免无谓的开发工作。

我们有一个博客系统,当前只需要用户能够发布文字内容,但开发者预见未来可能会需要图片、视频等其他媒体内容,于是提前设计了支持多种媒体的复杂系统:

# models.py
class Media(models.Model):
    MEDIA_TYPE_CHOICES = (
        ('text''Text'),
        ('image''Image'),
        ('video''Video'),
    )
    type = models.CharField(max_length=10, choices=MEDIA_TYPE_CHOICES)
    content = models.TextField()
    url = models.URLField(blank=True, null=True)

这种设计虽然很“全面”,但却并不符合当前需求,导致多了很多无用的代码。例如,当前需求只是发布文字,这样设计反而让处理逻辑复杂化。

使用 YAGNI 原则改写

根据当前需求,直接创建一个简单的 Post 模型即可,未来真正需要其他媒体支持时,再根据需求扩展:

# models.py
class Post(models.Model):
    content = models.TextField()
    published_date = models.DateTimeField(auto_now_add=True)

这样写更加贴合当前的需求,后续如需支持图片或视频,可以再逐步扩展,而不是在一开始就设计过度复杂的系统。

在此基础上,Django 只需创建 Post 实例,存储和显示文字内容变得更简洁,符合当前的需求。当后续需要添加其他媒体支持时,再通过扩展字段或关联表来实现,减少了维护负担。

4. 关注点分离

关注点分离的核心是将不同职责的代码分开,避免在一个文件或函数中处理多种逻辑。在 Django 项目中,我们可以通过模型、视图、模板、表单等组件来实现功能的分离,使每部分代码专注于各自的职责。这样既提升了代码的可读性,也便于维护和测试。

现在我们要实现一个简单的用户注册功能。没有使用关注点分离的情况下,所有逻辑(表单处理、数据验证、数据库操作、页面渲染)可能会被写在同一个视图里:

# views.py
from django.shortcuts import render, redirect
from django.contrib.auth.models import User

def register(request):
    if request.method == 'POST':
        username = request.POST['username']
        password = request.POST['password']
        email = request.POST['email']

        if len(password) < 8:
            return render(request, 'register.html', {'error''密码至少要有 8 位'})

        user = User.objects.create_user(username=username, password=password, email=email)
        return redirect('login')
    return render(request, 'register.html')

在这种情况下,验证逻辑、用户创建逻辑和视图渲染逻辑都混在一起,代码难以阅读且难以维护。

使用关注点分离改写

可以通过 Django 的表单类将验证逻辑独立出来,视图中只需处理表单提交和渲染,这样每个部分各司其职:

# forms.py
from django import forms
from django.contrib.auth.models import User

class RegisterForm(forms.ModelForm):
    password = forms.CharField(widget=forms.PasswordInput, min_length=8)

    class Meta:
        model = User
        fields = ['username''password''email']

# views.py
from django.shortcuts import render, redirect
from .forms import RegisterForm

def register(request):
    if request.method == 'POST':
        form = RegisterForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('login')
        else:
            return render(request, 'register.html', {'form': form})
    form = RegisterForm()
    return render(request, 'register.html', {'form': form})

重构后,视图代码更清晰地展现了页面加载和表单提交的逻辑。表单验证逻辑被转移到 RegisterForm 中,遵循关注点分离原则。如果需要修改验证规则,只需调整表单代码,视图逻辑无需改变,这样更易于维护和扩展。

5. SOLID 原则

SOLID 原则是一组设计原则,帮助我们编写结构清晰、可维护性高的代码。在 Django 项目中,通过合理应用 SOLID 原则,可以使代码更模块化,便于扩展和修改。

5.1 单一职责原则(SRP)

单一职责原则(SRP)要求每个类或函数只做一件事,避免过多的职责集中在一个地方。这样可以提高代码的可读性和可维护性,后续修改需求时只需改动一个地方,不会影响其他职责。

示例:

假设我们有一个视图用于上传文件和处理文件内容,没有遵循 SRP 原则时,代码可能会将上传和处理逻辑混在一起:

# views.py
from django.shortcuts import render
from .models import File
import csv

def upload_and_process_file(request):
    if request.method == 'POST' and request.FILES['file']:
        uploaded_file = request.FILES['file']
        file_instance = File.objects.create(file=uploaded_file)

        # 文件内容处理逻辑
        data = []
        for row in csv.reader(file_instance.file.open()):
            data.append(row)

        return render(request, 'upload_success.html', {'data': data})
    return render(request, 'upload.html')

在这里,文件上传和内容处理都集中在 upload_and_process_file 视图中,不便于维护。

应用 SRP 改写:

可以将文件上传和内容处理分开,使每个视图只负责一项任务:

# utils.py
import csv

def process_file_content(file):
    data = []
    for row in csv.reader(file.open()):
        data.append(row)
    return data

# views.py
from django.shortcuts import render
from .models import File
from .utils import process_file_content

def upload_file(request):
    if request.method == 'POST' and request.FILES['file']:
        uploaded_file = request.FILES['file']
        file_instance = File.objects.create(file=uploaded_file)
        return render(request, 'upload_success.html', {'file_id': file_instance.id})
    return render(request, 'upload.html')

def process_file(request, file_id):
    file_instance = File.objects.get(id=file_id)
    data = process_file_content(file_instance.file)
    return render(request, 'file_data.html', {'data': data})

通过 SRP 原则的改写,文件上传逻辑与内容处理逻辑分离。若要修改处理逻辑,只需改动 process_file_content 函数即可,而不会影响上传视图的逻辑。

5.2 开放/封闭原则(OCP)

开放/封闭原则(OCP)指的是:软件实体(类、模块、函数等)应该对扩展开放,但对修改封闭。换句话说,我们应尽量通过添加新代码的方式去扩展已有功能,而不是直接修改已有代码,这样可以减少对原有功能的影响,提高代码的稳定性。

我们有一个视图,用于根据用户的角色(如管理员、编辑、普通用户)显示不同的页面内容。没有应用 OCP 原则的代码可能如下:

# views.py
from django.shortcuts import render

def user_dashboard(request, role):
    if role == 'admin':
        return render(request, 'admin_dashboard.html')
    elif role == 'editor':
        return render(request, 'editor_dashboard.html')
    elif role == 'user':
        return render(request, 'user_dashboard.html')
    else:
        return render(request, 'error.html', {'message''未知角色'})

如果将来增加一个新角色,比如“访客”,我们就需要修改这段代码,增加新的条件判断。这种写法不利于扩展,因为每次添加新角色都要修改视图函数本身。

使用 OCP 改写

我们可以通过将不同角色的逻辑分开,把各角色的处理代码独立成函数,然后用字典映射的方式来实现角色的动态扩展,这样就不需要频繁修改原始代码了:

# views.py
from django.shortcuts import render

def admin_dashboard(request):
    return render(request, 'admin_dashboard.html')

def editor_dashboard(request):
    return render(request, 'editor_dashboard.html')

def user_dashboard_view(request):
    return render(request, 'user_dashboard.html')

role_dashboard_mapping = {
    'admin': admin_dashboard,
    'editor': editor_dashboard,
    'user': user_dashboard_view,
}

def user_dashboard(request, role):
    dashboard_view = role_dashboard_mapping.get(role)
    if dashboard_view:
        return dashboard_view(request)
    else:
        return render(request, 'error.html', {'message''未知角色'})

重构后的代码让每个角色的处理逻辑分开,并将其放入 role_dashboard_mapping 字典中。这样,如果要添加一个新角色,只需增加一个新的视图函数,并将其映射到字典中即可,不用再修改 user_dashboard 函数的主体逻辑。这种做法符合开放/封闭原则,提高了代码的扩展性和可维护性。

5.3 里氏替换原则(LSP)

里氏替换原则(LSP)要求:在继承关系中,子类应该可以替换掉基类而不影响程序的正确性。这一原则提醒我们在设计子类时,尽量避免更改父类的预期行为,以保持代码的一致性和可替换性。

我们有一个基本的通知类 Notification,用于发送消息。之后,我们创建了一个 EmailNotification 子类来发送电子邮件,未遵循 LSP 时,子类可能会更改父类的行为或接口,导致程序运行异常。

# notifications.py
class Notification:
    def send(self, message):
        print(f"Sending notification: {message}")

class EmailNotification(Notification):
    def send(self, message, email):
        print(f"Sending email to {email}{message}")

# views.py
def notify_user(notification: Notification, message: str):
    notification.send(message)

这里的 EmailNotification 重写了 send 方法并新增了一个 email 参数,导致 notify_user 函数无法正常运行,因为它期望所有通知类的 send 方法只需要 message 参数。这违反了 LSP。

使用 LSP 改写

我们可以通过调整设计,让 Notification 类在初始化时接受参数,从而避免子类更改接口。这样,子类 EmailNotification 可以保持与父类一致的 send 方法参数,确保子类可以无缝替换基类。

# notifications.py
class Notification:
    def __init__(self, recipient):
        self.recipient = recipient

    def send(self, message):
        print(f"Sending notification to {self.recipient}{message}")

class EmailNotification(Notification):
    def send(self, message):
        print(f"Sending email to {self.recipient}{message}")

# views.py
def notify_user(notification: Notification, message: str):
    notification.send(message)

改写后的代码使 EmailNotification 子类的 send 方法与基类 Notificationsend 方法保持一致,现在 notify_user 函数可以无论传入 Notification 还是 EmailNotification 都能正常工作。这遵循了 LSP,保证了代码的可替换性。

5.4 接口隔离原则(ISP)

接口隔离原则(ISP)指的是:不应强迫类去实现不需要的接口,避免接口臃肿。对于 Django 项目而言,我们可以通过将功能分离到多个小接口(或者多个小方法)中,避免让类承担过多职责,提升代码的灵活性。

现在我们有一个用户通知系统,其中有一个 Notifier 接口,既包括了短信通知的方法,也包括了电子邮件通知的方法。如果某些通知系统不需要所有的通知功能(例如仅使用短信),这将导致代码冗余。

未遵循 ISP 的代码如下:

# notifications.py
class Notifier:
    def send_sms(self, message, phone_number):
        pass

    def send_email(self, message, email_address):
        pass

class SMSNotifier(Notifier):
    def send_sms(self, message, phone_number):
        print(f"Sending SMS to {phone_number}{message}")

    def send_email(self, message, email_address):
        raise NotImplementedError("SMSNotifier does not support email notifications")

这里的 SMSNotifier 不需要发送电子邮件,但仍然被迫实现了 send_email 方法,这违背了接口隔离原则。

使用 ISP 改写

我们可以将 Notifier 拆分为更小的接口,使每个通知功能分离开。这样,每个通知类只需实现自己需要的功能。

# notifications.py
class SMSNotifier:
    def send_sms(self, message, phone_number):
        print(f"Sending SMS to {phone_number}{message}")

class EmailNotifier:
    def send_email(self, message, email_address):
        print(f"Sending email to {email_address}{message}")

重构后,SMSNotifier 只实现了 send_smsEmailNotifier 只实现了 send_email,各自的职责更加清晰。这样做符合接口隔离原则,避免了不必要的实现,从而让代码更简洁、灵活,后续扩展和维护也更加方便。

5.5 依赖倒置原则(DIP)

依赖倒置原则(DIP)要求我们依赖于抽象而不是具体实现。也就是说,高层模块(调用逻辑)不应该依赖于低层模块(具体实现),而是依赖于接口或抽象。这在 Django 项目中尤其适用,通过依赖抽象可以让代码更具扩展性和可测试性。

我们有一个用户通知功能,其中高层的 UserNotifier 类直接依赖于 SMSNotifier 类,如果我们将来需要改为电子邮件通知或者其他方式,就需要修改 UserNotifier 的代码,不符合 DIP 原则。

# notifications.py
class SMSNotifier:
    def send(self, message, phone_number):
        print(f"Sending SMS to {phone_number}{message}")

class UserNotifier:
    def __init__(self):
        self.notifier = SMSNotifier()

    def notify(self, message, phone_number):
        self.notifier.send(message, phone_number)

这里的 UserNotifier 类直接依赖 SMSNotifier,不利于扩展和测试。

使用 DIP 改写

我们可以定义一个抽象的 Notifier 接口,让 UserNotifier 依赖于这个接口,而不是具体实现。这样,我们可以随时替换 Notifier 的实现,而不需要修改 UserNotifier 的代码。

# notifications.py
from abc import ABC, abstractmethod

class Notifier(ABC):
    @abstractmethod
    def send(self, message, recipient):
        pass

class SMSNotifier(Notifier):
    def send(self, message, phone_number):
        print(f"Sending SMS to {phone_number}{message}")

class EmailNotifier(Notifier):
    def send(self, message, email_address):
        print(f"Sending email to {email_address}{message}")

class UserNotifier:
    def __init__(self, notifier: Notifier):
        self.notifier = notifier

    def notify(self, message, recipient):
        self.notifier.send(message, recipient)

在使用时,只需在初始化 UserNotifier 时传入需要的 Notifier 实现即可:

# 使用 SMSNotifier
sms_notifier = UserNotifier(SMSNotifier())
sms_notifier.notify("Hello!""+123456789")

# 使用 EmailNotifier
email_notifier = UserNotifier(EmailNotifier())
email_notifier.notify("Hello!""test@example.com")

重构后,UserNotifier 依赖于抽象接口 Notifier,而不是具体实现 SMSNotifierEmailNotifier。这样做不仅提高了代码的灵活性和可扩展性,也更符合依赖倒置原则,后续若要增加新的通知方式,只需实现 Notifier 接口即可,无需更改 UserNotifier 的代码。

总结

在 Django 项目中,合理运用设计模式不仅能提升代码的可读性和可维护性,还能减少未来修改和扩展时的工作量。通过 DRYKISSYAGNI 原则,我们可以避免代码冗余,保持代码简单、聚焦需求;关注点分离 能让每个模块各司其职;SOLID 原则则帮助我们构建更健壮、灵活的系统。

无论是大型项目还是小型应用,设计模式都能为代码架构提供良好的支撑,特别是随着功能复杂度的增加,合理的设计可以显著减少维护难度。我们在开发过程中,不妨多思考如何应用这些原则,让项目更易维护、更可扩展,编写出“优雅”的 Django 代码!

写作不易,欢迎关注

Previous Post

Python 中的不可变对象 vs 可变对象 

Next Post

No newer posts

评论已关闭。