我正在用Django写一个项目,我看到80%的代码都在models.py文件中。这段代码令人困惑,过了一段时间后,我就不明白到底发生了什么。

这是困扰我的问题:

I find it ugly that my model level (which was supposed to be responsible only for the work with data from a database) is also sending email, walking on API to other services, etc. Also, I find it unacceptable to place business logic in the view, because this way it becomes difficult to control. For example, in my application there are at least three ways to create new instances of User, but technically it should create them uniformly. I do not always notice when the methods and properties of my models become non-deterministic and when they develop side effects.

这里有一个简单的例子。一开始,用户模型是这样的:

class User(db.Models):

    def get_present_name(self):
        return self.name or 'Anonymous'

    def activate(self):
        self.status = 'activated'
        self.save()

随着时间的推移,它变成了这样:

class User(db.Models):

    def get_present_name(self): 
        # property became non-deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 

    def activate(self):
        # method now has a side effect (send message to user)
        self.status = 'activated'
        self.save()
        send_mail('Your account is activated!', '…', [self.email])

我想要的是在代码中分离实体:

数据库级实体,即数据库级逻辑:我的应用程序存储什么样的数据? 应用程序级实体,即业务级逻辑:我的应用程序做什么?

在Django中实现这样一种方法有哪些好的实践呢?


当前回答

似乎您在询问数据模型和领域模型之间的区别——后者是您可以找到最终用户感知到的业务逻辑和实体的地方,而前者是您实际存储数据的地方。

此外,我将你问题的第三部分解释为:如何注意到未能将这些模型分开。

这是两个完全不同的概念,很难把它们分开。但是,有一些常见的模式和工具可用于此目的。

关于领域模型

您需要认识到的第一件事是您的领域模型实际上与数据无关;它是关于“激活该用户”、“禁用该用户”、“当前激活哪些用户?”和“该用户的名称是什么?”等操作和问题。用经典的术语来说:它是关于查询和命令的。

命令思维

让我们从示例中的命令开始:“activate this user”和“deactivate this user”。命令的好处是它们可以很容易地用小的given-when-then场景来表示:

给定一个不活跃的用户 当管理员激活该用户时 然后用户变成活动用户 并向用户发送确认电子邮件 并将一个条目添加到系统日志中 (等等)。

这样的场景有助于了解单个命令如何影响基础设施的不同部分——在这种情况下,您的数据库(某种“活动”标志)、邮件服务器、系统日志等。

这样的场景也能真正帮助你建立一个测试驱动开发环境。

最后,用命令思考确实有助于创建面向任务的应用程序。你的用户会喜欢的:-)

表达命令

Django提供了两种简单的命令表达方式;它们都是有效的选择,混合使用这两种方法并不罕见。

服务层

@Hedde已经描述了服务模块。这里定义了一个单独的模块,每个命令都表示为一个函数。

services.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 Form。我更喜欢这种方法,因为它结合了多个密切相关的方面:

命令的执行(它做什么?) 命令参数的验证(它能做到吗?) 命令的表示(我如何做到这一点?)

forms.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)

当然,这只有在数据模型中实际表示数据时才有效。但情况并非总是如此。在这些情况下,您可以考虑以下选项。

自定义标记和过滤器

第一种方法对于仅仅是表示的查询很有用:自定义标记和模板过滤器。

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):
    # 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()

更好的选择是使用自定义信号。这些信号当然是由您的命令发出的。信号的优点是可以使多个查询模型与原始模型保持同步。此外,信号处理可以使用芹菜或类似的框架卸载到后台任务。

signals.py

user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])

forms.py

class ActivateUserForm(forms.Form):
    # see above
   
    def execute(self):
        # see above
        user_activated.send_robust(sender=self, user=user)

models.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文档:信号

架构:领域驱动设计

其他回答

这是一个老问题,但我还是想提出我的解决方案。它是基于这样一种接受:模型对象也需要一些额外的功能,而把它放在models.py中是很尴尬的。重业务逻辑可以根据个人喜好单独编写,但我至少喜欢模型做与自身相关的所有事情。该解决方案还支持那些喜欢将所有逻辑置于模型本身中的人。

因此,我设计了一个hack,允许我将逻辑从模型定义中分离出来,并且仍然可以从我的IDE中得到所有的提示。

优点应该是显而易见的,但下面列出了我观察到的一些优点:

DB定义仍然是那样-没有逻辑“垃圾”附加 与模型相关的逻辑都整齐地放在一个地方 所有服务(表单、REST、视图)都有一个逻辑访问点 最棒的是:当我意识到我的models.py变得太杂乱,不得不分离逻辑时,我不需要重写任何代码。分离是平滑和迭代的:我可以一次处理一个函数,也可以一次处理整个类或整个models.py。

