Unlocking the Power of DSLs: Stateless State Machines

  • This is also why I wrote this article, to provide a new angle for you to view DSLs, and apply DSLs and state machines.

DSL

The book, Domain-Specific Languages, begins by discussing state machines, and gradually progresses into a deeper understanding of DSLs. I recommend this book to anyone interested in DSLs and state machines. The following sections summarize the main points of the book.

What is a DSL?

“DSLs are a tool whose cutting edge lies in its ability to provide a means to more clearly communicate the intent of a part of a system.”

  • Limited expressiveness: A DSL is a general-purpose programming language that supports varied data, control, and abstract structures. These capabilities are useful, but also make the language harder to learn and use. A DSL supports the bare minimum of features that are required for its domain. A DSL can’t be used to build an entire software system, but it can be employed for one particular aspect of a system.
  • Domain focus: A DSL is a limited language that is only useful if it has a clear focus on a small domain. It is what makes a limited language worthwhile.
/\d{3}-\d{3}-\d{4}/

Categories of DSLs:

DSLs can be divided into three main categories: external DSLs, internal DSLs, and language workbenches. The following are Martin Fowler’s definitions:

builder.externalTransition()
.from(States.STATE1)
.to(States.STATE2)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());

How to Choose between DSLs

You may get a clearer understanding of what type of DSLs to use after learning how they are used differently:

  • External DSLs: They are a suitable choice if you need to perform configuration at runtime, or when code deployment is not needed after configurations. For instance, when you want to add a rule to a rule engine but don’t wish to republish the code afterward.
  • Language workbenches: This type of DSL isn’t user-friendly for making configurations or writing DSL scripts, but it can be useful in certain situations. For example, the promotions and regulations on Taobao require complex settings and must be constantly updated, which is a lot for the sales operations team to manage. We can provide a language workbench to allow them to set rules that take effect immediately.

Fluent Interfaces

We are faced with two choices when writing software libraries: one is to provide command-query APIs, and the other is to provide fluent interfaces. For example, the Mockito API:

when(mockedList.get(anyInt())).thenReturn("element")
when(mockedList.get(anyInt())).thenReturn("element")
String element = mockedList.get(anyInt());
boolean isExpected = "element".equals(element);
OkHttpClient.Builder builder=new OkHttpClient.Builder();
OkHttpClient okHttpClient=builder
.readTimeout(5*1000, TimeUnit.SECONDS)
.writeTimeout(5*1000, TimeUnit.SECONDS)
.connectTimeout(5*1000, TimeUnit.SECONDS)

State Machines

The following sections describe how to implement an internal DSL state machine.

State Machine Selection

As I said earlier, the overuse of processing engines is not a practice I endorse. However, in my view, state machines can be a helpful tool for the following three major reasons:

  • Secondly, using state machine DSLs to track transitions improves the clarity of semantics, and enhances the readability and maintainability of the code.
  • The enum approach only supports transitions between linear states, which are not sufficient in our case.

Open-Source State Machines are Too Complex

Just like processing engines, there are quite a few open-source state machines around. I checked the designs of the top 2 state machine implementations on GitHub, namely Spring Statemachine and Squirrel State Machine. They are both very powerful frameworks, but this can also be a disadvantage.

Open-Source State Machines have Poor Performance

We must acknowledge the fact that these open-source state machines are all stateful, and are by definition, supposed to maintain states. On the other hand, it is because they are stateful that they are not thread-safe. This means every time a state machine accepts a request, our multi-threaded application servers implemented in a distributed environment have to build a new state machine instance.

  • To build a stateless state machine that follows the singleton design pattern to allow all transitions to be handled by a single instance.

Implement the State Machine

State Machine Domain Model

As is illustrated in the following diagram, the core concepts of our lightweight state machine include:

  • Event: the entity that drives state changes
  • Transition: the change from one state to another
  • External Transition: the transition in which the source state is exited and the target state is entered
  • Internal Transition: the transition that executes without exiting or re-entering the state in which it is defined
  • Condition: the condition that allows or stops the transition to a certain state
  • Action: the behavior executed during the triggering of the transition
  • StateMachine: the state machine
//StateMachine
public class StateMachineImpl<S,E,C> implements StateMachine<S, E, C> {
private String machineId;
private final Map<S, State<S,E,C>> stateMap;
...
}
//State
public class StateImpl<S,E,C> implements State<S,E,C> {
protected final S stateId;
private Map<E, Transition<S, E,C>> transitions = new HashMap<>();
...
}
//Transition
public class TransitionImpl<S,E,C> implements Transition<S,E,C> {
private State<S, E, C> source;
private State<S, E, C> target;
private E event;
private Condition<C> condition;
private Action<S,E,C> action;
...
}

Fluent API for Creating the State Machine

I wrote more lines for the builder and the fluent interface than I did for the core code. The following is the code for TransitionBuilder:

class  TransitionBuilderImpl<S,E,C> implements ExternalTransitionBuilder<S,E,C>, InternalTransitionBuilder<S,E,C>, From<S,E,C>, On<S,E,C>, To<S,E,C> {    
...
@Override
public From<S, E, C> from(S stateId) {
source = StateHelper.getState(stateMap,stateId);
return this;
}
@Override
public To<S, E, C> to(S stateId) {
target = StateHelper.getState(stateMap, stateId);
return this;
}
...
}

Stateless Design of the State Machine

This section provides a solution to the performance issue: make the state machine stateless.

Use the State Machine

Using the state machine is as straightforward a process as the implementation. The following code shows the three transitions supported by the cola-statemachine.

StateMachineBuilder<States, Events, Context> builder = StateMachineBuilderFactory.create();
//external transition
builder.externalTransition()
.from(States.STATE1)
.to(States.STATE2)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());
//internal transition
builder.internalTransition()
.within(States.STATE2)
.on(Events.INTERNAL_EVENT)
.when(checkCondition())
.perform(doAction());
//external transitions
builder.externalTransitions()
.fromAmong(States.STATE1, States.STATE2, States.STATE3)
.to(States.STATE4)
.on(Events.EVENT4)
.when(checkCondition())
.perform(doAction());

builder.build(machineId);

Original Source:

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store