如何在Django中使用有限状态机操纵对象状态

236 阅读8分钟

在Django中使用有限状态机操纵对象状态

随着项目越来越大,向意大利语代码倾斜的趋势变得不可避免。避免这些复杂情况的方法之一是应用有限状态机(FSM)概念来处理对象的状态变化。

FSM帮助你简化代码,通过定义状态和它们之间的联系来避免大量的if-else条件。

在这篇文章中,我们将学习如何在运行时操作和改变对象的状态。

为了证明这一点,我们将建立一个具有基本CRUD操作的轻量级气缸跟踪系统。

内容列表

本教程将涵盖。

  • 什么是有限状态机?
  • 有限状态机(FSM)在Django中如何工作?
  • 选择改变状态的方法时需要注意的要点
  • 结论和建议

前提条件

要充分利用本教程,需要具备以下条件。

  • 对Python的基本了解。
  • 熟悉Django框架和Django Rest框架。
  • 熟悉Django rest框架的可浏览API界面。
  • 安装了PyCharm专业代码编辑器。
  • 快速浏览[计算理论]和[正则表达式]的[介绍]

什么是有限状态机?

有限状态机(FSM)是一个在面向对象编程中促进对象动态化的系统。

其理念是,对象每次只能承担一种状态。最流行的、最相关的例子是交通灯。

在任何时候,不管一个路口有多少个交通灯,每个板块一次只能有一个灯,否则会导致混乱。

在本教程中,我们将使用Django中构建的CynTrack 应用程序来实现对象状态的转换,以解释FSM的工作原理。

Django中的有限状态机是如何工作的?

CynTrack 是一个简单的应用程序,它根据检查时谁是占有者来跟踪一个圆筒。

随着圆柱体的移动,拥有者也会随之改变,而新的拥有者总是被记录在圆柱体对象中。

How FSM works in Django image

让我们深入了解一下实现的过程。

为了创建这个系统,我们将通过四个步骤进行工作。

  1. 定义这个对象所能承担的状态
  2. 创建该对象的模型
  3. 定义对象的状态之间的转换
  4. 实现负责这种转换的视图

我们想假设读者已经熟悉了Django项目的快速设置。然而,下面将分享快速设置一个项目的命令。

django-admin startproject project .
python manage.py startapp tracker

你可以在这里阅读更多关于设置Django项目的信息。

在我们创建对象的模型类之前,我们需要安装django-fsm 库。

你可以使用下面的命令来完成这个工作。

pip install django-fsm

这就是定义状态和创建对象后的圆柱体对象的样子。

from django.db import models
from django_fsm import FSMField, transition

LOCATION = (
    ('with-retailer', 'with-retailer'),
    ('with-dispatch', 'with-dispatch'),
    ('with-user', 'with-user')
)

class Cylinder(models.Model):
    cylinder_number = models.CharField(max_length=20)
    assigned_on = models.DateTimeField(auto_now=True)
    created_on = models.DateTimeField(auto_now_add=True)
    assigned_to = FSMField(choices=LOCATION, default='with-retailer', protected=True)

    def __str__(self):
        return self.cylinder_number

从上面的代码片段中,我们看到一个名为assigned_to 的字段,用于检查气缸的当前位置。

它被分配了一个带有以下参数的FSMField

  • choices - 任何圆柱体可以承担的状态列表
  • default - 圆柱体对象在创建时承担的初始状态
  • protected - 初始化为 ;这意味着除非它们被定义,否则状态不能被改变。True

现在,我们可以使用@transition 装饰器为任何给定的气缸定义不同状态之间的转换。

请看下面的内容。

from django.db import models
from django_fsm import FSMField, transition

LOCATION = (
    ('with-retailer', 'with-retailer'),
    ('with-dispatch', 'with-dispatch'),
    ('with-user', 'with-user')
)

