python - 在Django中分离业务逻辑和数据访问




model-view-controller data-access-layer (6)

Django旨在轻松地用于传递网页。 如果你不适应这个,也许你应该使用另一种解决方案。

我在模型的控制器上编写模型(具有相同的接口)和其他模型的根或常用操作。 如果我需要从其他型号的操作我导入其控制器。

这种方法足以满足我和我的应用程序的复杂性。

Hedde的回应是一个例子,显示了django和python本身的灵活性。

无论如何非常有趣的问题!

我正在Django编写一个项目,我发现80%的代码位于models.py文件models.py 。 这段代码令人困惑,在一段时间后,我停止了解真正发生的事情。

这是困扰我的事情:

  1. 我发现我的模型级别(应该只负责处理数据库中的数据)还发送电子邮件,将API用于其他服务等等,这很难看。
  2. 另外,我发现在视图中放置业务逻辑是不可接受的,因为这样很难控制。 例如,在我的应用程序中,至少有三种方法可以创建User新实例,但从技术上讲,它应该统一创建它们。
  3. 当我的模型的方法和属性变得不确定时,以及当它们产生副作用时,我并不总是注意到。

这是一个简单的例子。 起初, User模型是这样的:

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])

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

  1. 我的数据库实体,数据库级别:什么包含我的应用程序?
  2. 我的应用程序的实体,业务逻辑级别:什么可以使我的应用程序?

实施这种可以在Django中应用的方法的良好实践是什么?


Django采用了稍微改进的MVC。 在Django中没有“控制器”的概念。 最接近的代理是一个“视图”,这往往会导致与MVC转换混淆,因为在MVC中,视图更像是Django的“模板”。

在Django中,“模型”不仅仅是一个数据库抽象。 在某些方面,它与Django作为MVC控制者的“观点”分担责任。 它拥有与实例相关的所有行为。 如果该实例需要与外部API交互作为其行为的一部分,那么这仍然是模型代码。 实际上,模型根本不需要与数据库进行交互,因此您可以设想具有完全作为外部API的交互层存在的模型。 这是一个“模型”更自由的概念。


在Django中,正如Chris Pratt所说,MVC结构与其他框架中使用的经典MVC模型不同,我认为这样做的主要原因是避免了过于严格的应用程序结构,就像CakePHP等其他MVC框架中发生的一样。

在Django中,MVC的实现方式如下:

视图图层被分成两部分。 这些视图只能用于管理HTTP请求,它们会被调用并响应它们。 视图与您的其他应用程序(表单,模型,自定义类,以及直接使用模型的简单情况)进行通信。 要创建我们使用模板的界面。 模板类似于Django的字符串,它将一个上下文映射到它们中,并且该上下文被应用程序传递给视图(当视图询问时)。

模型层提供封装,抽象,验证,智能,并使您的数据面向对象(他们说有一天DBMS也会)。 这并不意味着你应该制作巨大的models.py文件(事实上,一个非常好的建议是将你的模型分成不同的文件,把它们放到一个名为“模型”的文件夹中,将一个'__init__.py'文件放入这个文件中文件夹,您可以导入所有模型,最后使用models.Model类的属性'app_label')。 模型应该使你的数据操作抽象化,它会使你的应用程序更简单。 您还应该根据需要为模型创建外部类,例如“工具”。您还可以在模型中使用遗产,将模型的Meta类的“抽象”属性设置为“真”。

其余的在哪里? 那么,小型Web应用程序通常是一种数据接口,在一些使用视图来查询或插入数据的小程序情况下就足够了。 更常见的情况将使用Forms或ModelForms,它们实际上是“控制器”。 这不仅仅是一个解决常见问题的实际解决方案,而且是一个非常快速的解决方案。 这是一个网站用来做什么。

如果Forms不是为你编写的,那么你应该创建自己的类来完成这个魔术,这是一个非常好的例子:管理员应用程序:你可以读取ModelAmin代码,这实际上是一个控制器。 没有标准的结构,我建议你检查现有的Django应用程序,这取决于每种情况。 这就是Django开发者想要的,你可以添加xml解析器类,一个API连接器类,添加Celery执行任务,为基于反应器的应用程序扭曲,仅使用ORM,创建Web服务,修改管理应用程序等等。 ..您的责任是制定高质量的代码,尊重MVC理念,使其基于模块并创建自己的抽象层。 它非常灵活。

我的建议:尽可能多地阅读代码,周围有很多django应用程序,但不要认真对待它们。 每个案例都是不同的,模式和理论有所帮助,但并非总是如此,这是一种不准确的科学,django只是为您提供了很好的工具,可以用来缓解一些痛苦(如管理界面,Web表单验证,国际化,观察者模式实施,所有前面提到的和其他的),但好的设计来自经验丰富的设计师。

PS:使用auth应用中的'User'类(来自标准的django),你可以创建例如用户配置文件,或者至少读取它的代码,这对你的情况很有用。


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

此外,我已经将问题的第三部分解释为:如何注意未能保持这些模型分离。

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

关于领域模型

你需要认识的第一件事是你的领域模型不是关于数据的; 它是关于诸如“激活此用户”,“关闭此用户”,“当前激活哪些用户?”和“此用户名是什么?”等操作问题 。 用经典术语来说:它是关于查询命令的

