参赛作品
在项目中,我遇到了三个例子,一种或另一种与
有限自动机理论相关的例子
- 示例1. 一个有趣的
govnokod 代码 。 需要花费大量时间来了解正在发生的事情。 代码中所示理论的实施例的特征是相当猛烈的转储,有时非常类似于过程代码。 此版本的代码最好不要接触项目,这一事实知道每个技术人员,方法专家和产品专家。 他们使用此代码来修复紧急情况(当它完全损坏时),毫无疑问地完成任何功能。 因为它令人恐惧。 隔离此类型的第二个醒目的功能是这种功能强大的开关(全屏)。
这个分数甚至有个玩笑:
最佳尺寸在其中一个JPoint上,一位发言人,也许是Nikolai Alimenkov,谈到了切换中有多少情况是正常的,他说最重要的答案是“到目前为止适合屏幕”。 因此,如果干扰并且您的开关已经不正常,请在IDE中减小字体大小
- 示例2. 模式状态 。 主要想法(对于那些不喜欢使用链接的人)是,我们将某个业务任务分解为一组最终状态,并用代码对其进行描述。
模式状态的主要缺点是状态彼此了解,它们知道有兄弟并互相呼叫。 这样的代码很难通用。 例如,在实现具有多种付款方式的付款系统时,您可能会冒充Generic-s的风险,以致于方法的声明可能会变成这样:
private < T extends BaseContextPayment, Q extends BaseDomainPaymentRequest, S, B extends AbstractPaymentDetailBuilder<T, Q, S, B>, F extends AbstractPaymentBuilder<T, Q, S, B> > PaymentContext<T, S> build(final Q request, final Class<F> factoryClass){
总结状态:实现可能导致相当复杂的代码。 - 示例3 StateMachine模式的主要思想是状态之间彼此一无所知,过渡控制是由上下文执行的,它更好,连接性更强-代码更简单。
在经历了第一种类型的所有“功能”和第二种类型的复杂性之后,我们决定将Pattern StateMachine用于新的业务案例。
为了不重新发明轮子,决定以Statemachine Spring为基础(这就是Spring)。
看完基座之后,我去了YouTube和Habr(了解人们如何使用它,它在产品上的感觉,什么样的耙子等)。事实证明,很少有信息,在YouTube上有几个视频,都非常肤浅。 在有关此主题的Habré上,我发现只有一篇文章以及视频很肤浅。
在一篇文章中,不可能描述Spring statemachine工作的所有细微之处,无法深入研究所有案例,但是我会尽力讲出最重要和最需要的内容,关于rake,特别是对我来说,当我熟悉框架时,下面的信息是将非常有帮助。
主体
我们将创建一个Spring Boot应用程序,添加一个Web Starter(使Web应用程序尽快运行),该应用程序将是购买过程的抽象。 购买的产品将经历新的,保留的,保留的下降和购买完成的阶段。
一点即兴创作,在一个真实的项目中会有更多的状态,但是,哦,我们也有一个非常真实的项目。
在新烘焙的Web应用程序的pom.xml中,添加对计算机及其测试的依赖项(如果通过
start.spring.io收集,则Web Starter应该已经存在):
<dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-core</artifactId> <version>2.1.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.statemachine</groupId> <artifactId>spring-statemachine-test</artifactId> <version>2.1.3.RELEASE</version> <scope>test</scope> </dependency> <cut />
创建结构:

我还不需要详细介绍这种结构,我将按顺序说明所有内容,并且在文章末尾将提供指向该源的链接。
所以走吧
我们有一个带有必要依赖项的干净项目,首先,我们将创建带有状态和事件的枚举,以及一个相当简单的抽象,这些组件本身不包含任何逻辑。
public enum PurchaseEvent { RESERVE, BUY, RESERVE_DECLINE }
public enum PurchaseState { NEW, RESERVED, CANCEL_RESERVED, PURCHASE_COMPLETE }
尽管从形式上来说,您可以向这些枚举添加字段,并在其中对某些具有某种逻辑特征的东西进行硬编码,这是很合逻辑的(我们很方便地通过解决情况来做到这一点)。
我们将通过java config配置机器,创建配置文件,并为扩展类EnumStateMachineConfigurerAdapter <PurchaseState,PurchaseEvent>。 因为我们的状态和事件是枚举,所以接口是合适的,但不是必须的,任何类型的对象都可以用作泛型(由于我认为EnumStateMachineConfigurerAdapter绰绰有余,因此我们不会考虑本文中的其他示例)。
下一个要点是,一台机器是否将驻留在应用程序上下文中:在@EnableStateMachine的单个实例中,还是每次创建新的@EnableStateMachineFactory时。 如果这是一个有大量用户的多用户Web应用程序,那么第一个选项几乎不适合您,因此我们将使用第二个选项作为更流行的选项。 StateMachine也可以通过构建器作为常规bean创建(请参阅文档),在某些情况下很方便(例如,您需要将机器显式声明为bean),如果它是单独的bean,那么我们可以告诉我们我们的范围例如会话或请求。 在我们的项目中,包装器(业务逻辑的特征)是在状态机bean上实现的,包装器是单例,原型机本身
耙子如何在Singlton中实现原型?
实际上,您需要做的就是每次访问对象时从applicationContext获取一个新bean。 将applicationContext注入业务逻辑是一种罪过,因此,Bean状态机必须使用至少一个方法或抽象方法(方法注入)来实现接口,在创建Java配置时,您将需要实现所指示的抽象方法,并且我们将从applicationContext新bean。 通常的做法是在config类中链接到applicationContext,通过抽象方法,我们将调用.getBean();。
EnumStateMachineConfigurerAdapter类有几种方法,它们覆盖了我们配置机器的方法。
首先,注册状态:
@Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .states(EnumSet.allOf(PurchaseState.class)); }
初始(NEW)是机器在创建Bean之后将处于的状态,结束(PURCHASE_COMPLETE)是机器将执行statemachine.stop()的状态,对于不确定的机器(其中大多数)是无关紧要的,但需要指定一些内容。 .states(EnumSet.allOf(PurchaseState.class)所有状态的列表,您可以批量推送。
配置全局机器设置
@Override public void configure(final StateMachineConfigurationConfigurer<PurchaseState, PurchaseEvent> config) throws Exception { config .withConfiguration() .autoStartup(true) .listener(new PurchaseStateMachineApplicationListener()); }
在此,autoStartup确定默认情况下是否在创建后自动启动计算机,换句话说-是否将自动切换到NEW状态(默认为false)。 立即,我们为机器上下文注册一个侦听器(稍后介绍),在相同的配置中,您可以设置一个单独的TaskExecutor,当对它们的某些转换执行长时间的Action且应用程序应该走得更远时,这很方便。
好吧,过渡本身:
@Override public void configure(final StateMachineTransitionConfigurer<PurchaseState, PurchaseEvent> transitions) throws Exception { transitions .withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(CANCEL_RESERVED) .event(RESERVE_DECLINE) .action(cancelAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .action(buyAction(), errorAction()); }
在此处设置了所有转换逻辑或转换逻辑,Guard可以挂在转换上,一个始终返回布尔值的组件,您将自行决定从一个状态到另一种状态的转换究竟要检查什么,在Guard中任何逻辑都可以完美,这是一个完全普通的组件但他必须返回布尔值。 例如,在我们项目的框架内,HideGuard可以检查用户可以设置的某个设置(不显示此产品),并且根据它,不使计算机进入受Guard保护的状态。 我注意到,Guard,只能将一个添加到配置中的一个过渡中,这样的设计将行不通:
.withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .guard(veryHideGuard())
更确切地说,它将起作用,但只有第一个后卫(hideGuard())
但是您可以添加几个动作(现在我们正在讨论动作,这是我们在过渡配置中指定的),我个人试图将三个动作添加到一个过渡中。
.withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction())
第二个参数是ErrorAction,如果ReservedAction引发异常(抛出),则控件将获取它。
耙子请记住,如果在Action中仍通过try / catch处理错误,则不会进入ErrorAction,如果需要处理并进入ErrorAction,则应从catch中抛出RuntimeException(),例如(您自己说这是非常必要的)。
除了在过渡中“挂起”动作外,您还可以在状态状态的configure方法中“挂起”动作,大致采用以下形式:
@Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .stateEntry() .stateExit() .state() .states(EnumSet.allOf(PurchaseState.class)); }
这完全取决于您要如何执行操作。
耙子请注意,如果您在配置状态()时指定了操作,例如
states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .state(randomAction())
它会异步执行,例如,假设您说.stateEntry(),则应在入口处直接执行该操作,但如果说.state(),则应在目标状态下执行该操作,但是何时执行则不是那么重要。
在我们的项目中,我们在过渡配置上配置了所有操作,因为您可以一次将它们挂起几个。
配置的最终版本将如下所示:
@Configuration @EnableStateMachineFactory public class StateMachineConfig extends EnumStateMachineConfigurerAdapter<PurchaseState, PurchaseEvent> { @Override public void configure(final StateMachineConfigurationConfigurer<PurchaseState, PurchaseEvent> config) throws Exception { config .withConfiguration() .autoStartup(true) .listener(new PurchaseStateMachineApplicationListener()); } @Override public void configure(final StateMachineStateConfigurer<PurchaseState, PurchaseEvent> states) throws Exception { states .withStates() .initial(NEW) .end(PURCHASE_COMPLETE) .stateEntry() .stateExit() .state() .states(EnumSet.allOf(PurchaseState.class)); } @Override public void configure(final StateMachineTransitionConfigurer<PurchaseState, PurchaseEvent> transitions) throws Exception { transitions .withExternal() .source(NEW) .target(RESERVED) .event(RESERVE) .action(reservedAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(CANCEL_RESERVED) .event(RESERVE_DECLINE) .action(cancelAction(), errorAction()) .and() .withExternal() .source(RESERVED) .target(PURCHASE_COMPLETE) .event(BUY) .guard(hideGuard()) .action(buyAction(), errorAction()); } @Bean public Action<PurchaseState, PurchaseEvent> reservedAction() { return new ReservedAction(); } @Bean public Action<PurchaseState, PurchaseEvent> cancelAction() { return new CancelAction(); } @Bean public Action<PurchaseState, PurchaseEvent> buyAction() { return new BuyAction(); } @Bean public Action<PurchaseState, PurchaseEvent> errorAction() { return new ErrorAction(); } @Bean public Guard<PurchaseState, PurchaseEvent> hideGuard() { return new HideGuard(); } @Bean public StateMachinePersister<PurchaseState, PurchaseEvent, String> persister() { return new DefaultStateMachinePersister<>(new PurchaseStateMachinePersister()); }
注意机器的方案,在我们精确编码的内容(哪些事件有效的过渡,哪个Guard保护状态以及切换状态时将执行的操作,哪个Action)上非常清晰可见。

让我们做控制器:
@RestController @SuppressWarnings("unused") public class PurchaseController { private final PurchaseService purchaseService; public PurchaseController(PurchaseService purchaseService) { this.purchaseService = purchaseService; } @RequestMapping(path = "/reserve") public boolean reserve(final String userId, final String productId) { return purchaseService.reserved(userId, productId); } @RequestMapping(path = "/cancel") public boolean cancelReserve(final String userId) { return purchaseService.cancelReserve(userId); } @RequestMapping(path = "/buy") public boolean buyReserve(final String userId) { return purchaseService.buy(userId); } }
服务接口
public interface PurchaseService { boolean reserved(String userId, String productId); boolean cancelReserve(String userId); boolean buy(String userId); }
耙子您知道在使用Spring时通过接口创建bean的重要性吗? 面对这个问题(是的,是的,是的,Zhenya Borisov在开膛手中讲话),当他们进入控制器后,他们试图实现一个临时的非空接口。 Spring为组件创建了一个代理,如果组件未实现任何接口,它将通过CGLIB来实现,但是一旦实现某个接口,Spring就会尝试通过动态代理创建代理,结果您将获得难以理解的对象类型和NoSuchBeanDefinitionException 。
下一个重要的点是如何恢复机器的状态,因为对于每次调用,都会创建一个新的bean,该bean对机器的先前状态及其上下文一无所知。
为此,spring statemachine具有Persistens机制:
public class PurchaseStateMachinePersister implements StateMachinePersist<PurchaseState, PurchaseEvent, String> { private final HashMap<String, StateMachineContext<PurchaseState, PurchaseEvent>> contexts = new HashMap<>(); @Override public void write(final StateMachineContext<PurchaseState, PurchaseEvent> context, String contextObj) { contexts.put(contextObj, context); } @Override public StateMachineContext<PurchaseState, PurchaseEvent> read(final String contextObj) { return contexts.get(contextObj); } }
对于我们的天真的实现,我们使用通常的Map作为状态存储,在非天真的实现中,它将是某种数据库,请注意第三个通用类型String,这是保存计算机状态的键,其中包含所有状态,上下文中的变量,id等等。 在我的示例中,我使用用户ID作为保存密钥,该ID绝对可以是任何密钥(用户session_id,唯一登录名等)。
耙子在我们的项目中,从盒子中保存和恢复状态的机制不适合我们,因为我们将机器的状态存储在数据库中,并且可以通过对机器一无所知的工作进行更改。
我必须固定从数据库接收的状态,执行一些InitAction,当计算机启动时,它会从数据库接收状态,并对其进行强制设置,然后才引发事件,即执行上述操作的示例代码:
stateMachine .getStateMachineAccessor() .doWithAllRegions(access -> { access.resetStateMachine(new DefaultStateMachineContext<>({ResetState}, null, null, null, null)); }); stateMachine.start(); stateMachine.sendEvent({NewEventFromResetState});
我们将考虑每种方法中服务的实现:
@Override public boolean reserved(final String userId, final String productId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); stateMachine.getExtendedState().getVariables().put("PRODUCT_ID", productId); stateMachine.sendEvent(RESERVE); try { persister.persist(stateMachine, userId); } catch (final Exception e) { e.printStackTrace(); return false; } return true; }
我们从工厂取车,在机器上下文中放置一个参数,在本例中是productId,上下文是一种盒子,您可以在需要访问状态机bean或其上下文的任何地方放置所需的所有东西,因为机器会在上下文开始时自动启动,然后在启动之后,我们的汽车将处于NEW状态,将事件抛出在预订商品上。
其余两种方法类似:
@Override public boolean cancelReserve(final String userId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); try { persister.restore(stateMachine, userId); stateMachine.sendEvent(RESERVE_DECLINE); } catch (Exception e) { e.printStackTrace(); return false; } return true; } @Override public boolean buy(final String userId) { final StateMachine<PurchaseState, PurchaseEvent> stateMachine = stateMachineFactory.getStateMachine(); try { persister.restore(stateMachine, userId); stateMachine.sendEvent(BUY); } catch (Exception e) { e.printStackTrace(); return false; } return true; }
在这里,我们首先为特定用户的userId恢复计算机的状态,然后引发与api方法相对应的事件。
请注意,productId不再出现在方法中,我们将其添加到计算机上下文中,并且将从备份中恢复计算机后将其获取。
在Action实现中,我们将从机器上下文中获取产品ID,并在日志中显示与转换相对应的消息,例如,我将给出代码ReservedAction:
public class ReservedAction implements Action<PurchaseState, PurchaseEvent> { @Override public void execute(StateContext<PurchaseState, PurchaseEvent> context) { final String productId = context.getExtendedState().get("PRODUCT_ID", String.class); System.out.println(" " + productId + " ."); } }
我们不能不提及侦听器,该侦听器开箱即用提供了许多您可以挂起的脚本,请亲自看看:
public class PurchaseStateMachineApplicationListener implements StateMachineListener<PurchaseState, PurchaseEvent> { @Override public void stateChanged(State<PurchaseState, PurchaseEvent> from, State<PurchaseState, PurchaseEvent> to) { if (from.getId() != null) { System.out.println(" " + from.getId() + " " + to.getId()); } } @Override public void stateEntered(State<PurchaseState, PurchaseEvent> state) { } @Override public void stateExited(State<PurchaseState, PurchaseEvent> state) { } @Override public void eventNotAccepted(Message<PurchaseEvent> event) { System.out.println(" " + event); } @Override public void transition(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void transitionStarted(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void transitionEnded(Transition<PurchaseState, PurchaseEvent> transition) { } @Override public void stateMachineStarted(StateMachine<PurchaseState, PurchaseEvent> stateMachine) { System.out.println("Machine started"); } @Override public void stateMachineStopped(StateMachine<PurchaseState, PurchaseEvent> stateMachine) { } @Override public void stateMachineError(StateMachine<PurchaseState, PurchaseEvent> stateMachine, Exception exception) { } @Override public void extendedStateChanged(Object key, Object value) { } @Override public void stateContext(StateContext<PurchaseState, PurchaseEvent> stateContext) { } }
唯一的问题是,这是一个接口,这意味着您需要实现所有这些方法,但是由于您不太可能全部使用它们,因此其中一些将空无一物,其覆盖范围将表明测试未涵盖这些方法。
在lisener中,我们可以将任何指标完全挂在机器的完全不同的事件上(例如,付款没有通过,机器经常进入某种PAYMENT_FAIL状态,我们监听过渡,并且如果机器进入错误状态-我们用奇怪的日志写,或驻扎或报警,等等。
耙子lisener-e中有一个状态stateMachineError事件,但有一点细微差别,当您遇到异常并在catch中处理该异常时,机器不认为存在错误,您需要在catch中明确地说出
stateMachine.setStateMachineError(异常)并传递错误。
为了检查我们所做的事情,我们将执行两种情况:
- 1.保留并随后拒绝购买。 我们将使用参数userId = 007,productId = 10001向应用程序发送URI请求“ / reserve”,然后在请求后使用参数userId = 007进行“ / cancel”控制台输出:
Machine started
10001 .
NEW RESERVED
Machine started
10001
RESERVED CANCEL_RESERVED
- 2.预订并成功购买:
Machine started
10001 .
NEW RESERVED
Machine started
10001
RESERVED PURCHASE_COMPLETE
结论
总而言之,我将给出一个测试框架的示例,我认为代码中的所有内容都将变得清晰起来,您只需要对测试机器的依赖性,就可以声明性地检查配置。
@Test public void testWhenReservedCancel() throws Exception { StateMachine<PurchaseState, PurchaseEvent> machine = factory.getStateMachine(); StateMachineTestPlan<PurchaseState, PurchaseEvent> plan = StateMachineTestPlanBuilder.<PurchaseState, PurchaseEvent>builder() .defaultAwaitTime(2) .stateMachine(machine) .step() .expectStates(NEW) .expectStateChanged(0) .and() .step() .sendEvent(RESERVE) .expectState(RESERVED) .expectStateChanged(1) .and() .step() .sendEvent(RESERVE_DECLINE) .expectState(CANCEL_RESERVED) .expectStateChanged(1) .and() .build(); plan.test(); } @Test public void testWhenPurchaseComplete() throws Exception { StateMachine<PurchaseState, PurchaseEvent> machine = factory.getStateMachine(); StateMachineTestPlan<PurchaseState, PurchaseEvent> plan = StateMachineTestPlanBuilder.<PurchaseState, PurchaseEvent>builder() .defaultAwaitTime(2) .stateMachine(machine) .step() .expectStates(NEW) .expectStateChanged(0) .and() .step() .sendEvent(RESERVE) .expectState(RESERVED) .expectStateChanged(1) .and() .step() .sendEvent(BUY) .expectState(PURCHASE_COMPLETE) .expectStateChanged(1) .and() .build(); plan.test(); }
耙子如果您突然想在不使用常规单元测试引发上下文的情况下测试计算机,则可以通过构建器创建计算机(上面已部分讨论),使用config创建类的实例并从那里获取操作和保护,它将在没有上下文的情况下工作,您可以编写一些测试该框架是模拟的,在不同情况下检查哪些动作被调用,哪些未被调用,多少次等等将是一个加号。
聚苯乙烯
, , , , (Guard- Action- )
, choice, , switch, Guards, , choice Guard, Events, , , .
参考文献