这篇文章旨在解决Django(一个流行的Python框架)中最常见的十种反面模式(anti-pattern:指的是在实践中明显出现但又低效或是有待优化的设计模式)。请确保避免这些陷阱,以确保代码的生产就绪性。
公众号: 滑翔的纸飞机
Django 的理念是 “Fat models, thin views —— 胖模型,瘦视图”。然而,过多的将逻辑封装到模型是一种反面模式。更好的建议是将相关逻辑封装到单独的类或函数中。
Fat Models:在许多框架中,架构师们通常建议在模型中处理大部分逻辑,让模型变得臃肿,而控制器则变得瘦小。这个想法是将业务逻辑尽可能地放在模型中,以使控制器的职责尽量简单和干净。
优势包括:
(1)可重用性:将逻辑放在模型中意味着该逻辑可以在不同的控制器和视图中共享和重复使用。
(2)可维护性:通过集中逻辑处理,并在模型中定义清晰的接口,可以更容易地维护代码。
(3)测试性:将逻辑放在模型中允许更容易进行单元测试和集成测试。
(4)封装数据和行为:模型是为了表达数据和执行与数据相关的行为而存在的,将逻辑放在模型中使数据和行为紧密关联。模型臃肿的缺点:
(1)可读性:将所有的逻辑都集中到模型中可能导致模型变得难以阅读和理解。如果逻辑过于复杂,或者包含大量的业务规则,可能会使代码难以维护和扩展。
(2)耦合度:将过多的逻辑放在模型中可能会导致模型与其他组件(如视图和控制器)之间的耦合度增加。这种紧密的耦合关系可能会导致代码的可测试性和可维护性下降。
(3)违反单一职责原则:如果一个模型承载了太多的责任和功能,那么它很可能违反了单一职责原则。模型的职责应该是与数据相关的,而不是包含复杂的业务逻辑。
在实际开发中,模型设计和控制器设计需要找到一个平衡点,以便实现高内聚、低耦合的代码。
示例:
# Anti-pattern
class Order(models.Model):
...
def total(self):
return sum(i.price for i in self.items.all())
# 更好的方法
class Order(models.Model):
...
def total(self):
return OrderCalculator(self).total()
class OrderCalculator:
def __init__(self, order):
self.order = order
def total(self):
return sum(i.price for i in self.order.items.all())
在 Django 中,空字符串值总是存储为空字符串,而不是 NULL。除非必要,否则不要对 CharField 和 TextField 等基于字符串的字段使用 Null=True。
**原因:**如 CharField 和 TextField 。如果一个基于字符串的字段有null=True,这意味着它有两个可能的 “无数据 “值:NULL 和 空字符串。在大多数情况下,为 “无数据 “设置两个可能的值是多余的;
Django的惯例是使用空字符串而不是 NULL 。一个例外是当 CharField 同时设置了unique=True和blank=True。在这种情况下,null=True是必须的,以避免在保存多个空白值的对象时违反唯一约束。
对于基于字符串和非字符串的字段,如果你想在表单中允许空值,还需要设置 blank=True,因为null参数只影响数据库存储(见 blank)。
# Anti-pattern
name = models.CharField(max_length=100, null=True)
# 更好的方法
name = models.CharField(max_length=100, default="")
避免重复代码 —— 确保 DRY(不要重复自己)原则。
# Anti-pattern
def view(request):
order = get_object_or_404(Order, id=request.POST['order'])
order.status = 'shipped'
order.shipped_date = datetime.datetime.now()
order.save()
# 更好的方法
def view(request):
order = get_object_or_404(Order, id=request.POST['order'])
order.mark_as_shipped() # 封装关联逻辑:order.status = 'shipped' ...
切勿在模板中直接查询数据库;这有悖于 MVC 模式,并可能导致性能问题。
{% for item in order.items.all %}
{% for item in items %}
Django ORM提供了强大的特性,比如 F( ) 表达式,可以减少数据库的访问次数,提高效率。
F( ) 函数: 一个 F( ) 对象代表了一个model的字段值或注释列。使用它就可以直接参考model的field和执行数据库操作而不用再把它们(model field)查询出来放到python内存中。
# Anti-pattern
product.view_count = product.view_count + 1
product.save()
# 更好的方法
from django.db.models import F
product.view_count = F('view_count') + 1
product.save(update_fields=['view_count'])
看起来似乎都差不多,但是用 F( ) 函数有几个显著的好处:
减少了操作次数。product.view_count + 1
是 Python 在内存中操作的,然后再从内存把数据更新到数据库;而 F('view_count') + 1
是直接操作的数据库,减少了一个操作层级。
避免竞争。竞争是指多个 Python 线程同时对同一个数据进行更新,product.view_count + 1
就有可能丢失其中的某些更新操作,而 F('view_count') + 1
由于是直接操作数据库,不会有丢失数据的问题。
注意,正因为F( )函数没有在内存中操作,因此更新完数据后需要重新刷新内存中的模型对象:
# 重新取值
product = Product.objects.get(...)
...
product.save()
# 重新取值
product.refresh_from_db()
避免在视图中处理表单逻辑。Django 的表单允许你封装所有与表单相关的逻辑,包括验证。
# Anti-pattern
def register(request):
if request.method == 'POST':
if not request.POST.get('username'):
return HttpResponse("Username is required")
# 更好的方法
class RegisterForm(forms.Form):
username = forms.CharField()
def register(request):
if request.method == 'POST':
form = RegisterForm(request.POST)
if form.is_valid():
...
装饰器可以使代码更简洁,特别是用于访问控制。
# Anti-pattern
def edit(request, id):
if not request.user.is_authenticated:
return redirect('login')
...
# 更好的方法
from django.contrib.auth.decorators import login_required
@login_required
def edit(request, id):
...
硬编码 URL 可能会在更改 URL 结构时造成问题。请使用 Django 的 reverse() 函数或 {% url % }
模板标签。
# Anti-pattern
def view(request):
return redirect('/home/')
# 更好的方法
from django.urls import reverse
def view(request):
return redirect(reverse('home'))
模板:
Home
Home
通过使用 select_related
和 prefetch_related
,避免 N+1 问题。
# Anti-pattern
for order in Order.objects.all():
print(order.customer.name) # 每次迭代都会访问数据库
# 更好的方法
for order in Order.objects.select_related('customer').all():
print(order.customer.name) # 只访问一次DB
编写测试用例不是一种选择,而是一种需要。Django 内置的测试框架非常强大,请充分利用它。
# Anti-pattern
# 缺少测试!
# 更好的方法
from django.test import TestCase
from .models import Order
class OrderModelTest(TestCase):
def test_order_total(self):
...
编写可维护且高效的 Django 代码是一门需要不断学习的艺术。请牢记这些反面模式,以避免常见陷阱、确保代码质量并提高 Django 应用程序的性能。