目录与简介
📋 文档概览
本文档是 Django Styleguide 的完整中文翻译版本。这是一份由 HackSoft 团队基于多年 Django 项目经验总结而成的最佳实践指南。
翻译说明:
✅ 本翻译保留了所有原文内容和代码示例
📝 添加了详细的中文注解和难点说明
🔖 对专有名词提供了解释
💡 标注了关键要点和注意事项
📚 完整目录
概览
Django Styleguide 的核心可以总结如下:
在 Django 中,业务逻辑应该位于:
Services(服务) - 函数,主要负责向数据库写入数据。
Selectors(选择器) - 函数,主要负责从数据库读取数据。
Model properties(模型属性)(有一些例外)。
Model
clean方法用于额外验证(有一些例外)。
在 Django 中,业务逻辑不应该位于:
❌ APIs 和 Views
❌ Serializers 和 Forms
❌ Form tags
❌ Model 的
save方法❌ 自定义 managers 或 querysets
❌ Signals(信号)
🔖 核心概念解释 (Business Logic & Architecture)
业务逻辑(Business Logic):应用程序处理核心业务规则的代码。
✅ 属于业务逻辑:计算订单总价、验证业务权限、复杂状态流转、更新库存。
❌ 不属于业务逻辑:API 路由分发、JSON 序列化/反序列化(Serializers)、HTTP 状态码处理、底层数据库连接管理。这些属于基础设施/胶水代码。
Services vs Selectors:
Services:写操作(Create, Update, Delete)。
Selectors:读操作(Read, Query)。
分离关注点(Separation of Concerns):将核心业务行为从接口层(API)和持久层(Model)中抽离,让代码不再是一团乱麻。
控制反转(IoC/DI)与逻辑纯度:
💡 进阶建议:对于复杂的外部依赖(如短信网关、第三方支付 SDK),可以通过依赖注入的方式传入 Service,而不是在 Service 内部硬编码实例化。
目的:防止底层实现细节“污染”核心业务逻辑。这样在测试时,我们只需传入一个 Mock 对象即可,无需真正连接网络。
⚠️ 架构适用范围说明:
本指南的最佳实践主要针对单体架构或垂直单机逻辑。在分布式架构中,涉及到跨服务的分布式事务(TCC/Saga)、最终一致性等逻辑,其复杂度和处理方式会有本质不同。
业务逻辑(Business Logic):指应用程序中处理核心业务规则的代码,例如: ⚠️ N+1 查询问题详解
这是 ORM 中常见的性能问题。当你在循环中访问一个关联对象字段(如 ForeignKey 或 OneToOneField)且没有提前加载它时,就会发生这种现象:
python# 错误示例:会产生 N+1 查询 # 假设 User 模型有一个 profile 关联字段 for user in User.objects.all(): # 第 1 次查询:获取所有 User # 每次循环都会去数据库查一次对应的 Profile (N 次重复操作) print(user.profile.bio) # 正确示例:使用 select_related (针对普通外键和一对一字段) # 通过 SQL JOIN 一次性把 User 和 Profile 的数据都拿回来 for user in User.objects.select_related('profile'): # 仅 1 次查询 (JOIN) print(user.profile.bio) # 内存中直接获取,不会额外查询
总体思路是"分离关注点",以便这些关注点可以被维护和测试。
为什么不建议将逻辑分散在各处?
🤔 为什么不把业务逻辑放在传统的 APIs / Views / Serializers / Forms 中?
🚩 默认“脚手架”的陷阱
当你运行 python manage.py startapp my_app 时,Django 会为你生成如下结构:
my_app/
├── admin.py
├── apps.py
├── migrations/
├── models.py # ❌ 这里的后果:肥大的 Model
├── tests.py
└── views.py # ❌ 这里的后果:臃肿的视图函数
如果你再引入 DRF,通常还会增加:
├── serializers.py # ❌ 这里的后果:充满副作用的校验逻辑
└── urls.py
为什么这种结构不合理? Django 默认的 MTV(Model-Template-View)设计初衷是处理“快速原型开发”。在复杂的商业环境下,这种结构由于缺乏独立的“业务层(Service Layer)”,导致开发者被迫将逻辑强行塞入上述文件中:
逻辑碎片化(Fragmentation):业务逻辑散落在
views.py、serializers.py和models.py中。追踪一个简单的“支付成功”流程需要你像侦探一样在三四个文件间反复跳转。重度依赖黑盒抽象:为了在 View 层修改一个小逻辑,你不得不深入研究 DRF
CreateAPIView的内部生命周期。而且,最关键的是——这些逻辑无法脱离 HTTP 请求环境运行。如果你想在 Celery 异步任务、管理命令或其他非 API 场景复用代码,你会发现它们被锁死在了 View 里。
💡 实际案例
想象一个用户注册流程:
不好的做法(逻辑分散):
python# 在 serializer 中 class UserSerializer(serializers.ModelSerializer): def create(self, validated_data): user = User.objects.create(**validated_data) # 发送欢迎邮件?创建用户资料? return user # 在 view 中 class UserCreateView(CreateAPIView): def perform_create(self, serializer): user = serializer.save() # 还要在这里做些什么? # 在 model 中 class User(models.Model): def save(self, *args, **kwargs): # 还有更多逻辑? super().save(*args, **kwargs)好的做法(集中在 service):
pythondef user_create(*, email: str, password: str) -> User: """用户注册的所有逻辑都在这里""" user = User(email=email) user.set_password(password) user.full_clean() user.save() profile_create(user=user) send_welcome_email(user=user) return user
通用的 APIs 和 Views,结合 serializers 和 forms,是 Django 默认脚手架提供的标准装备。它们对于处理“一写一读”的最直接模型 CRUD(增删改查)非常高效。
但根据我们的实战经验,在严肃的商业项目中,单纯的增删改查几乎不存在。一旦逻辑稍微跳出简单的 CRUD 路径(比如:创建用户的同时要发邮件、初始化多张关联表、调用外部风控 API),代码就会迅速变得臃肿且混乱。
一旦事情变得混乱,你就需要更多的架构“文件夹”(我们姑且称之为“逻辑盒子”),以更科学的方式把代码安家落户。
本风格指南的核心目标即是:
为你定义这些核心“盒子”(例如 Services 和 Selectors)。
帮助你建立一种思维习惯,学会根据业务深度,拆分出适合你自己的逻辑容器。
此外,感谢 GitHub 社区的这段精彩评论(Issue #170),它提出了一个更有力的视角:
业务逻辑与“你是如何调用它”的方式(传输层/入口)应当完全无关。
这是一种非常清晰的界限:
入口(Interface):可以是 API 接口、Admin 动作、管理脚本、甚至是 Celery 异步任务。它们只是“触发器”。
核心(Core):Service 函数。它不应该知道自己是被 HTTP 请求触发的还是被一段定时脚本触发的。
💡 为什么要分离?
如果你把业务逻辑写在 API 视图(View)里,当你某天需要在后台脚本(Management Command)里重执行这段逻辑时,你不得不去模拟网络请求、伪造请求头或创建一个“API 工具用户”。这种对 HTTP 环境的强制依赖,就是典型的代码耦合陷阱。
因此,我们希望在此彻底分离关注点:“核心(Core)” vs “接口(Interface)”。
当然,有些情况下事情会纠缠在一起,但是,思考这种分离的一个好的基准是 - "核心" vs "接口"。
🔖 架构概念:核心与接口
核心(Core):业务规则和逻辑,独立于外部世界。
接口(Interface):与外部交互的方式(API、CLI、Web UI 等)。
💡 实战案例:为什么这对多端开发至关重要?
想象一下,你的项目现在需要同时支持:Web 后台、微信小程序和移动端 APP:
如果没有 Service 层:你可能需要为这三端分别写三个 View。为了实现同一个“下单逻辑”,你不得不在这三个地方进行 Copy-Paste。当订单规则变动时,你需要同步修改三个文件,漏掉任何一个都会导致多端数据或逻辑不一致。
有了 Service 层:不论是 Web 请求、小程序点击还是 APP 调用,它们在 Interface 层处理完各自的权限和格式后,最终都会汇聚到同一个 Core 层方法:
order_create_service()。这种做法不仅解决了解耦问题,更是保障“多端业务一致性”的唯一出路。
🤔 为什么不把业务逻辑放在自定义 managers 和/或 querysets 中?
这实际上是个好主意,对于纯粹的数据过滤和查询增强(例如 Order.objects.active().by_user(user)),自定义 manager 是最佳归宿。
🔖 概念科普:Manager vs QuerySet 到底是什么?
Manager(管理器):它是 Model 的数据库操作入口(表级入口)。它的职责是发起查询。
QuerySet(查询集):它是从数据库查出的对象集合。它的特点是类似函数式编程的声明式链式调用(如
.filter().exclude())。为什么开发者会被“诱惑”在其中写入逻辑?
这种写法具有极强的语义化美感。例如
Order.objects.create_with_gift(...)。这种写法看起来业务逻辑紧贴数据实体,非常符合直觉。但这种“美感”是有代价的:
过度抽象带来的语义坍塌:QuerySet 的链式调用是一种声明式编程。当你把复杂的“核销、扣费、通知”都埋在里面时,这个链条就变成了黑盒。你无法一眼看出调用这行代码会带来多少隐蔽的数据库写入或外部 API 动作。
调试地狱(Debugging Hell):Django 的 QuerySet 是 Lazy(惰性求值) 的。这意味着:
非线性执行:你在 Manager 里打断点看逻辑时,数据库其实还没动;等你真正需要数据时(比如在模板里),逻辑才爆发。这种执行与响应的脱节,会让你的调试堆栈信息(Stack Trace)变得支离破碎,极难定位问题。
无法复用与测试:由于这种逻辑被死死绑定在 QuerySet 的特定状态下,你很难在没有数据库环境的情况下进行纯粹的单元测试。
结论:Managers 是用来 “描述数据” 的;而业务逻辑需要 “编排动作”。后者应该交给平铺直叙、过程清晰的 Service 层。
但是,试图将所有业务逻辑(特别是涉及多表操作的生命周期)都塞进 Manager 绝非良策:
职责错位:Manager 属于持久层/数据访问层。它的核心任务是“如何与数据库打交道”,而不应该关心“如何核销优惠券”或“如何计算复杂的会员费率”。
循环依赖陷阱(跨模型问题):
假设一个“下单”逻辑涉及到
User、Product和Order三个模型。如果你把逻辑放在
OrderManager里,它就必须 importUser和Product。这种跨模型的相互引用,在 Django 项目变大后会引发灾难性的循环导入错误。
Service 层则作为独立的“第三方代理”,可以从容地调用这些模型,保持模型层的纯净和单向引用。
不可控的副作用(Side Effects):跨系统调用(如发送短信、调用支付网关)不应该出现在数据操作层。否则,当你仅仅是为了写个统计脚本或跑个简单的单元测试而创建对象时,你的代码可能会由于误触外部 API 而导致严重的后果。
💡 实际示例
python# 不好的做法:在 manager 中混杂太多业务逻辑 class OrderManager(models.Manager): def create_order(self, user, items): # 创建订单 order = self.create(user=user) # 添加商品 for item in items: order.items.add(item) # 发送邮件(这不应该在 manager 中!) send_mail(...) # 调用支付 API(这也不应该在这里!) payment_gateway.charge(...) # 更新库存(跨越多个模型) Inventory.objects.update(...) return order # 好的做法:在 service 中组织业务逻辑 def order_create(*, user: User, items: List[Item]) -> Order: order = Order.objects.create(user=user) order.items.add(*items) inventory_update_for_order(order=order) payment_process(order=order) order_confirmation_email_send(order=order) return order
核心思想是让你的领域独立于数据模型和 API 层而存在。
如果我们将拥有自定义 queryset/manager 的想法与让领域独立存在的想法结合起来,我们最终会得到我们所说的"服务层"。
Services 可以是函数、类、模块,或者任何对你的具体情况有意义的东西。
考虑到所有这些,自定义 managers 和 querysets 是非常强大的工具,应该用于为你的模型暴露更好的接口。
✅ 最佳实践建议
在 Manager/QuerySet 中放置:
数据访问逻辑
复杂的查询构建
查询优化(select_related, prefetch_related)
在 Service 中放置:
跨模型的业务逻辑
外部 API 调用
发送邮件/通知
复杂的业务规则
🤔 为什么不把业务逻辑放在 signals 中?
在所有可用选项中,也许这个选项会最快地把你带到一个非常糟糕的地方:
Signals 是连接那些本不应该相互了解、但你又希望它们连接起来的事物的好工具。
Signals 也是在业务逻辑层之外处理缓存失效的好工具。
如果我们开始对紧密连接的事物使用 signals,我们只是让连接变得更加隐式,使得追踪数据流变得更加困难。
💡 深度解析:Signal 的机制与“避雷针”
功能定义:Signals 是 Django 内置的“观察者模式”实现。它像一个全项目共享的 “广播电台”:当某个 Model 发生改变时,它喊一嗓子,全项目所有监听这个信号的地方都会被动触发。但这种“被动响应”在复杂业务面前有三大致命雷点:
雷点 1:不可见的“幽灵逻辑”:你在代码里写
user.save(),以为它只改了一个字段。但在你看不到的地方,可能有 5 个信号点正在偷偷修改其他关联表。当你排查数据错误时,你得翻遍全项目搜post_save,这种逻辑断层是调试者的噩梦。雷点 2:批量操作的“集体失灵”:这是最隐蔽的技术坑。Django 的
QuerySet.update()和bulk_create完全不会触发save()信号。如果你把核心逻辑(如“发放奖励”)写在信号里,当你用脚本批量刷数据时,系统逻辑会悄无声息地全部失效,导致严重的数据不一致。雷点 3:性能与循环暗礁:信号是串行执行的。一个简单的保存动作可能因为牵扯了 10 个信号监听器而变得极其沉重;更糟的是,如果 A 的信号修改了 B,B 又由于某种逻辑反过来触改了 A,会引发死循环或性能雪崩。
核心铁律: 信号应只用于 “关注”(如日志记录、缓存清理、ES 索引刷新);绝对不应被用于 “驱动” 核心业务流程。
⚠️ Signals 的陷阱
python# 不好的做法:使用 signal 处理紧密相关的逻辑 @receiver(post_save, sender=User) def create_user_profile(sender, instance, created, **kwargs): if created: Profile.objects.create(user=instance) # 几个月后,另一个开发者添加了... @receiver(post_save, sender=User) def send_welcome_email(sender, instance, created, **kwargs): if created: send_mail(...) # 又有人添加了... @receiver(post_save, sender=User) def create_user_settings(sender, instance, created, **kwargs): # 现在用户创建逻辑分散在 4 个地方! # 很难理解完整的流程好的做法:
pythondef user_create(*, email: str, password: str) -> User: """所有用户创建逻辑都在这里,清晰明了""" user = User(email=email) user.set_password(password) user.save() profile_create(user=user) user_settings_create(user=user) welcome_email_send(user=user) return user
这就是为什么我们建议在非常特定的用例中使用 signals,但总的来说,我们不建议使用它们来构建领域/业务层。
🏗️ 现代软件开发:两大“黄金准则”
为什么我们要如此费劲地拆分 Service 层?因为在现代大型系统的演进中,有两个无法逾越的黄金准则:
1. 单向数据流(Unidirectional Data Flow)
数据应该像河流一样,有明确的上下游,而不是互相关联、反复触发的混乱网状结构。
坏模式:模型 A 改变 -> 触发信号 -> 修改模型 B -> 触发信号 -> 又改了模型 A。这种“死亡循环”是极其难以调试的。
好模式:所有变更都经过 Service 层 这个唯一的阀门,流向数据库。
2. 状态集中管理(Centralized State Management)
业务状态的改变应该有且只有一个 “真相来源(Single Source of Truth)”。
如果逻辑分散在 View、Model 和 Signal 中,你永远无法确定“是谁在什么时候改了我的数据”。
在本指南中,Service 层就是系统唯一的逻辑大脑。
架构流程对比
❌ 传统混乱模式(多源触发)
数据流向是杂乱交织的,容易产生副作用。
查看Mermaid源码
graph TD
UI[用户操作] --> View[View/Serializer]
View --> M1[模型A save]
M1 --> Sig1[Signal 信号]
Sig1 --> M2[模型B update]
M2 --> Sig2[Signal 另一个信号]
Sig2 --> M1
View -.-> Other[第三方接口]
✅ Service 模式(单向且集中)
数据流向是垂直且清晰的,一切皆可预测。
查看Mermaid源码
graph TD
UI[用户操作] --> Interface[API/View 接口层]
Interface --> Service[Service 业务核心层]
subgraph Core[核心业务大脑]
Service --> M1[修改模型A]
Service --> M2[修改模型B]
Service --> Third[调用第三方接口]
end
M1 --> DB[(数据库)]
M2 --> DB
Cookie Cutter(项目模板)
我们建议使用某种 cookiecutter 来开始每个新项目。从一开始就拥有适当的结构会带来回报。
🔖 专有名词:Cookiecutter
Cookiecutter 是一个用于从项目模板创建项目的命令行工具。它可以快速搭建出具有标准结构的新项目。
几个示例:
你可以使用
Styleguide-Example项目作为起点。你也可以使用
cookiecutter-django,因为它内部有大量好东西。或者你可以创建适合你情况的东西,并将其转变为一个 cookiecutter 项目。
💡 项目模板的价值
使用项目模板的好处:
节省时间:避免重复配置相同的东西
标准化:团队成员都使用相同的结构
最佳实践:模板通常包含社区验证的最佳实践
减少错误:避免配置错误
建议:
小团队:使用现有的成熟模板
大团队/公司:创建自己的定制模板
📖 文档结构说明
本翻译文档被拆分为以下文件:
00_目录和简介.md(当前文件)- 目录、简介和核心概念
01_模型层.md - 完整的 Models 章节翻译
02_服务层和选择器.md - 完整的 Services 和 Selectors 章节翻译
03_API和序列化器.md - 完整的 APIs 和 Serializers 章节翻译
04_URL和配置.md - URLs 和 Settings 章节翻译
05_错误和异常处理.md - 完整的错误处理章节翻译
06_测试和Celery.md - Testing 和 Celery 章节翻译
07_实用技巧和资源.md - Cookbook、DX 和其他资源
每个文件都包含:
✅ 完整的内容翻译
✅ 详细的代码示例
✅ 中文注解和说明
✅ 难点解释
✅ 最佳实践建议
📝 结语
本文档介绍了 Django Styleguide 的核心理念和使用方式。主要要点:
灵活应用:根据项目实际情况选择合适的实践
分离关注点:将业务逻辑、数据访问、API 接口分开
清晰的架构:Services 负责写入,Selectors 负责读取
避免常见陷阱:不要将业务逻辑分散在多处
使用项目模板:从标准结构开始新项目
接下来的文档将详细介绍每个部分的具体实现。建议按顺序阅读,以获得完整的理解。
👉 下一章:模型层 (Models)
翻译日期: 2026 原文版本: Latest (as of translation date) 翻译质量: 完整翻译,包含所有原文内容和代码示例 维护者: 社区贡献