您似乎在询问数据模型和域模型之间的区别——后者是您可以找到最终用户感知的业务逻辑和实体的地方,前者是您实际存储数据的地方。
此外,我将您问题的第三部分解释为:如何注意到未能将这些模型分开。
这是两个非常不同的概念,总是很难将它们分开。但是,有一些常见的模式和工具可用于此目的。
关于领域模型
您需要认识到的第一件事是您的域模型并不是真正的数据;它是关于诸如“激活该用户”、“停用该用户”、“当前激活了哪些用户?”和“该用户的名字是什么?”之类的操作和问题。用经典术语来说:它是关于查询和命令的。
在命令中思考
让我们从查看示例中的命令开始:“激活此用户”和“停用此用户”。命令的好处是它们可以很容易地用小的给定时间场景来表达:
给定一个非活动用户
,当管理员激活该用户
时,该用户变为活动状态
,并向该用户发送一封确认电子邮件,
并将一个条目添加到系统日志中
(等等)
这样的场景有助于查看单个命令如何影响基础设施的不同部分——在这种情况下,您的数据库(某种“活动”标志)、邮件服务器、系统日志等。
这样的场景也确实可以帮助您设置测试驱动的开发环境。
最后,在命令中思考确实可以帮助您创建面向任务的应用程序。您的用户会欣赏这一点 :-)
表达命令
Django 提供了两种简单的命令表达方式;它们都是有效的选择,混合使用这两种方法并不罕见。
服务层
@Hedde已经描述了服务模块。在这里,您定义了一个单独的模块,每个命令都表示为一个函数。
服务.py
def activate_user(user_id):
user = User.objects.get(pk=user_id)
# set active flag
user.active = True
user.save()
# mail user
send_mail(...)
# etc etc
使用表格
另一种方法是为每个命令使用 Django 表单。我更喜欢这种方法,因为它结合了多个密切相关的方面:
- 命令的执行(它做了什么?)
- 验证命令参数(可以这样做吗?)
- 命令的介绍(我该怎么做?)
表格.py
class ActivateUserForm(forms.Form):
user_id = IntegerField(widget = UsernameSelectWidget, verbose_name="Select a user to activate")
# the username select widget is not a standard Django widget, I just made it up
def clean_user_id(self):
user_id = self.cleaned_data['user_id']
if User.objects.get(pk=user_id).active:
raise ValidationError("This user cannot be activated")
# you can also check authorizations etc.
return user_id
def execute(self):
"""
This is not a standard method in the forms API; it is intended to replace the
'extract-data-from-form-in-view-and-do-stuff' pattern by a more testable pattern.
"""
user_id = self.cleaned_data['user_id']
user = User.objects.get(pk=user_id)
# set active flag
user.active = True
user.save()
# mail user
send_mail(...)
# etc etc
在查询中思考
您的示例不包含任何查询,因此我冒昧地提出了一些有用的查询。我更喜欢使用术语“问题”,但查询是经典术语。有趣的查询是:“这个用户的名字是什么?”、“这个用户可以登录吗?”、“给我显示停用用户的列表”和“停用用户的地理分布是什么?”
在开始回答这些问题之前,您应该始终问自己这个问题,是这样的:
- 仅针对我的模板的表示查询,和/或
- 与执行我的命令相关的业务逻辑查询,和/或
- 报告查询。
呈现性查询仅用于改进用户界面。业务逻辑查询的答案直接影响命令的执行。报告查询仅用于分析目的,并且具有较宽松的时间限制。这些类别并不相互排斥。
另一个问题是:“我可以完全控制答案吗?” 例如,当查询用户名时(在此上下文中),我们无法控制结果,因为我们依赖于外部 API。
查询
Django 中最基本的查询是使用 Manager 对象:
User.objects.filter(active=True)
当然,这仅在数据实际表示在您的数据模型中时才有效。这并非总是如此。在这些情况下,您可以考虑以下选项。
自定义标签和过滤器
第一个替代方案对于仅是展示性的查询很有用:自定义标签和模板过滤器。
模板.html
<h1>Welcome, {{ user|friendly_name }}</h1>
模板标签.py
@register.filter
def friendly_name(user):
return remote_api.get_cached_name(user.id)
查询方法
如果您的查询不仅仅是展示性的,您可以将查询添加到您的services.py(如果您正在使用它),或者引入一个queries.py模块:
查询.py
def inactive_users():
return User.objects.filter(active=False)
def users_called_publysher():
for user in User.objects.all():
if remote_api.get_cached_name(user.id) == "publysher":
yield user
代理模型
代理模型在业务逻辑和报告的上下文中非常有用。您基本上定义了模型的增强子集。您可以通过覆盖Manager.get_queryset()
方法来覆盖 Manager 的基本 QuerySet。
模型.py
class InactiveUserManager(models.Manager):
def get_queryset(self):
query_set = super(InactiveUserManager, self).get_queryset()
return query_set.filter(active=False)
class InactiveUser(User):
"""
>>> for user in InactiveUser.objects.all():
… assert user.active is False
"""
objects = InactiveUserManager()
class Meta:
proxy = True
查询模型
对于本质上复杂但经常执行的查询,可以使用查询模型。查询模型是一种非规范化形式,其中单个查询的相关数据存储在单独的模型中。诀窍当然是使非规范化模型与主模型保持同步。只有当更改完全在您的控制之下时,才能使用查询模型。
模型.py
class InactiveUserDistribution(models.Model):
country = CharField(max_length=200)
inactive_user_count = IntegerField(default=0)
第一个选项是在您的命令中更新这些模型。如果这些模型仅由一两个命令更改,这将非常有用。
表格.py
class ActivateUserForm(forms.Form):
# see above
def execute(self):
# see above
query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
query_model.inactive_user_count -= 1
query_model.save()
更好的选择是使用自定义信号。这些信号当然是由您的命令发出的。信号的优势在于您可以使多个查询模型与原始模型保持同步。此外,可以使用 Celery 或类似框架将信号处理卸载到后台任务。
信号.py
user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])
表格.py
class ActivateUserForm(forms.Form):
# see above
def execute(self):
# see above
user_activated.send_robust(sender=self, user=user)
模型.py
class InactiveUserDistribution(models.Model):
# see above
@receiver(user_activated)
def on_user_activated(sender, **kwargs):
user = kwargs['user']
query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
query_model.inactive_user_count -= 1
query_model.save()
保持清洁
使用这种方法时,很容易确定您的代码是否保持干净。只需遵循以下准则:
- 我的模型是否包含不仅仅管理数据库状态的方法?您应该提取一个命令。
- 我的模型是否包含不映射到数据库字段的属性?您应该提取一个查询。
- 我的模型是否引用了不是我的数据库的基础架构(例如邮件)?您应该提取一个命令。
视图也是如此(因为视图经常遇到同样的问题)。
- 我的视图是否主动管理数据库模型?您应该提取一个命令。
一些参考资料
Django 文档:代理模型
Django 文档:信号
架构:领域驱动设计