In the daily work process, we often encounter the scene changes state, such as order status changes, changes in commodity status. These state changes, we called finite state machines, abbreviated FSM (F State Machine) .. It is called a limited, because the status of these scenarios can often be enumerated a limited number of so called finite state machine. Let's look at a specific example of a scene.
Simple scenario:
Metro State has two stop gate: closed, has opened two states. After the card is turned from the closed gate goes after people through the gate from the closed state is turned into a.
01 problems of this sort, when coding How should we deal with it?
- Switch on
- Based on the set of states
- Based on State Mode
- Based realization enumerates
Here we analyze for each implementation. Behind the scenes there will be two states break it down four situations occurs:
Index | State | Event | NextState | Action |
---|---|---|---|---|
1 | Gates port LOCKED | Coin | Gates port UN_LOCKED | Shutter gates open port |
2 | Gates port LOCKED | by | Gates port LOCKED | The caution gates |
3 | Gates port UN_LOCKED | Coin | Gates port UN _LOCKED | Coin gates port |
4 | Gates port UN_LOCKED | by | Gates port LOCKED | Close the shutter opening gates |
For four or more requests were split five Test Case
T01
Given: a stint Locked gate
When: coin
Then: Open the Gate
T02
Given: a stint Locked gate
When: through the gate
Then: warning
T03
Given: a Unocked stint gate
When: through the gate
Then: Gate closed
T04
Given: a stint Gate Unlocked
When: coin
Then: refund of coins
T05
Given: a port gates
When: Illegal Operation
Then: The operation failed
Code address: https: //gitlab.com/tengbai/fsm-java
There are 4 project implementation in the state machine.
Switch statement based on the finite state machine implemented in the code master branch
State mode based on finite state machine implementation. Code state-pattern branch
Based on a set of finite state machine implementation. Code collection-state branch
Based enumeration state machine implemented. Code enum-state branch
01.01 Switch implemented using a finite state machine
This only need to know Java syntax and can achieve them. Look at the code, and then we discuss this implementation is good.
EntranceMachineTest.java
package com.page.java.fsm;
import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;
class EntranceMachineTest {
@Test
void should_be_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
String result = entranceMachine.execute(Action.INSERT_COIN);
then(result).isEqualTo("opened");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}
@Test
void should_be_locked_and_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
String result = entranceMachine.execute(Action.PASS);
then(result).isEqualTo("alarm");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}
@Test
void should_fail_when_execute_invalid_action_given_a_entrance_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
assertThatThrownBy(() -> entranceMachine.execute(null))
.isInstanceOf(InvalidActionException.class);
}
@Test
void should_locked_when_pass_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);
String result = entranceMachine.execute(Action.PASS);
then(result).isEqualTo("closed");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}
@Test
void should_refund_and_unlocked_when_insert_coin_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);
String result = entranceMachine.execute(Action.INSERT_COIN);
then(result).isEqualTo("refund");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}
}
复制代码
Action.java
public enum Action {
INSERT_COIN,
PASS
}
复制代码
EntranceMachineState.java
public enum EntranceMachineState {
UNLOCKED,
LOCKED
}
复制代码
InvalidActionException.java
package com.page.java.fsm.exception;
public class InvalidActionException extends RuntimeException {
}
复制代码
EntranceMachine.java
package com.page.java.fsm;
import com.page.java.fsm.exception.InvalidActionException;
import lombok.Data;
import java.util.Objects;
@Data
public class EntranceMachine {
private EntranceMachineState state;
public EntranceMachine(EntranceMachineState state) {
this.state = state;
}
public String execute(Action action) {
if (Objects.isNull(action)) {
throw new InvalidActionException();
}
if (EntranceMachineState.LOCKED.equals(state)) {
switch (action) {
case INSERT_COIN:
setState(EntranceMachineState.UNLOCKED);
return open();
case PASS:
return alarm();
}
}
if (EntranceMachineState.UNLOCKED.equals(state)) {
switch (action) {
case PASS:
setState(EntranceMachineState.LOCKED);
return close();
case INSERT_COIN:
return refund();
}
}
return null;
}
private String refund() {
return "refund";
}
private String close() {
return "closed";
}
private String alarm() {
return "alarm";
}
private String open() {
return "opened";
}
}
复制代码
if (), swich statement is the switch statement, but Switch Smell of Bad is a Code , because it is essentially an iterative. When the code has the same multiple switch, the system will become obscure, fragile, difficult to modify.
Despite the above code nested but fairly simple structure, but does not want to clear the mouth of logic gates or point of time. If the state of the port gates and other some more, then read, it is more difficult to understand.
So in their daily work, I follow ** "Shibuguosan, three reconstruction" of principles **:
Shibuguosan:
When only one or two states (or duplicate), then the first implemented in the simplest implementation.
Once the three kinds of state and one or more (or repeated) appears immediately reconstructed.
01.02 State mode
EntranceMachineTest.java
package com.page.java.fsm;
import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;
class EntranceMachineTest {
@Test
void should_be_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(new LockedEntranceMachineState());
String result = entranceMachine.execute(Action.INSERT_COIN);
then(result).isEqualTo("opened");
then(entranceMachine.isUnlocked()).isTrue();
}
@Test
void should_be_locked_and_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(new LockedEntranceMachineState());
String result = entranceMachine.execute(Action.PASS);
then(result).isEqualTo("alarm");
then(entranceMachine.isLocked()).isTrue();
}
@Test
void should_fail_when_execute_invalid_action_given_a_entrance_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(new LockedEntranceMachineState());
assertThatThrownBy(() -> entranceMachine.execute(null))
.isInstanceOf(InvalidActionException.class);
}
@Test
void should_locked_when_pass_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(new UnlockedEntranceMachineState());
String result = entranceMachine.execute(Action.PASS);
then(result).isEqualTo("closed");
then(entranceMachine.isLocked()).isTrue();
}
@Test
void should_refund_and_unlocked_when_insert_coin_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(new UnlockedEntranceMachineState());
String result = entranceMachine.execute(Action.INSERT_COIN);
then(result).isEqualTo("refund");
then(entranceMachine.isUnlocked()).isTrue();
}
}
复制代码
EntranceMachineState.java
package com.page.java.fsm;
public interface EntranceMachineState {
String insertCoin(EntranceMachine entranceMachine);
String pass(EntranceMachine entranceMachine);
}
复制代码
LockedEntranceMachineState.java
package com.page.java.fsm;
public class LockedEntranceMachineState implements EntranceMachineState {
@Override
public String insertCoin(EntranceMachine entranceMachine) {
return entranceMachine.open();
}
@Override
public String pass(EntranceMachine entranceMachine) {
return entranceMachine.alarm();
}
}
复制代码
UnlockedEntranceMachineState.java
package com.page.java.fsm;
public class UnlockedEntranceMachineState implements EntranceMachineState {
@Override
public String insertCoin(EntranceMachine entranceMachine) {
return entranceMachine.refund();
}
@Override
public String pass(EntranceMachine entranceMachine) {
return entranceMachine.close();
}
}
复制代码
Action.java
package com.page.java.fsm;
public enum Action {
PASS,
INSERT_COIN
}
复制代码
EntranceMachine.java
package com.page.java.fsm;
import com.page.java.fsm.exception.InvalidActionException;
import java.util.Objects;
public class EntranceMachine {
private EntranceMachineState locked = new LockedEntranceMachineState();
private EntranceMachineState unlocked = new UnlockedEntranceMachineState();
private EntranceMachineState state;
public EntranceMachine(EntranceMachineState state) {
this.state = state;
}
public String execute(Action action) {
if (Objects.isNull(action)) {
throw new InvalidActionException();
}
if (Action.PASS.equals(action)) {
return state.pass(this);
}
return state.insertCoin(this);
}
public boolean isUnlocked() {
return state == unlocked;
}
public boolean isLocked() {
return state == locked;
}
public String open() {
setState(unlocked);
return "opened";
}
public String alarm() {
setState(locked);
return "alarm";
}
public String refund() {
setState(unlocked);
return "refund";
}
public String close() {
setState(locked);
return "closed";
}
private void setState(EntranceMachineState state) {
this.state = state;
}
}
复制代码
State mode and Proxy mode, but EntranceMachineState hold a reference EntranceMachine instances in State mode.
We found EntranceMachine the execute () method becomes simple logic, but the increased code complexity. Because each instance state provides two actions for achieving insertCoin () and pass (). I think this place is not enough expressive, because the action taken is added to the two states, even though it can achieve business business, but not conducive to clearly understand the meaning of service.
State mode, while logic can be resolved, the order of those states, and in several states, are not very intuitive observed.
However, in actual business, State mode is also a good way to achieve, after all, he avoids the accumulation of problems switch.
01.03 using state set
The state is the set state change set transaction described elements.
Each element in the set contains four attributes: the current state of events, the next state, trigger action.
Through the collection to find specific elements according to the action, and more properties and events on the elements to complete the business logic use.
Specific code as follows:
EntranceMachineTest.java
package com.page.java.fsm;
import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;
class EntranceMachineTest {
@Test
void should_be_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
String result = entranceMachine.execute(Action.INSERT_COIN);
then(result).isEqualTo("opened");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}
@Test
void should_be_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
String result = entranceMachine.execute(Action.PASS);
then(result).isEqualTo("alarm");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}
@Test
void should_fail_when_execute_invalid_action_given_a_entrance_machine() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
assertThatThrownBy(() -> entranceMachine.execute(null))
.isInstanceOf(InvalidActionException.class);
}
@Test
void should_closed_when_pass_given_a_entrance_machine_with_unlocked() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);
String result = entranceMachine.execute(Action.PASS);
then(result).isEqualTo("closed");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}
@Test
void should_refund_when_insert_coin_given_a_entrance_machine_with_unlocked() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);
String result = entranceMachine.execute(Action.INSERT_COIN);
then(result).isEqualTo("refund");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}
}
复制代码
Action.java
package com.page.java.fsm;
public enum Action {
PASS,
INSERT_COIN
}
复制代码
EntranceMachineState.java
package com.page.java.fsm;
public enum EntranceMachineState {
LOCKED,
UNLOCKED
}
复制代码
EntranceMachine.java
package com.page.java.fsm;
import com.page.java.fsm.events.AlarmEvent;
import com.page.java.fsm.events.CloseEvent;
import com.page.java.fsm.events.OpenEvent;
import com.page.java.fsm.events.RefundEvent;
import com.page.java.fsm.exception.InvalidActionException;
import lombok.Data;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Data
public class EntranceMachine {
List<EntranceMachineTransaction> entranceMachineTransactionList = Arrays.asList(
EntranceMachineTransaction.builder()
.currentState(EntranceMachineState.LOCKED)
.action(Action.INSERT_COIN)
.nextState(EntranceMachineState.UNLOCKED)
.event(new OpenEvent())
.build(),
EntranceMachineTransaction.builder()
.currentState(EntranceMachineState.LOCKED)
.action(Action.PASS)
.nextState(EntranceMachineState.LOCKED)
.event(new AlarmEvent())
.build(),
EntranceMachineTransaction.builder()
.currentState(EntranceMachineState.UNLOCKED)
.action(Action.PASS)
.nextState(EntranceMachineState.LOCKED)
.event(new CloseEvent())
.build(),
EntranceMachineTransaction.builder()
.currentState(EntranceMachineState.UNLOCKED)
.action(Action.INSERT_COIN)
.nextState(EntranceMachineState.UNLOCKED)
.event(new RefundEvent())
.build()
);
private EntranceMachineState state;
public EntranceMachine(EntranceMachineState state) {
setState(state);
}
public String execute(Action action) {
Optional<EntranceMachineTransaction> transactionOptional = entranceMachineTransactionList
.stream()
.filter(transaction ->
transaction.getAction().equals(action) && transaction.getCurrentState().equals(state))
.findFirst();
if (!transactionOptional.isPresent()) {
throw new InvalidActionException();
}
EntranceMachineTransaction transaction = transactionOptional.get();
setState(transaction.getNextState());
return transaction.getEvent().execute();
}
}
复制代码
EntranceMachineTransaction.java
package com.page.java.fsm;
import com.page.java.fsm.events.Event;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EntranceMachineTransaction {
private EntranceMachineState currentState;
private Action action;
private EntranceMachineState nextState;
private Event event;
}
复制代码
Event.java
package com.page.java.fsm.events;
public interface Event {
String execute();
}
复制代码
OpenEvent.java
package com.page.java.fsm.events;
public class OpenEvent implements Event {
@Override
public String execute() {
return "opened";
}
}
复制代码
AlarmEvent.java
package com.page.java.fsm.events;
public class AlarmEvent implements Event {
@Override
public String execute() {
return "alarm";
}
}
复制代码
CloseEvent.java
package com.page.java.fsm.events;
public class CloseEvent implements Event {
@Override
public String execute() {
return "closed";
}
}
复制代码
RefundEvent.java
package com.page.java.fsm.events;
public class RefundEvent implements Event {
@Override
public String execute() {
return "refund";
}
}
复制代码
InvalidActionException.java
package com.page.java.fsm.exception;
public class InvalidActionException extends RuntimeException {
}
复制代码
Switch compared to implementation, implementation of the state set state rule described more intuitive. And more scalable, does not require modification to achieve the roadbed, just add the relevant state description can be.
We know that daily work read code and write code in the proportion of 10: 1, in some scenarios even to 20: 1. Switch every time we need to organize a state of mind in order and rules, and the collection can be very intuitive to express this rule.
01.04 use Enum to implement a state machine
EntranceMachineTest.java
package com.page.java.fsm;
import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;
class EntranceMachineTest {
@Test
void should_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
String result = entranceMachine.execute(Action.INSERT_COIN);
then(result).isEqualTo("opened");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}
@Test
void should_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
String result = entranceMachine.execute(Action.PASS);
then(result).isEqualTo("alarm");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}
@Test
void should_fail_when_execute_invalid_action_given_a_entrance_machine() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);
assertThatThrownBy(() -> entranceMachine.execute(null))
.isInstanceOf(InvalidActionException.class);
}
@Test
void should_refund_when_insert_coin_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);
String result = entranceMachine.execute(Action.INSERT_COIN);
then(result).isEqualTo("refund");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
}
@Test
void should_closed_when_pass_given_a_entrance_machine_with_unlocked_state() {
EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);
String result = entranceMachine.execute(Action.PASS);
then(result).isEqualTo("closed");
then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
}
}
复制代码
EntraceMachine.java
package com.page.java.fsm;
import com.page.java.fsm.exception.InvalidActionException;
import lombok.Data;
import java.util.Objects;
@Data
public class EntranceMachine {
private EntranceMachineState state;
public EntranceMachine(EntranceMachineState state) {
setState(state);
}
public String execute(Action action) {
if (Objects.isNull(action)) {
throw new InvalidActionException();
}
return action.execute(this, state);
}
public String open() {
return "opened";
}
public String alarm() {
return "alarm";
}
public String refund() {
return "refund";
}
public String close() {
return "closed";
}
}
复制代码
Action.java
package com.page.java.fsm;
public enum Action {
PASS {
@Override
public String execute(EntranceMachine entranceMachine, EntranceMachineState state) {
return state.pass(entranceMachine);
}
},
INSERT_COIN {
@Override
public String execute(EntranceMachine entranceMachine, EntranceMachineState state) {
return state.insertCoin(entranceMachine);
}
};
public abstract String execute(EntranceMachine entranceMachine, EntranceMachineState state);
}
复制代码
EntranceMachineState.java
package com.page.java.fsm;
public enum EntranceMachineState {
LOCKED {
@Override
public String insertCoin(EntranceMachine entranceMachine) {
entranceMachine.setState(UNLOCKED);
return entranceMachine.open();
}
@Override
public String pass(EntranceMachine entranceMachine) {
entranceMachine.setState(this);
return entranceMachine.alarm();
}
},
UNLOCKED {
@Override
public String insertCoin(EntranceMachine entranceMachine) {
entranceMachine.setState(this);
return entranceMachine.refund();
}
@Override
public String pass(EntranceMachine entranceMachine) {
entranceMachine.setState(LOCKED);
return entranceMachine.close();
}
};
public abstract String insertCoin(EntranceMachine entranceMachine);
public abstract String pass(EntranceMachine entranceMachine);
}
复制代码
InvalidActionException.java
package com.page.java.fsm.exception;
public class InvalidActionException extends RuntimeException {
}
复制代码
Through the above code, you can find Action, EntranceMachineState complexity of the two enumerations are improved. Not just define constants that simple. Also it provides a corresponding logic.
In the commit record EntranceMachineState.java, performs a reconstruction, to execute specific business logic to move EntranceMachine, the process in each state EntranceMachineState only responsible for scheduling. So what can be done by EntranceMachineState relatively straightforward look, what has become of the state.
Flaw is, EntranceMachine outside a public offer of setState method, which means that the caller maintenance is likely to abuse setState method in the future.
02 summary
By FSM to achieve the above 4, we see that each has advantages and achieve its shortcomings. So in their daily work, how to choose it, I personally think that it can follow two recommendations:
-
Design follows the Simple . Without an external reference, then the use of which can not be overemphasized. So the introduction of a principle as a reference, can help us make better decisions. Here daily work we often use Simple Design: pass the test, revealing intentions, eliminate duplication, at least elements. And continue the reconstruction in the implementation process, the code is reconstructed, rather than one-off designed.
-
Try to do more in the realization of the state machine . Examples of just a simple scene, we can only see the results achieved under simple scenario, the actual state of the business lines will be very rich, and the action in each state line really is different. So for specific problems encountered by scene, to try to practice thinking, experience after practice thinking it is the most important.
reference
- "Agile development practices."