在Django Rest Framework中,模型方法中的业务逻辑应该放在哪里?

9

我有三个模型,它们之间存在一对多的关系。

  • 模型A可以有多个模型B的实例。
  • 模型A可以有多个模型C的实例。
  • 模型B可以有多个模型C的实例。

用户将创建模型A的一个实例(如股票组合),然后输入股票持仓(模型C)。模型B的作用是基于投资组合(模型A)中的股票(模型C)运行计算/逻辑,并使用另一个类/模型来跟踪事物,从而使生活更轻松。

最初,我在Django视图中拥有这些计算的逻辑,但在《Two Scoops of Django》中读到业务逻辑应与视图分离。因此,我将逻辑移到模型A的一个方法(投资组合)中,并从视图调用该方法。此逻辑循环遍历股票持仓并创建模型B的新实例,即结果。

现在,我有兴趣探索Django-rest-framework以提供API给JavaScript前端(如Angular)。我猜测我将无法在REST接口中进行此类数据操作。但是,此逻辑的结果(模型B中的数据)需要通过REST可见。因此,这种类型的计算/逻辑放在哪里?

2个回答

20

Django Rest Framework的主要部分是视图(ViewSets、ApiViews等)和序列化器(serializers),但这些都不是编写逻辑的理想场所。

正如你所提到的,在任何视图中编写逻辑都不好。为什么?

  1. 无法从应用程序的另一部分使用相同的逻辑
  2. 无法封装该逻辑(您需要调用该视图才能运行该逻辑)
  3. 无法对逻辑进行单元测试,因为它与视图耦合

在我看来,模型并不是编写逻辑的好地方。将模型视为数据库定义。我会尽可能保持它们简单。您可以重写“save”和其他方法来执行微不足道的任务。任何其他高级功能都应存储在其外部。

我可以想到两个更好的适合您需要的位置:

其中之一是Django信号(signal)

更好的一个是自定义类。将逻辑封装/解耦在您自己的类中(您可以使用静态或实例方法,没有关系),然后您将能够:

  1. 单元测试这些方法/模拟该类
  2. 在其他地方重用此逻辑
  3. 根据KISS原则简化您的代码
  4. 从信号中使用它,使信号代码简单
  5. 使用celery任务(如果您将来实施)而无需修改您的代码的任何一行

更新 代码组织示例。

从评论中可以清楚地看出,信号不起作用,因为分析操作将在用户请求时运行。如果该操作应在保存特定模型时自动运行,则可以使用信号。我假设您知道如何使用Django Rest Framework API视图或视图集、序列化器等。如果不知道,请提出另一个问题。这将更多是关于Python的解释。

在您的应用程序模块中,创建一个名为app_business_logic.py或其他您想要命名的文件。例如,您可以将其放置在与models.py相同的级别上,但这不是必须的:

class HoldingsAnalyser:
    # static method sample. Call it like this: "HoldingsAnalyser.run(..)"
    @staticmethod
    def run(holding_list):
        # do your model generation here or whatever you need
        return True # or whatever you need to return

    # instance method sample. Create an instance first and then call the method: 
    # analyser = HoldingsAnalyser()
    # analyser.run(...)
    def run(self, holding_list):
        # do your model generation here or whatever you need
        return True # or whatever you need to return

现在,在django-rest-framework的api视图或视图集中,创建一个POST方法,当客户端应用程序用户按下按钮(生成分析的按钮)时调用该方法:

from yourapp.app_business_logic import HoldingsAnalyser

class StockPortfolioViewSet(WhatEverMixingYouNeedToInheritFrom):
    serializer_class = whatever # look at the docs
    
    @detail_route(methods=['post'])
    def run_analysis(self, request, pk):
        stock_portfolio = get the object based on the pk
        holdings = make your query to get the holdings
        analysis_result = HoldingsAnalyser.run(holdings)
        if analysis_result:
            # everything ok
            return Response(status=status.HTTP_204_NO_CONTENT)
        else:
            return Response(a useful error for your client)
         

这个链接的URL应该类似于http://server.com/api_path/stockportfolio/21/run_analysis/,其中的21是指股票组合的ID。


感谢@xleon的回复。我仍然不确定所有这些(信号或自定义类)在项目中的位置。我设想用户将查看其投资组合(模型A)中的持股(模型C)。然后,他们将能够通过单击“分析”按钮来运行分析(循环遍历每个持股并创建模型B实例)。最终,所有3个模型都将通过REST API可用。如果用户尚未运行分析,则模型B的API将为空。如果分析逻辑在信号或自定义类中,则何时调用以填充模型B API? - shudoh
如果您通过用户操作运行分析,则Django信号不是选项(仅建议在保存其他模型时自动运行该分析时使用)。我将使用一些伪代码更新答案以适应您的工作流程。 - xleon
哇,这是一个很好的例子。我一直在想将业务逻辑放在models.py中是否合适。一些流行的出版物建议将其放在那里,但我一直有所疑虑。 - Nostalg.io
没错,很多Django专家都建议将那些逻辑放在models.py文件中。但我还是不太理解。 - xleon
1
这是一个不错的回答,经过在 Django 中创建几个项目后,我意识到模型是放置数据库特定逻辑的地方,而自定义类应该为您的业务逻辑创建。业务逻辑类始终可以使用模型类方法来实现其逻辑。这提供了关注点分离。Django 文档以及任何其他地方都建议将其放在 models.py 中,但我认为那是针对特定于数据库的逻辑,而不是业务逻辑。 - Tragaknight

0

我曾经为业务逻辑的放置位置苦恼了一段时间。我同意,模型应该是轻量级的。视图也应该如此。

在一个项目中,我有一个名为services.py的文件,里面有数千行函数来处理所有的逻辑。老实说,这完全没问题。

目前,我制作帮助类,我称之为“Logic”后缀。通常这些与模型一一对应(例如,我可能有一个Holdings模型和一个Portofolio模型。然后我会创建一个HoldingsLogicPortfolioLogic来处理这些模型中的内容。有时候事情跨越多个模型...也许你需要一个操作多个模型的PortfolioMetrcsLogic

不确定这是否比我以前的一个巨大的services.py文件更好,但迹象表明是的。 :)


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接