看起来你在问数据模型和领域模型的区别 - 后者是你的最终用户所感知的业务逻辑和实体的位置,前者是你实际存储数据的位置。
此外,我把你问题的第三部分解释为:如何发现未能将这些模型分开的失败。
这是两个非常不同的概念,很难将它们分开。然而,有一些常见的模式和工具可以用于此目的。
关于领域模型
首先,你需要认识到你的领域模型实际上并不是关于数据的;它关乎“动作”和“问题”,例如“激活这个用户”、“停用这个用户”、“哪些用户当前处于激活状态?”以及“这个用户的名字是什么?”。从经典角度来看:它关乎查询和命令。
思考命令
让我们从你的例子中看看命令:“激活这个用户”和“停用这个用户”。命令的好处是它们可以通过小的给定-当-然场景轻松地表达:
假设有一个未激活的用户
当管理员激活此用户时
那么该用户将变为活跃状态
并且还会向该用户发送确认电子邮件
以及系统日志中添加一条记录
(等等等等)
这种情景非常有用,可以看到单个命令如何影响基础架构的不同部分 - 在这种情况下是您的数据库(某种“活动”标志),您的邮件服务器,您的系统日志等。
这种情景也真正帮助您设置面向测试驱动开发的环境。
最后,思考命令确实有助于创建面向任务的应用程序。 您的用户会感激这点:-)
表达命令
Django提供了两种轻松表达命令的方式; 两种方法都是有效的选择,并且混合使用这两种方法并不罕见。
服务层
服务模块已由@Hedde进行了描述。在这里,您定义一个单独的模块,每个命令表示为函数。
services.py
def activate_user(user_id):
user = User.objects.get(pk=user_id)
user.active = True
user.save()
send_mail(...)
使用表单
另一种方法是为每个命令使用Django表单。我更喜欢这种方法,因为它结合了多个密切相关的方面:
- 执行命令(做什么?)
- 验证命令参数(是否能够执行?)
- 呈现命令(如何执行?)
forms.py
class ActivateUserForm(forms.Form):
user_id = IntegerField(widget = UsernameSelectWidget, verbose_name="Select a user to activate")
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")
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)
user.active = True
user.save()
send_mail(...)
思考查询
您的示例中没有包含任何查询,所以我自作主张编写了一些有用的查询。我更喜欢使用术语“问题”,但查询是经典术语。有趣的查询包括:“这个用户的名字是什么?”,“这个用户能登录吗?”,“显示已停用用户列表”,以及“已停用用户的地理分布是什么?”
在回答这些查询之前,您应该始终问自己这个问题,这是:
- 一个仅供模板使用的 展示性 查询,和/或
- 与执行命令相关的 业务逻辑 查询,和/或
- 一个 报告 查询。
展示性查询仅仅是为了改善用户界面。业务逻辑查询的答案直接影响您命令的执行。报告查询仅仅是为了分析目的,并且具有较松散的时间限制。这些类别并不是互相排斥的。
另一个问题是:“我对答案有完全控制吗?”例如,在查询用户的名称(在此上下文中)时,我们对结果没有任何控制,因为我们依赖于外部 API。
创建查询
在Django中最基本的查询是使用Manager对象:
User.objects.filter(active=True)
当然,这仅在数据实际上被表示在数据模型中时才有效。并非总是如此。在这种情况下,您可以考虑以下选项。
自定义标签和过滤器
第一个选择对于仅呈现查询很有用:自定义标签和模板过滤器。
template.html
<h1>Welcome, {{ user|friendly_name }}</h1>
template_tags.py
@register.filter
def friendly_name(user):
return remote_api.get_cached_name(user.id)
查询方法
如果你的查询不仅仅是呈现,你可以在你的 services.py 中添加查询(如果你正在使用它),或者引入一个 queries.py 模块:
queries.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。
models.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
查询模型
对于本质上复杂但经常执行的查询,可以使用查询模型。查询模型是一种反规范化形式,其中单个查询的相关数据存储在单独的模型中。当然,关键是要保持反规范化模型与主模型同步。只有在完全掌控更改时才能使用查询模型。
models.py
class InactiveUserDistribution(models.Model):
country = CharField(max_length=200)
inactive_user_count = IntegerField(default=0)
第一个选项是在您的命令中更新这些模型。如果这些模型只被一个或两个命令更改,这非常有用。
forms.py
class ActivateUserForm(forms.Form):
def execute(self):
query_model = InactiveUserDistribution.objects.get_or_create(country=user.country)
query_model.inactive_user_count -= 1
query_model.save()
更好的选择是使用自定义信号。这些信号当然由您的命令发出。信号的优点是您可以将多个查询模型与原始模型保持同步。此外,可以使用Celery或类似框架将信号处理卸载到后台任务中。
signals.py
user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])
forms.py
class ActivateUserForm(forms.Form):
def execute(self):
user_activated.send_robust(sender=self, user=user)
models.py
class InactiveUserDistribution(models.Model):
@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文档:信号
架构:领域驱动设计