我一直在使用Python 3.4及更高版本和Django 1.8及更高版本。

app / models.py

....
from app.logic.user import UserLogic

class User(models.Model, UserLogic):
    field1 = models.AnyField(....)
    ... field definitions ...

应用程序/逻辑/ user.py

if False:
    # This allows the IDE to know about the User model and its member fields
    from main.models import User

class UserLogic(object):
    def logic_function(self: 'User'):
        ... code with hinting working normally ...

我唯一想不明白的是如何让我的IDE(在这种情况下是PyCharm)识别UserLogic实际上是用户模型。但由于这显然是一种hack,我很乐意接受总是为self参数指定类型的小麻烦。

似乎您在询问数据模型和领域模型之间的区别——后者是您可以找到最终用户感知到的业务逻辑和实体的地方,而前者是您实际存储数据的地方。

此外,我将你问题的第三部分解释为:如何注意到未能将这些模型分开。

这是两个完全不同的概念,很难把它们分开。但是,有一些常见的模式和工具可用于此目的。

关于领域模型

您需要认识到的第一件事是您的领域模型实际上与数据无关;它是关于“激活该用户”、“禁用该用户”、“当前激活哪些用户?”和“该用户的名称是什么?”等操作和问题。用经典的术语来说:它是关于查询和命令的。

命令思维

让我们从示例中的命令开始:“activate this user”和“deactivate this user”。命令的好处是它们可以很容易地用小的given-when-then场景来表示:

给定一个不活跃的用户 当管理员激活该用户时 然后用户变成活动用户 并向用户发送确认电子邮件 并将一个条目添加到系统日志中 (等等)。

这样的场景有助于了解单个命令如何影响基础设施的不同部分——在这种情况下,您的数据库(某种“活动”标志)、邮件服务器、系统日志等。

这样的场景也能真正帮助你建立一个测试驱动开发环境。

最后,用命令思考确实有助于创建面向任务的应用程序。你的用户会喜欢的:-)

表达命令

Django提供了两种简单的命令表达方式;它们都是有效的选择,混合使用这两种方法并不罕见。

服务层

@Hedde已经描述了服务模块。这里定义了一个单独的模块,每个命令都表示为一个函数。

services.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 Form。我更喜欢这种方法,因为它结合了多个密切相关的方面:

命令的执行(它做什么?) 命令参数的验证(它能做到吗?) 命令的表示(我如何做到这一点?)

forms.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)

当然,这只有在数据模型中实际表示数据时才有效。但情况并非总是如此。在这些情况下,您可以考虑以下选项。

自定义标记和过滤器

第一种方法对于仅仅是表示的查询很有用:自定义标记和模板过滤器。

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):
    # 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()

更好的选择是使用自定义信号。这些信号当然是由您的命令发出的。信号的优点是可以使多个查询模型与原始模型保持同步。此外,信号处理可以使用芹菜或类似的框架卸载到后台任务。

signals.py

user_activated = Signal(providing_args = ['user'])
user_deactivated = Signal(providing_args = ['user'])

forms.py

class ActivateUserForm(forms.Form):
    # see above
   
    def execute(self):
        # see above
        user_activated.send_robust(sender=self, user=user)

models.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文档:信号

架构:领域驱动设计

首先,不要重复你自己。

然后,请注意不要过度设计,有时只是浪费时间,让人失去对重要事情的关注。时不时复习一下python的禅意。

看一看活跃的项目

more people = more need to organize properly the django repository they have a straightforward structure. the pip repository they have a straigtforward directory structure. the fabric repository is also a good one to look at. you can place all your models under yourapp/models/logicalgroup.py e.g User, Group and related models can go under yourapp/models/users.py e.g Poll, Question, Answer ... could go under yourapp/models/polls.py load what you need in __all__ inside of yourapp/models/__init__.py

更多关于MVC

model is your data this includes your actual data this also includes your session / cookie / cache / fs / index data user interacts with controller to manipulate the model this could be an API, or a view that saves/updates your data this can be tuned with request.GET / request.POST ...etc think paging or filtering too. the data updates the view the templates take the data and format it accordingly APIs even w/o templates are part of the view; e.g. tastypie or piston this should also account for the middleware.

利用中间件/模板标签

如果您需要为每个请求做一些工作,中间件是一种方法。 例如,添加时间戳 例如,更新页面点击率的指标 例如,填充缓存 如果你的代码片段总是在格式化对象时重复出现,那么templatetags是很好的选择。 例如,active TAB / url面包屑

利用模型管理器

