如何为状态机或有限自动机实现RESTful资源

31

我是Rails和REST方面的新手,正在尝试找出最佳方式来暴露一个由状态机(也就是有限自动机)支持的域对象资源。

我见过许多用于将模型类变成状态机的gems,例如aasm、transitions、workflow,但它们都没有文档说明如何在面向资源的控制器中实际使用。它们似乎都意味着状态转换是由"事件"触发的,这实际上是一个方法调用。这意味着一些问题:

  1. 更新操作(PUT方法)不合适,因为PUT应该是幂等的。唯一可能的是如果状态作为表示的一部分发送。这与“事件”不一致。这正确吗?
  2. 由于事件不是幂等的,因此必须使用POST。但是,针对哪个资源?是否为每个可能的事件都存在子资源?还是有一个(/updatestate)接受事件触发和任何参数的表示形式?
  3. 由于资源的状态可能会被另一个资源触发的事件修改,因此创建操作是否应接受对状态属性(或任何依赖于状态机的其他属性)的更改?
  4. [更新后的问题] 如何在UI中展示状态转换?由于事件不是状态,似乎允许更新状态属性(以及任何其他依赖于状态转换的属性)是没有意义的。这是否意味着应该在更新操作中忽略这些属性?

4
我的建议是:不要为了满足所谓的100% RESTfulness 而让自己发疯。REST 是一个方便的工具,用于构建 API,但大多数复杂的 API 都不得不打破 REST 的规则,特别是在 PUT 幂等性方面。 - Kyle Wild
2
@dorkitude:似乎有许多级别的RESTfulness,但至少要遵守基本规则:对于幂等操作使用GET/PUT/DELETE,对于非幂等操作使用POST,对于失败使用HTTP错误代码,对父资源描述子资源的指针。这些(相对)容易部分。 - Donal Fellows
2
REST troll在此:如果您不遵循REST约束,请不要称其为RESTful而应该称之为RESTlike ;) - Derick Schoonbee
4个回答

9

我来晚了,但是我正在为自己研究这个确切的问题,发现我目前使用来管理状态机的gem(pluginaweek的state_machine)有一些方法可以很好地处理这个问题。

当与ActiveRecord(我假设其他持久层也是如此)一起使用时,它提供了一个#state_event=方法,接受一个事件的字符串表示形式,您想要触发。请参见此处的文档。

# For example,

vehicle = Vehicle.create          # => #<Vehicle id: 1, name: nil, state: "parked">
vehicle.state_event               # => nil
vehicle.state_event = 'invalid'
vehicle.valid?                    # => false
vehicle.errors.full_messages      # => ["State event is invalid"]

vehicle.state_event = 'ignite'
vehicle.valid?                    # => true
vehicle.save                      # => true
vehicle.state                     # => "idling"
vehicle.state_event               # => nil

# Note that this can also be done on a mass-assignment basis:

vehicle = Vehicle.create(:state_event => 'ignite')  # => #<Vehicle id: 1, name: nil, state: "idling">
vehicle.state                                       # => "idling"

这使您可以在资源的编辑表单中简单地添加一个state_event字段,并像更新任何其他属性一样轻松地获取状态转换。现在,显然仍然使用PUT来触发事件,这不是符合RESTful标准的。然而,该gem提供了一个有趣的例子,即使在底层使用相同的非RESTful方法,它至少“感觉”相当符合RESTful标准。正如您可以在这里这里看到的那样,该gem的内省能力允许您在表单中呈现要触发的事件或该事件结果状态的名称。
<div class="field">
  <%= f.label :state %><br />
  <%= f.collection_select :state_event, @user.state_transitions, :event, :human_to_name, :include_blank => @user.human_state_name %>
</div>

<div class="field">
  <%= f.label :access_state %><br />
  <%= f.collection_select :access_state_event, @user.access_state_transitions, :event, :human_event, :include_blank => "don't change" %>
</div>