在命令中思考

我们先看看示例中的命令:“激活此用户”和“停用此用户”。 关于命令的好处在于,它们可以很容易地用小时给定的场景来表示:

一个不活跃的用户
管理员激活这个用户
那么用户变得活跃
并向用户发送确认电子邮件
并将一个条目添加到系统日志中
(等等)

这种情况对于查看基础架构的不同部分如何受到单个命令的影响很有用 - 在这种情况下,您的数据库(某种“活动”标志),邮件服务器,系统日志等。

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

最后,在命令中思考确实可以帮助您创建一个面向任务的应用程序。 你的用户会喜欢这个:-)

表达命令

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

  • 命令的执行(它有什么作用?)
  • 命令参数验证(它可以做到这一点?)
  • 演示命令(我该怎么做?)

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

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

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

架构:领域驱动设计


我通常在视图和模型之间实现服务层。 这就像你的项目的API一样,给你一个很好的直升机视图。 我从我的一位同事那里继承了这个实践,这个实践在Java项目(JSF)中使用了这种分层技术,例如:

models.py

class Book:
   author = models.ForeignKey(User)
   title = models.CharField(max_length=125)

   class Meta:
       app_label = "library"

services.py

from library.models import Book

def get_books(limit=None, **filters):
    """ simple service function for retrieving books can be widely extended """
    if limit:
        return Book.objects.filter(**filters)[:limit]
    return Book.objects.filter(**filters)

views.py

from library.services import get_books

class BookListView(ListView):
    """ simple view, e.g. implement a _build and _apply filters function """
    queryset = get_books()

请注意,我通常将模型,视图和服务提供给模块级别,并根据项目的规模进一步分离


首先, 不要重复自己

然后,请小心不要过度工作,有时这只是浪费时间,并会让某人失去对重要事物的关注。 不时检阅python禅宗

看看活跃的项目

  • 更多的人=更需要妥善组织
  • 他们有一个简单的结构django存储库
  • 他们有一个straigtforward目录结构的点信息库
  • 面料库也是一个很好的看看。

    • 您可以将所有模型放置在yourapp/models/logicalgroup.py
  • 例如UserGroup和相关模型可以在yourapp/models/users.py
  • 例如PollQuestionAnswer ......可以在yourapp/models/polls.py
  • yourapp/models/__init__.py中的__all__加载所需的yourapp/models/__init__.py

更多关于MVC

  • 模型是你的数据
    • 这包括您的实际数据
    • 这还包括你的session / cookie / cache / fs / index数据
  • 用户与控制器交互操作模型
    • 这可能是一个API或保存/更新数据的视图
    • 这可以通过request.GET / request.POST等来调整
    • 认为寻呼过滤
  • 数据更新视图
    • 模板将采集数据并对其进行格式化
    • 甚至没有模板的API也是视图的一部分; 如tastypiepiston
    • 这也应该考虑到中间件。

利用middleware / templatetags

  • 如果您需要为每个请求完成一些工作,则中间件是一种可行的方法。
    • 例如添加时间戳
    • 例如更新关于页面点击的度量
    • 例如填充缓存
  • 如果您的代码片段始终会重复用于格式化对象,那么templatetags是很好的选择。
    • 例如活动标签页/网址面包屑

充分利用manager

  • 创建User可以进入UserManager(models.Manager)
  • 实例的血统细节应该放在models.Modelmodels.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 documentation非常好。 如果您有很多定制(或者有时为了更高级的用途,有时候避免循环导入错误),将表单的代码与模型代码分开可能会更好。

尽可能使用管理命令

  • 例如yourapp/management/commands/createsuperuser.py
  • 例如yourapp/management/commands/activateinbulk.py

如果你有商业逻辑,你可以将它分开

  • django.contrib.auth 使用后端 ,就像db有后端...等。
  • 为您的业务逻辑添加一个setting (例如AUTHENTICATION_BACKENDS
  • 你可以使用django.contrib.auth.backends.RemoteUserBackend
  • 你可以使用yourapp.backends.remote_api.RemoteUserBackend
  • 你可以使用yourapp.backends.memcached.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

更多关于设计模式

更多关于界面边界

  • 你想使用的代码是模型的一部分吗? - > yourapp.models
  • 代码是业务逻辑的一部分吗? - > yourapp.vendor
  • 代码是通用工具/库的一部分吗? - > yourapp.libs
  • 代码是业务逻辑库的一部分吗? - > yourapp.libs.vendoryourapp.vendor.libs
  • 这是一个很好的例子:你能独立地测试你的代码吗?
    • 对很好 :)
    • 不,你可能有接口问题
    • 当明确的分离时,单元测试应该是一个轻松使用嘲笑
  • 分离是合乎逻辑的吗?
    • 对很好 :)
    • 不,您可能无法单独测试这些逻辑概念。
  • 当您获得10倍以上的代码时,您是否认为需要重构?
    • 是的,没有好的,没有bueno,重构可能是很多工作
    • 不,这太棒了!

总之,你可以拥有

  • 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

或其他任何可以帮助你的东西; 找到你需要接口边界将帮助你。





business-logic-layer