class Cylinder(models.Model):
    cylinder_number = models.CharField(max_length=20)
    assigned_on = models.DateTimeField(auto_now=True)
    created_on = models.DateTimeField(auto_now_add=True)
    assigned_to = FSMField(choices=LOCATION, default='with-retailer', protected=True)

    def __str__(self):
        return self.cylinder_number

    @transition(field=assigned_to, source='with-retailer', target='with-dispatch')
    def issue_cylinder_for_delivery(self):
        return "Cylinder has been issued for delivery to user"

    @transition(field=assigned_to, source='with-dispatch', target='with-user')
    def issue_cylinder_to_final_user(self):
        return "Cylinder has been delivered to the final user"

    @transition(field=assigned_to, source='with-user', target='with-dispatch')
    def return_cylinder_from_final_user(self):
        return "Cylinder has been retrieved from final user for return"

    @transition(field=assigned_to, source='with-dispatch', target='with-retailer')
    def return_cylinder_to_retailer_store(self):
        return "Cylinder has been returned to the retailer"

FSM中状态间的转换是由@transition 装饰器提供的。

这个装饰器主要需要三个参数。

  • field - 要进行转换的领域
  • source - 对象被改变或被转换的状态
  • target - 同一对象要被改变或过渡到的状态。

让我们为我们的对象创建一个最小的序列化器。

我们将使用这个序列化器来创建一个圆柱体对象,我们将在本教程中与之合作。

下面是我们的序列化器。

from rest_framework import serializers
from tracker.models import Cylinder

class CylinderSerializer(serializers.ModelSerializer):
    class Meta:
        model = Cylinder
        fields = '__all__'

现在,是时候创建一个圆柱体对象了,创建视图来操作对象的状态,并创建URL来查看这些发生的变化。

用于创建和获取一个圆柱体实例的视图和URL如下所示。

from rest_framework import generics

from tracker.models import Cylinder
from tracker.serializer import CylinderSerializer

class CylinderCreateView(generics.ListCreateAPIView):
    queryset = Cylinder.objects.all()
    serializer_class = CylinderSerializer

class CylinderDetailView(generics.RetrieveUpdateDestroyAPIView):
    queryset = Cylinder.objects.all()
    serializer_class = CylinderSerializer

在上面的代码列表中,我们从rest框架模块中导入泛型功能。

我们还分别从models.pyserializers.py 中导入了CylinderCylinderSerializer 模型。

CylinderCreateView 让我们用它的POST 方法创建一个新的圆柱体对象,以获取所有创建的圆柱体。

通过GET 方法,CylinderDetailView 让你获取一个圆柱体的实例(单一实体),所以我们可以从存储(数据库)中更新或删除它。

from django.urls import path

from tracker.views import CylinderCreateView, CylinderDetailView,

urlpatterns = [
    path("cylinder/", CylinderCreateView.as_view()),
    path("cylinder/<int:pk>/", CylinderDetailView.as_view()),
]

通过上面的端点,我们可以创建一个圆柱体。

用命令运行项目的服务器。

Django runserver outcome image

Django的默认服务器端口是8000 ,除非用其他值指定。

导航到端点:http://127.0.0.1:9000/cylinder并创建一个圆柱体,如图所示。

Create cylinder object image

从上面的图片中,我们看到圆柱体已经入驻了零售商。

现在,让我们试着通过调度来发布它。一个简单的视图就可以完成这个任务。

视图和URL显示如下。

@api_view(['GET', 'PATCH'])
def change_location_from_retailer_to_dispatch(self, cylinder_name):
    obj = get_object_or_404(Cylinder, cylinder_number=cylinder_name)
    obj.issue_cylinder_for_delivery()
    obj.save()
    return Response("Changed state to dispatch")
from django.urls import path

from tracker.views import CylinderCreateView, change_location_from_retailer_to_dispatch, CylinderDetailView,

urlpatterns = [
    path("cylinder/", CylinderCreateView.as_view()),
    path("cylinder/<int:pk>/", CylinderDetailView.as_view()),

    path("retailer_to_dispatch/<str:cylinder_name>/", change_location_from_retailer_to_dispatch), # New line
    ]

现在,我们可以访问URL;http://127.0.0.1:9000/retailer-to-dispatch/cylinderone/,看看钢瓶是否已经从零售商那里发出。

通过其数据库索引ID,即1 ,检查钢瓶对象,通过端点:http://127.0.0.1:9000/retailer-to-dispatch/cylinder/1/

Cylinder object image

