-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Do not define actions to be executed inside the state machine (#6)
Do not define actions to be executed inside the state machine, instead (optionally) set the action to be done on the call site. This has several advantages: - The API within the library becomes much smaller and there is less code to maintain - The user of the library can decide if and how to execute certain action on state transitions, which also give a greater freedom on how process and map return types, or what the return types should be. This is stark contrast to the current API where all actions must always process the same type, and may never return a value. - Supporting both suspending functions and non suspending functions is now trivial Some addition changes that worth to mention: - Interceptors defined within the state machine is now removed, since this can quite easily, and with more freedom, be built on top of the existing API offered by this library - A method to get the accepted events for a given state is added to the StateMachine, this can be useful when you want to provide a user with what given actions are legal in the current state of an entity. Here is an example of how the functionality _could_ be extended in a service where fsm is used: ```kotlin // Since this method is inline, it doesn't matter if "action" is suspending or not inline fun <S : Any, E : Any, T : Any> StateMachine<S, E>.onEvent( state: S, event: E, action: (state: S) -> T ): T { return when (val transition = onEvent(state, event)) { is Accepted -> { fsmLog.info { "Transitioned from state $state to ${transition.state} due to event $event" } action(transition.state) } Rejected -> throw InvalidStateException(state, event) } } ``` Creating custom extension methods like this on top of the now simplified API provides much more powerful and flexible conditions for interacting with the state machine.
- Loading branch information
Showing
11 changed files
with
217 additions
and
269 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
146 changes: 52 additions & 94 deletions
146
lib/src/main/kotlin/io/nexure/fsm/StateMachineBuilder.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,108 +1,66 @@ | ||
package io.nexure.fsm | ||
|
||
@Suppress("UNUSED_PARAMETER") | ||
private fun <N : Any> noOp(signal: N) {} | ||
|
||
/** | ||
* - [S] - the type of state that the state machine handles | ||
* - [E] - the type of events that the can trigger state changes | ||
* - [N] - the type of the input used in actions which are executed on state transitions | ||
*/ | ||
class StateMachineBuilder<S : Any, E : Any, N : Any> private constructor( | ||
private var initialState: S? = null, | ||
private val transitions: List<Edge<S, E, N>> = emptyList(), | ||
private val interceptors: List<(S, S, E, N) -> (N)> = emptyList(), | ||
private val postInterceptors: List<(S, S, E, N) -> Unit> = emptyList() | ||
) { | ||
constructor() : this(null, emptyList(), emptyList(), emptyList()) | ||
|
||
/** | ||
* Set the initial state for this state machine. There must be exactly one initial state, | ||
* no more or less. Failing to set an initial state for a state machine will cause an | ||
* [InvalidStateMachineException] to be thrown when [build()] is invoked. | ||
* | ||
* Calling this method more than once, with a different initial state will also cause an | ||
* [InvalidStateMachineException] to be thrown, but immediately upon the second call to this | ||
* method rather when the state machine is built. | ||
*/ | ||
@Throws(InvalidStateMachineException::class) | ||
fun initial(state: S): StateMachineBuilder<S, E, N> { | ||
return if (initialState == null) { | ||
StateMachineBuilder(state, transitions, interceptors, postInterceptors) | ||
} else if (state === initialState) { | ||
StateMachineBuilder(initialState, transitions, interceptors, postInterceptors) | ||
} else { | ||
throw InvalidStateMachineException("There can only be one initial state") | ||
} | ||
sealed class StateMachineBuilder<S : Any, E : Any> { | ||
class Uninitialized<S : Any, E : Any> internal constructor( | ||
private val transitions: List<Edge<S, E>> = emptyList(), | ||
) : StateMachineBuilder<S, E>() { | ||
/** | ||
* Set the initial state for this state machine. There can only one initial state, | ||
* no more or less. | ||
*/ | ||
fun initial(state: S): Initialized<S, E> = Initialized(state, transitions) | ||
} | ||
|
||
/** | ||
* Create a state transition from [source] state to [target] state that will be triggered by | ||
* [event], and execute an optional [action] when doing the state transition. There can be | ||
* multiple events that connect [source] and [target], but there must never be any ambiguous | ||
* transitions. | ||
* | ||
* For example, having both of the following transitions, would NOT be permitted | ||
* - `(S1, E1) -> S2` | ||
* - `(S1, E1) -> S3` | ||
* | ||
* since it would not be clear if the new state should be `S2` or `S3` when event `E1` is | ||
* received. | ||
*/ | ||
fun connect( | ||
source: S, | ||
target: S, | ||
event: E, | ||
action: (signal: N) -> Unit = ::noOp | ||
): StateMachineBuilder<S, E, N> = connect(Edge(source, target, event, action)) | ||
|
||
fun connect(source: S, target: S, event: E, action: Action<N>): StateMachineBuilder<S, E, N> = | ||
connect(source, target, event, action::action) | ||
class Initialized<S : Any, E : Any> internal constructor( | ||
private val initialState: S, | ||
private val transitions: List<Edge<S, E>>, | ||
) : StateMachineBuilder<S, E>() { | ||
/** | ||
* Create a state transition from [source] state to [target] state that will be triggered by | ||
* [event]. There can be multiple events that connect [source] and [target], | ||
* but there must never be any ambiguous transitions. | ||
* | ||
* For example, having both of the following transitions, would NOT be permitted | ||
* - `(S1, E1) -> S2` | ||
* - `(S1, E1) -> S3` | ||
* | ||
* since it would not be clear if the new state should be `S2` or `S3` when event `E1` is | ||
* received. | ||
*/ | ||
fun connect( | ||
source: S, | ||
target: S, | ||
event: E, | ||
): Initialized<S, E> = connect(Edge(source, target, event)) | ||
|
||
private fun connect(edge: Edge<S, E, N>): StateMachineBuilder<S, E, N> = | ||
StateMachineBuilder(initialState, transitions.plus(edge), interceptors, postInterceptors) | ||
private fun connect(edge: Edge<S, E>): Initialized<S, E> = | ||
Initialized(initialState, transitions.plus(edge)) | ||
|
||
/** | ||
* Add an interceptor that is run _before_ any state machine action or state transition is done. | ||
* The interceptor will only be run if the current state permits the event in question. No | ||
* execution of the interceptor will be done if the state is rejected. | ||
*/ | ||
fun intercept( | ||
interception: (source: S, target: S, event: E, signal: N) -> N | ||
): StateMachineBuilder<S, E, N> = | ||
StateMachineBuilder(initialState, transitions, interceptors.plus(interception), postInterceptors) | ||
/** | ||
* @throws InvalidStateMachineException if the configured state machine is not valid. The main | ||
* reasons for a state machine not being valid are: | ||
* - No initial state | ||
* - More than one initial state | ||
* - The state machine is not connected (some states are not possible to reach from the initial | ||
* state) | ||
* - The same source state and event is defined twice | ||
*/ | ||
@Throws(InvalidStateMachineException::class) | ||
fun build(): StateMachine<S, E> { | ||
StateMachineValidator.validate(initialState, transitions) | ||
|
||
/** | ||
* Add an interceptor that is run _after_ a successful processing of an event by the state | ||
* machine. This interceptor will not be run if the event was rejected by the state machine, or | ||
* if there was an exception thrown while executing the state machine action (if any). | ||
*/ | ||
fun postIntercept( | ||
interception: (source: S, target: S, event: E, signal: N) -> Unit | ||
): StateMachineBuilder<S, E, N> = | ||
StateMachineBuilder(initialState, transitions, interceptors, postInterceptors.plus(interception)) | ||
|
||
/** | ||
* @throws InvalidStateMachineException if the configured state machine is not valid. The main | ||
* reasons for a state machine not being valid are: | ||
* - No initial state | ||
* - More than one initial state | ||
* - The state machine is not connected (some states are not possible to reach from the initial | ||
* state) | ||
* - The same source state and event is defined twice | ||
*/ | ||
@Throws(InvalidStateMachineException::class) | ||
fun build(): StateMachine<S, E, N> { | ||
val initState: S = initialState | ||
?: throw InvalidStateMachineException("No initial state set for state machine") | ||
|
||
StateMachineValidator.validate(initState, transitions) | ||
return StateMachineImpl( | ||
initialState, | ||
transitions, | ||
) | ||
} | ||
} | ||
|
||
return StateMachineImpl( | ||
initState, | ||
transitions, | ||
interceptors, | ||
postInterceptors | ||
) | ||
companion object { | ||
operator fun <S : Any, E : Any> invoke(): Uninitialized<S, E> = Uninitialized() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.