创建用户可以进入用户管理器(models.Manager)。 实例的血淋淋的细节应该放在models.Model上。 queryset的详细信息可以放到models.Manager中。 你可能想一次创建一个User,所以你可能认为它应该存在于模型本身,但在创建对象时,你可能没有所有的细节:

例子:

class UserManager(models.Manager):
   def create_user(self, username, ...):
      # plain create
   def create_superuser(self, username, ...):
      # may set is_superuser field.
   def activate(self, username):
      # may use save() and send_mail()
   def activate_in_bulk(self, queryset):
      # may use queryset.update() instead of save()
      # may use send_mass_mail() instead of send_mail()

尽可能使用表单

如果您有映射到模型的表单,那么可以省去大量的样板代码。ModelForm文档非常好。如果您有很多自定义(或者有时为了更高级的用途避免循环导入错误),那么将表单代码与模型代码分离是很好的。

尽可能使用管理命令

例如yourapp /管理/命令/ createsuperuser.py 例如yourapp /管理/命令/ activateinbulk.py

如果您有业务逻辑,您可以将其分离出来

Django.contrib.auth使用后端,就像db有后端一样…等等。 为您的业务逻辑添加一个设置(例如AUTHENTICATION_BACKENDS) 你可以使用django.contrib.auth.backends.RemoteUserBackend 你可以使用yourapp.backend .remote_api. remoteuserbackend 你可以使用yourapp.backend .memcache . remoteuserbackend 将困难的业务逻辑委托给后端 确保在输入/输出上设置正确的期望。 更改业务逻辑就像更改设置一样简单:)

后端例子:

class User(db.Models):
    def get_present_name(self): 
        # property became not deterministic in terms of database
        # data is taken from another service by api
        return remote_api.request_user_name(self.uid) or 'Anonymous' 

可能成为:

class User(db.Models):
   def get_present_name(self):
      for backend in get_backends():
         try:
            return backend.get_present_name(self)
         except: # make pylint happy.
            pass
      return None

关于设计模式的更多信息

关于设计模式已经有了一个很好的问题 一个关于实用设计模式的很好的视频 Django的后端明显使用了委托设计模式。

关于接口边界的更多信息

Is the code you want to use really part of the models? -> yourapp.models Is the code part of business logic? -> yourapp.vendor Is the code part of generic tools / libs? -> yourapp.libs Is the code part of business logic libs? -> yourapp.libs.vendor or yourapp.vendor.libs Here is a good one: can you test your code independently? yes, good :) no, you may have an interface problem when there is clear separation, unittest should be a breeze with the use of mocking Is the separation logical? yes, good :) no, you may have trouble testing those logical concepts separately. Do you think you will need to refactor when you get 10x more code? yes, no good, no bueno, refactor could be a lot of work no, that's just awesome!

简而言之,你本可以

yourapp/core/backends.py yourapp/core/models/__init__.py yourapp/core/models/users.py yourapp/core/models/questions.py yourapp/core/backends.py yourapp/core/forms.py yourapp/core/handlers.py yourapp/core/management/commands/__init__.py yourapp/core/management/commands/closepolls.py yourapp/core/management/commands/removeduplicates.py yourapp/core/middleware.py yourapp/core/signals.py yourapp/core/templatetags/__init__.py yourapp/core/templatetags/polls_extras.py yourapp/core/views/__init__.py yourapp/core/views/users.py yourapp/core/views/questions.py yourapp/core/signals.py yourapp/lib/utils.py yourapp/lib/textanalysis.py yourapp/lib/ratings.py yourapp/vendor/backends.py yourapp/vendor/morebusinesslogic.py yourapp/vendor/handlers.py yourapp/vendor/middleware.py yourapp/vendor/signals.py yourapp/tests/test_polls.py yourapp/tests/test_questions.py yourapp/tests/test_duplicates.py yourapp/tests/test_ratings.py

或者其他对你有帮助的东西;找到你需要的接口和界限会对你有帮助。

最全面的关于不同选择的利弊的文章:

想法1:胖模特 想法#2:将业务逻辑放在视图/表单中 理念#3:服务 想法4:QuerySets/Managers 结论

来源: https://sunscrapers.com/blog/where-to-put-business-logic-django/

Django使用了一种稍微修改过的MVC。在Django中没有“控制器”的概念。最接近的代理是一个“视图”,它容易引起MVC转换的混淆,因为在MVC中,视图更像是Django的“模板”。

In Django, a "model" is not merely a database abstraction. In some respects, it shares duty with the Django's "view" as the controller of MVC. It holds the entirety of behavior associated with an instance. If that instance needs to interact with an external API as part of it's behavior, then that's still model code. In fact, models aren't required to interact with the database at all, so you could conceivable have models that entirely exist as an interactive layer to an external API. It's a much more free concept of a "model".