在Django中使用有限状态机操纵对象状态
随着项目越来越大,向意大利语代码倾斜的趋势变得不可避免。避免这些复杂情况的方法之一是应用有限状态机(FSM)概念来处理对象的状态变化。
FSM帮助你简化代码,通过定义状态和它们之间的联系来避免大量的if-else条件。
在这篇文章中,我们将学习如何在运行时操作和改变对象的状态。
为了证明这一点,我们将建立一个具有基本CRUD操作的轻量级气缸跟踪系统。
内容列表
本教程将涵盖。
- 什么是有限状态机?
- 有限状态机(FSM)在Django中如何工作?
- 选择改变状态的方法时需要注意的要点
- 结论和建议
前提条件
要充分利用本教程,需要具备以下条件。
- 对Python的基本了解。
- 熟悉Django框架和Django Rest框架。
- 熟悉Django rest框架的可浏览API界面。
- 安装了PyCharm专业代码编辑器。
- 快速浏览[计算理论]和[正则表达式]的[介绍]
什么是有限状态机?
有限状态机(FSM)是一个在面向对象编程中促进对象动态化的系统。
其理念是,对象每次只能承担一种状态。最流行的、最相关的例子是交通灯。
在任何时候,不管一个路口有多少个交通灯,每个板块一次只能有一个灯,否则会导致混乱。
在本教程中,我们将使用Django中构建的CynTrack
应用程序来实现对象状态的转换,以解释FSM的工作原理。
Django中的有限状态机是如何工作的?
CynTrack
是一个简单的应用程序,它根据检查时谁是占有者来跟踪一个圆筒。
随着圆柱体的移动,拥有者也会随之改变,而新的拥有者总是被记录在圆柱体对象中。
让我们深入了解一下实现的过程。
为了创建这个系统,我们将通过四个步骤进行工作。
- 定义这个对象所能承担的状态
- 创建该对象的模型
- 定义对象的状态之间的转换
- 实现负责这种转换的视图
我们想假设读者已经熟悉了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.py
和serializers.py
中导入了Cylinder
和CylinderSerializer
模型。
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的默认服务器端口是8000
,除非用其他值指定。
导航到端点:http://127.0.0.1:9000/cylinder并创建一个圆柱体,如图所示。
从上面的图片中,我们看到圆柱体已经入驻了零售商。
现在,让我们试着通过调度来发布它。一个简单的视图就可以完成这个任务。
视图和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/
要把同一个钢瓶从调度处传递给最终用户,请访问端点:http://127.0.0.1:8000/dispatch-to-user/cylinderone/
我们可以看到,圆柱体的位置已经从 "与调度 "变为 "与用户"。
同样的方法可以用来将钢瓶从用户传递给调度,以及从调度传递给零售商,如下所示。
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为我们提供了一个处理项目复杂问题的绝佳方式。
通过采用它,我们可以降低由于系统的不一致性而产生的错误总量。而且,代码结构会更有条理,更干净,更容易阅读,而且容易扩展。