要把同一个钢瓶从调度处传递给最终用户,请访问端点:http://127.0.0.1:8000/dispatch-to-user/cylinderone/

Cylinder object image

我们可以看到,圆柱体的位置已经从 "与调度 "变为 "与用户"。

Cylinder object image

同样的方法可以用来将钢瓶从用户传递给调度,以及从调度传递给零售商,如下所示。

http://127.0.0.1:8000/user_to_dispatch/cylinderone/

http://127.0.0.1:8000/dispatch_to_retailer/cylinderone/

@api_view(['GET', 'PATCH'])
def change_location_from_user_to_dispatch(self, cylinder_name):
    obj = get_object_or_404(Cylinder, cylinder_number=cylinder_name)
    obj.return_cylinder_from_final_user()
    obj.save()
    return Response("Changed state to dispatch")

@api_view(['GET', 'PATCH'])
def change_location_from_dispatch_to_retailer(self, cylinder_name):
    obj = get_object_or_404(Cylinder, cylinder_number=cylinder_name)
    obj.return_cylinder_to_retailer_store()
    obj.save()
    return Response("Changed state to retailer")
from django.urls import path

from tracker.views import CylinderCreateView, change_location_from_retailer_to_dispatch, CylinderDetailView, \
    change_location_from_dispatch_to_user, change_location_from_user_to_dispatch, \
    change_location_from_dispatch_to_retailer

urlpatterns = [
    path("cylinder/", CylinderCreateView.as_view()),
    path("cylinder/<int:pk>/", CylinderDetailView.as_view()),

    path("retailer_to_dispatch/<str:cylinder_name>/", change_location_from_retailer_to_dispatch),
    path("dispatch_to_user/<str:cylinder_name>/", change_location_from_dispatch_to_user),
    path("user_to_dispatch/<str:cylinder_name>/", change_location_from_user_to_dispatch),
    path("dispatch_to_retailer/<str:cylinder_name>/", change_location_from_dispatch_to_retailer),
]

上面的代码列表显示了每个创建的视图的端点。

  • cylinder/ 用于创建一个气缸对象并获取所有创建的气缸。
  • cylinder/pk 从数据库中获取一个由URL中附加的主键(pk)索引的圆柱体。
  • retailer_to_dispatch/<str:cylinder_name>/ 控制视图,该视图基本上是将钢瓶从零售商那里交给调度人员。
  • dispatch_to_user/<str:cylinder_name>/ 控制将钢瓶从调度员转给最终用户的视图。
  • user_to_dispatch/<str:cylinder_name>/ 实现从终端用户到调度员的钢瓶返回。
  • dispatch_to_retailer/<str:cylinder_name>/ 实现钢瓶从派送员返回到原始保管人,即零售商。

需要注意的几点

让我们先假设我们想改变一个以上的字段。

例如,在我们的钢瓶追踪器项目中,我们可能想检查每个钢瓶的气体水平--无论是空的还是装满的--当它从一个地方移动到另一个地方时。

为此,我们简单地创建另一个选择元组,就像模型中的位置元组。然后,我们在模型中加入一个字段来引用这个新元组,然后为这个气体体积状态编写过渡。

最后,我们在相应的视图文件中为每个气体体积状态调用过渡状态。

奖金

处理异常

在FSM中提供了一种机制让你设置一个自定义的状态和响应,如果一个过渡导致了异常。

这显示在下面。

@transition(field=assigned_to, source='with-lpg', target='with-retailer', on_error='failed')
def close(self):
   """ Some exception could happen here """
   pass

基于权限的转换

权限可以在过渡装饰器的参数中作为一个引用被传递,以控制谁可以执行这个状态变化基于权限的过渡。

在过渡装饰器的参数中,可以将许可作为一个引用来传递,以控制谁可以执行这个状态变化。

@transition(field=assigned_to, source='with-user', target='with-retailer', permission='tracker.can_move_cylinder')
def close(self):
   """ This method will contain the action that needs to be taken once state is changed. """
   pass

结论

最后,使用FSM为我们提供了一个处理项目复杂问题的绝佳方式。

通过采用它,我们可以降低由于系统的不一致性而产生的错误总量。而且,代码结构会更有条理,更干净,更容易阅读,而且容易扩展。