使用后一种技术,您可以通过简单的基于表单的方式更新模型状态到任何有效的下一个状态,而无需编写任何额外的代码。虽然它不是严格意义上的RESTful,但它允许您在UI中轻松地呈现它。这种技术的简洁性以及尝试将基于事件的状态机转换为简单的RESTful资源时固有的冲突足以满足我,希望它也能为您提供一些见解。

1
非常有帮助的答案。我不知道state_event方法的文档在哪里,但是我可能遇到了这种技术的限制。当您使用state_event方法触发转换事件时,无法传递参数给它。这意味着您无法记录触发事件的current_user等信息(例如:https://dev59.com/n3A75IYBdhLWcg3wAUF9#6347369)。 - David Tuite

9
  • 更新操作(PUT方法)不适合,因为PUT应该是幂等的。唯一可能的情况是如果状态作为表示的一部分发送。这与“事件”不一致。这是正确的吗?

正确。

  • 由于事件不是幂等的,因此必须使用POST。但是,要用哪个资源?每个可能的事件都有一个子资源吗?还是有一个(/updatestate),它以其表示为触发事件和任何事件参数?

你可以两种方式都做。您可以在同一应用程序中支持两者,事件类型的变化取决于传入文档或接收资源。就我个人而言,我更喜欢通过不同的文档类型来完成,但这只是我的意见。如果您选择多个资源路线,请确保它们是可发现的(即,在GET其父资源时返回的文档中描述每个资源的链接)。

  • 由于资源的状态是通过可能由另一个资源触发的事件修改的,因此创建操作是否应接受对状态属性(或任何依赖于状态机的其他属性)的更改?

由您决定;您没有必要密切关注创建过程中的任何特定属性。(您可以通过说状态立即更改为状态机的适当初始状态来合理化此操作。)在我做的状态机中,创建本身就是通过POST完成的(并且使用了不同的 - 相当复杂的 - 文档),因此整个过程都是无关紧要的,但如果您允许多个初始状态,则在创建文档中接受“这是我的首选起始状态”提示是有意义的。要明确,仅因为用户想要它并不意味着您必须这样做;是否要在拒绝其建议时向用户抱怨取决于您。

  • 列表项

[标准回答。]


@Galex:说实话,我对如何映射到你的GUI没有真正的意见。但是,如果你将将状态设置为自身变成始终不执行任何操作的东西,那么你就拥有了幂等性,并且可以使用PUT。由于发生的其他事情,你可能选择不这样做,但它足够适合。 - Donal Fellows
一直在苦恼类似的问题,你的回答非常有用。谢谢。 - BrunoF

6

我来晚了,而且并不是专家,因为我有一个类似的问题,但是...

把这个事件作为资源如何?

所以,不是...

PUT /order/53?state_event="pay" #Order.update_attributes({state_event: "pay})

你会...

POST /order/53/pay     #OrderEvent.create(event_name: :pay)
POST /order/53/cancel  #OrderEvent.create(event_name: :cancel)

使用Order和OrderEvent之间的pub/sub监听器或回调函数来尝试在Order上触发该事件并记录转换消息。它还为您提供了所有状态更改事件的便捷审计。

灵感来自Shopify的Willem Bergen

我有什么遗漏吗?抱歉,我自己也很难理解。


3
如果您的资源具有某种状态属性,您可以使用一种称为微型PUT的技术来更新其状态。
PUT /Customer/1/Status
Content-Type: text/plain

Closed

=> 200 OK
Content-Location: /Customer/1

你可以将资源状态建模为集合,并在这些集合之间移动资源。
GET /Customer/1
=>
Content-Type: application/vnd.acme.customer+xml
200 OK


POST /ClosedCustomers
Content-Type: application/vnd.acme.customer+xml
=>
200 OK

POST /OpenCustomers
Content-Type: application/vnd.acme.customer+xml
=>
200 OK

您可以始终使用新的PATCH方法。
PATCH /Customer/1
Content-Type: application/x-www-form-urlencoded
Status=Closed
=>
200 OK

有微型PUT参考链接吗?这是我之前从未听说过的东西。 - undefined

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