4 kinds of the finite state machine implementation of Java Comparative

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:

  1. 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.

  2. 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."

Guess you like

Origin juejin.im/post/5dff7595f265da33d645bc63