Blog-Archiv

Montag, 21. Februar 2022

Java Template Driven Comparator

Did you ever have the need to sort a list after a template list?
For instance, you always want to appear "Cut", "Copy", "Paste" menu items in that given order, but your resource reader delivers the item labels in an uncertain random order?

Comparator

Here is a generic Comparator that can sort items according to a template list. Values not found in the template list will be sorted to end.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.Comparator;
import java.util.List;

public abstract class TemplateDrivenComparator<T> implements Comparator<T>
{
    /**
     * @return the list of objects in template sort order.
     */
    protected abstract List<T> template();
    
    @Override
    public int compare(T o1, T o2) {
        // estimate template position for both elements
        int index1 = template().indexOf(o1);
        int index2 = template().indexOf(o2);
        
        // sort unknown to end
        if (index1 == -1)
            index1 = Integer.MAX_VALUE;
        
        if (index2 == -1)
            index2 = Integer.MAX_VALUE;
        
        // try to sort unknown values by optional override
        if (index1 == Integer.MAX_VALUE && index2 == Integer.MAX_VALUE)
            return notInTemplate(o1, o2);
        
        return index1 - index2;
    }
    
    /**
     * Called by <code>compare()</code> when neither of the compared values
     * was in template. To be overridden. This default implementation returns 0.
     * @param o1 first object that was not found in template.
     * @param o2 second object that was not found in template.
     * @return a <code>compare()</code> result for given values. 
     */
    protected int notInTemplate(T o1, T o2) {
        return 0;
    }
}

This is an abstract class that requires you to implement template(), see line 9. The template() implementation must return a list of objects that is in the desired sort order.

The compare() method on line 12 will be called by Collections.sort(list, comparator), see example below. On line 14 and 15 it fetches the indexes of given objects in the template list, which is done by calling equals() in indexOf(). Essentially that is all to be done when returning it as index1 - index2 on line 28. Every list passed to a comparator derived from TemplateDrivenComparator will then have exactly that sort order, given that all elements of the list to sort can be found in the template list.

In case a sort item passed to compare() is not in the template list, it will be sorted to end. This is done by lines 18 to 22. In case both sort items can not be found in the template list, the overridable method notInTemplate() on line 38 will be called. By default it returns zero, which means there is no sort order.

You could now override notInTemplate() to call another TemplateDrivenComparator, this creates a separate sort order at end of list. Or you could extend a base comparator and join its super.template() list to another list to be returned from template(), this allows to put either list to start or to end. Use Stream.concat().collect(Collectors.toList()) for that.

Example

Here is how to use this abstract class:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.*;

public class Main
{
    public static void main(String[] args) {
        final List<String> sequence = new ArrayList<>();
        sequence.add("Copy");
        sequence.add("Paste");
        sequence.add("Cut");
        System.out.println(sequence);
        
        final Comparator<String> comparator = new TemplateDrivenComparator<String>()
        {
            private final List<String> sorted = List.of(
                    "Cut", 
                    "Copy", 
                    "Paste");
            @Override
            protected List<String> template() {
                return sorted;
            }
        };
        Collections.sort(sequence, comparator);
        System.out.println(sequence);
    }
}

Lines 6 to 9 allocate an unordered example list. It is printed out on line 10.

[Copy, Paste, Cut]

The desired order is given by the template list on lines 14 to 17, returned by the TemplateDrivenComparator.template() implementation on line 19.

On line 23 the list is sorted through given comparator, and then printed out on line 24.
Result is:

[Cut, Copy, Paste]

Resume

Hope this was helpful. I couldn't find any example of such a comparator on the web.




Samstag, 12. Februar 2022

Custom State Design Pattern in Java

In this article I want to introduce a custom state machine that refines the classic "Gang-of-Four" State design pattern example that I presented in my last Blog.

For seeing the UML state diagram and the state/event table of the example's workflow, please refer to that article.

Why Customize the Pattern?

Advantage is that you ....

  1. .... have a reusable AbstractStateMachine with a reusable Transition class for implementing many state machines in a standard way

  2. .... don't need the big switch-case statement in StateMachine to associate an action-event with an action-method, because states do not contain action-methods any more, these methods have been replaced by a list of Transition instances

  3. .... have a more flexible inheritance concept that allows to remove action-events (transitions) inherited from a super-class without having to override it (and throw an exception from the override)

  4. .... can provide the possible actions on a follower-state without enumerating implemented action-methods as a set of action-ids (which is logic-duplication)

UML Class Diagrams

The state machine can perform a state transition given by an input state and the ID of an action to perform on that state, and it will return a follower state. The caller is responsible for constructing the input state and determining the ID of the action (event). The state machine provides just one transition at a time, sequences of transitions are up to the caller.

Every state holds a set of possibe transitions, every transition refers to an action (event) ID and a follower-state. So performing a transition is just calling the action of the transition that refers to the ID of the action. The AbstractState provides means for managing inheritance of transitions.

Source Code Files and Folders

All classes in the actions package are the same as in my preceding article, just using different imports, so I didn't repeat them here. They just expose their static ID and do nothing concrete.

Please click onto the expand-controls to see source code of the classes that have not been opened automatically:

statemachine
custom
actions
→ The actions package contains the same classes as in classic state pattern, please see my preceding article.
states
State.java
/**
 * Superclass of all states that defines transitions available in any state.
 */
public class State extends AbstractState
{
    /** @return default transitions that should be possible in all states. */
    @Override
    public Set<Transition> transitions()    {
        return Set.of(
                new Transition(
                    ReadAction.ID,    // allow "read" action in any state,
                    this)    // and stay in that state afterwards
            );
    }
}
InitialState.java
/**
 * After creation, data are in this initial state.
 */
public class InitialState extends State
{
    @Override
    public Set<Transition> transitions()    {
        return extend(
                super.transitions(),    // allow all transitions from super-class, like "read"
                new Transition(
                    CreateAction.ID,    // initial state must accept "create" action
                    this),    // stay in this state after creation
                new Transition(
                    SaveAction.ID,    // can save data
                    this),    // stay in this state afterwards
                new Transition(
                    DeleteAction.ID,    // can delete
                    new DeletedState()),    // go to deleted state state afterwards
                new Transition(
                    ReleaseAction.ID,    // can publish
                    new ReleasedState())    // go to published state state afterwards
            );
    }
}
DraftState.java
/**
 * After publishing, data can be brought back to a draft state.
 * Nearly identical to initial state, but it doesn't allow "create" action.
 */
public class DraftState extends InitialState
{
    @Override
    public Set<Transition> transitions()    {
        return remove(
                super.transitions(),    // inherit all super-transitions,
                CreateAction.ID    // but don't allow "create" action
            );
    }
}
ReleasedState.java
/**
 * From initial state or draft state, data can be published.
 */
public class ReleasedState extends State
{
    @Override
    public Set<Transition> transitions()    {
        return extend(
                super.transitions(),
                new Transition(
                        EditAction.ID,    // "edit" is supported in published state only
                        new DraftState()),    // go to draft state afterwards
                new Transition(
                        DeleteAction.ID,    // "delete" is supported
                        new DeletedState())    // go to deleted state afterwards
            );
    }
}
DeletedState.java
/**
 * Doesn't provide any transition.
 */
public class DeletedState extends State
{
    @Override
    public Set<Transition> transitions()    {
        return Set.of();    // support no further actions and states
    }
}
StateMachine.java
public class StateMachine extends AbstractStateMachine
{
    @Override
    public Set<AbstractAction> getActions() {
        return Set.of(
                new CreateAction(),
                new ReadAction(),
                new DeleteAction(),
                new SaveAction(),
                new ReleaseAction(),
                new EditAction()
            );
    }
}
AbstractAction.java
AbstractAction is the same as in classic state pattern, please see my preceding article.
AbstractState.java
/**
 * Reusable superclass for all states. A state aggregates transitions.
 */
public abstract class AbstractState
{
    /**
     * @return transitions that should be possible in this state,
     *         or an empty set when none, but never null.
     */
    public abstract Set<Transition> transitions();
    
    /**
     * @return all possible actions to be performed on this state,
     *         derived from the action-ids in all possible transitions.
     */
    public final Set<String> actionIds()    {
        final Set<String> actionIds = new HashSet<>();
        for (Transition transition : transitions())
            actionIds.add(transition.actionId);
        
        return Collections.unmodifiableSet(actionIds);
    }
    
    /**
     * Convenience method to avoid code duplications.
     * @param inheritedTransitions required, use <i>super.transitions()</i> for that.
     * @param moreTransitions required, further transitions to add.
     * @return the union of <i>inheritedTransitions</i> and <i>moreTransitions</i>.
     */
    protected final Set<Transition> extend(Set<Transition> inheritedTransitions, Transition... moreTransitions)    {
        final Set<Transition> extended = new HashSet<>();
        
        for (Transition inheritedTransition : inheritedTransitions)
            addButAssertNotContained(extended, inheritedTransition);
        
        for (Transition moreTransition : moreTransitions)
            addButAssertNotContained(extended, moreTransition);
        
        return Collections.unmodifiableSet(extended);
    }
    
    /**
     * Convenience method to avoid code duplications.
     * @param inheritedTransitions required, use <i>super.transitions()</i> for that.
     * @param actionIdsToRemove required, action-ids referencing transitions to remove.
     * @return a new set consisting of the <i>inheritedTransitions</i> minus those
     *         transitions that reference an action contained in <i>actionsToRemove</i>.
     */
    protected final Set<Transition> remove(Set<Transition> inheritedTransitions, String... actionIdsToRemove)    {
        final Set<String> toRemove = Set.of(actionIdsToRemove);
        final Set<Transition> reduced = new HashSet<>();
        
        for (Transition inheritedTransition : inheritedTransitions)
            if (false == toRemove.contains(inheritedTransition.actionId))
                addButAssertNotContained(reduced, inheritedTransition);
        
        return Collections.unmodifiableSet(reduced);
    }
    
    /** @return the hash of this class. */
    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
    
    /** @return the hash of this class. */
    @Override
    public boolean equals(Object o) {
        return getClass().equals(o.getClass());
    }


    private void addButAssertNotContained(Set<Transition> set, Transition transition) {
        if (set.contains(transition))
            throw new IllegalArgumentException(
                "Transitions referring to the same action are not allowed in the same state: "+
                transition.actionId);
        
        set.add(transition);
    }
}
AbstractStateMachine.java
/**
 * Reusable abstraction of a state machine.
 */
public abstract class AbstractStateMachine
{
    /** @return the actions this state machine supports. */
    protected abstract Set<AbstractAction> getActions();

    /**
     * Transitions from given state according to given action, performing that action.
     * @return the follower state resulting from done transition.
     */
    public AbstractState transition(AbstractState state, String actionId)    {
        for (Transition transition : state.transitions())
            if (transition.actionId.equals(actionId))
                return perform(state, transition, actionId);
        
        throw new IllegalStateException(
                "No transition in state "+state.getClass().getSimpleName()+
                " can dispatch action "+actionId);
    }

    /** @return the ids of actions that can be performed on given state. */
    public Set<String> followerActionIds(AbstractState followerState)    {
        return followerState.actionIds();
    }


    private AbstractState perform(AbstractState oldState, Transition transition, String actionId) {
        final AbstractAction action = findAction(actionId);
        final AbstractState newState = transition.followerState;
        action.perform(oldState, newState);
        return newState;
    }

    private AbstractAction findAction(String actionId) {
        for (AbstractAction action : getActions())
            if (action.getId().equals(actionId))
                return action;
        
        throw new IllegalArgumentException("No action found for id "+actionId);
    }
}
Transition.java
/**
 * A transition connects exactly one action with one follower state,
 * and it represents exactly one state/event table cell.
 * Transitions are unique within one state, identified by their actionId,
 * so that one state can never have two transitions with same actionId.
 */
public class Transition
{
	public final String actionId;
	public final State followerState;
	
	public Transition(String actionId, State followerState) {
		this.actionId = Objects.requireNonNull(actionId);
		this.followerState = Objects.requireNonNull(followerState);
	}
	
	/** @return hash of actionId. */
	@Override
	public int hashCode() {
		return actionId.hashCode();
	}
	
	/** @return true if local actionId is equal to that in given transition. */
	@Override
	public boolean equals(Object o) {
		final Transition other = (Transition) o;
		return actionId.equals(other.actionId);
	}
}

Source Descriptions

The essential difference to the classic State pattern is that action-events are no more implemented as methods in a state but represented by a list of transitions.

Transitions

A transition connects exactly one action-id with exactly one follower state, and it represents exactly one state/event table cell. Transitions are identified by their action-id, because they must be unique within one state (table column), so that one state can never have two transitions with the same action-id. (If your workflow requires such, you should consider to spawn another state that inherits from its parent.)

Transitions do not carry the action instance directly, they just refer to its ID, so that the AbstractStateMachine can perform a given action-id when it knows all available connected action classes. This is what happens in AbstractStateMachine.transition() method. The set of actions is provided by its workflow-specific sub-class StateMachine in getActions() method.

Using transitions makes it easy to determine follower action-ids. You just have to enumerate all transitions of the follower-state and collect their actionids. This is what happens in AbstractStateMachine.followerActionIds() method.

States

When you look at AbstractState, you see the protected utility methods extend() and remove(). They serve to reuse a set of transitions inherited from a super-class. (Being able to inherit from another state is an important feature I would never want to miss, it prevents lots of code duplications!)

In our workflow it is the DraftState that inherits from InitialState (because these two are almost identical), and then uses remove() to get rid of the "create" action.

  • The super-class State declares that the "read" action is possible in any state (should not the sub-class remove it)
  • Then InitialState extends it and adds "create", "save", "delete" and "release" actions
  • As DraftState doesn't allow "create", it inherits all actions from InitialState but removes CreateAction.ID

That way you can maintain the transitions in InitialState without having to care for DraftState, and vice-versa!

The methods extend() and remove() also check that no ambiguous transition gets added, by calling addButAssertNotContained(). This state machine implementation does not allow to have two transitions with same action-id in one state. That means, any action-id has exactly one follower-state in the context of its state.

State Machine

The AbstractStateMachine is a base class for many different state machines that can then be written in a standard way. It requires that all possible actions are defined by a sub-class, everything else can be done like implemented in its very simple private perform() method.

When you look at the concrete StateMachine class that was made for the documentation-workflow, you see that it provides the action instances for that workflow.

Mind that the state machine is the only one that knows concrete action classes, all other participants just use action-IDs. Not even the caller of the state machine knows the action classes or can determine them. That means there is a "loose binding" between transitions and actions, in other words, you could provide different action semantics for a workflow by using the same transitions but different action classes. Just override StateMachine.getActions(), or use a protected factory method for action instances there.

Resume

I hope I could show how the state design pattern can be customized for better code reusage. The difference to the classic variant is not so big. Using the Transition API instead of defining action-methods is just a matter of habit change.




Dienstag, 8. Februar 2022

Classic State Design Pattern in Java

The "State" Design Pattern is a quite complicated pattern. It implements a finite state machine, also called automaton. This article contains a small example how this can be implemented in Java, following the famous "Gang-of-Four" design patterns book.

I want to outline also the weaknesses of this example implementation, and I plan to introduce a more sophisticated one in a follow-up article, for comparing the two afterwards.

States and Actions

A state is a long-lasting condition a system is in.
An action is an atomic event that brings a system into a new state.
State machines combine states and actions to control workflows. The term "event" (causing a state transition, → state/event table) was replaced by "action-id" in the context of this article.

Workflow Example

My example state machine represents a workflow for e.g. online documentations. Such a documentation will be created, edited and saved several times, then released for public viewing. Maybe it will get deleted before being released, because it is of no use any more. A released documentation could be drawn back to be edited again, I called this "draft". The draft may be released once more, or even deleted.

Thus we have following states:

  • initial (after creation, before first release)
  • released
  • draft (drawn back from release)
  • deleted

With following actions:

  • create (generate or clear, initialize with defaults)
  • save
  • read (load for modifications)
  • delete
  • release
  • edit (draw back to draft)

The only difference between the initial and the draft state is that the draft state can not dispatch a create-action.

Mind that not every action leads to a new state. For instance, "save" and "read" actions would not alter the state, an initial documentation would stay in initial state, an edited one would stay in draft state.

State Machine Illustrations

A state-machine manages states and actions and allows to control which action is legal in which state. Here is our example's UML diagram:

The table below is a more accurate display. Every table cell represents a possible state transition to consider. Should the transition be illegal, put an "X" into it. Else define the action to perform, and the follower state to go to. If there is no follower state, the action causes no state change, this is legal. When no table cell is empty any more, you can transfer the state-machine to source code!

States
Actions
Initial Released Draft Deleted
Create CreateAcion → Initial X X X
Save SaveAction X SaveAction X
Read ReadAction ReadAction ReadAction X
Release ReleaseAction → Released X ReleaseAction → Released X
Edit X EditAction → Draft X X
Delete DeleteAction → Deleted DeleteAction → Deleted DeleteAction → Deleted X

The header row enumerates all states, the first column in every row defines the arriving action. Any table cell then represents an arriving action in the state defined by the column header. Every possible combination of action and state is visible in this table. Empty "X" cells represent illegal combinations and should cause an exception.

Mind that "Initial" and "Draft" states are almost identical, just the "Create" action is not allowed in "Draft" state. This is sign that we will need some kind of code reusage on states.

Source Code Files and Folders

How to bring this into Java source code?
Very simple:

  • → every state is represented by a class,
  • → every action by a method in one or more state-classes.

Please click onto the expand controls below to see Java source code!

statemachine
classic
actions
Action.java
/**
 * Action events move a system from one state to another.
 * They always perform some work.
 */
public abstract class Action extends AbstractAction
{
    public Object perform(AbstractState oldState, AbstractState newState) {
        throw new RuntimeException("Implement me!");
    }
}
CreateAction.java
/**
 * Generates data when not exists, else clears it ("reset").
 * In any case it establishes default values.
 */
public class CreateAction extends Action
{
    public static final String ID = "create";

    @Override
    public String getId() {
        return ID;
    }
}
DeleteAction.java
/**
 * Pushes data into a deleted state.
 */
public class DeleteAction extends Action
{
    public static final String ID = "delete";

    @Override
    public String getId() {
        return ID;
    }
}
EditAction.java
/**
 * Brings published data back to draft state.
 */
public class EditAction extends Action
{
    public static final String ID = "edit";

    @Override
    public String getId() {
        return ID;
    }
}
ReadAction.java
/**
 * Reads data from persistence.
 */
public class ReadAction extends Action
{
    public static final String ID = "read";

    @Override
    public String getId() {
        return ID;
    }
}
ReleaseAction.java
/**
 * Publishes initial or draft data.
 */
public class ReleaseAction extends Action
{
    public static final String ID = "release";

    @Override
    public String getId() {
        return ID;
    }
}
SaveAction.java
/**
 * Saves data to persistence.
 */
public class SaveAction extends Action
{
    public static final String ID = "save";

    @Override
    public String getId() {
        return ID;
    }
}
states
State.java
/**
 * Superclass of all states.
 * A state contains one method for every action-event it supports.
 */
public abstract class State extends AbstractState
{
    @Override
    protected Set<Action> getActions() {
        return Set.of(
            new CreateAction(),
            new DeleteAction(),
            new EditAction(),
            new ReadAction(),
            new ReleaseAction(),
            new SaveAction()
        );
    }
    
    public State create()    {
        throw exception(CreateAction.ID);
    }
    public State delete()    {
        throw exception(DeleteAction.ID);
    }
    public State edit()    {
        throw exception(EditAction.ID);
    }
    public State read()    {
        final State followerState = this;
        performAction(ReadAction.ID, followerState);
        return followerState;
    }
    public State release()    {
        throw exception(ReleaseAction.ID);
    }
    public State save()    {
        throw exception(SaveAction.ID);
    }
    
    /**
     * @return all possible actions to be performed on this state,
     *         derived from all action-methods that do not throw exception.
     */
    public Set<String> actionIds() {
        return Set.of(
                ReadAction.ID);
    }
}
DeletedState.java
public class DeletedState extends State
{
    @Override
    public State read()    {
        throw new IllegalStateException("Illegal action 'read' in state "+getClass().getSimpleName());
    }
    
    
    @Override
    public Set<String> actionIds() {
        return Set.of();    // no actions when deleted
    }
}
DraftState.java
/**
 * After publishing, data can be brought back to a draft state.
 * Nearly identical to initial state, but it doesn't allow "create" action.
 */
public class DraftState extends InitialState
{
    @Override
    public State create() {
        throw exception(CreateAction.ID);
    }
    
    
    @Override
    public Set<String> actionIds() {
        return remove(
                super.actionIds(), 
                CreateAction.ID
            );
    }
}
InitialState.java
/**
 * After creation, data are in this initial state.
 */
public class InitialState extends State
{
    @Override
    public State create() {
        final State followerState = this;
        performAction(CreateAction.ID, followerState);
        return followerState;
    }
    
    @Override
    public State save() {
        final State followerState = this;
        performAction(SaveAction.ID, followerState);
        return followerState;
    }
    
    @Override
    public State delete() {
        final State followerState = new DeletedState();
        performAction(DeleteAction.ID, followerState);
        return followerState;
    }
    
    @Override
    public State release() {
        final State followerState = new ReleasedState();
        performAction(ReleaseAction.ID, followerState);
        return followerState;
    }
    
    
    @Override
    public Set<String> actionIds() {
        return extend(
                super.actionIds(),    // read
                CreateAction.ID, 
                SaveAction.ID, 
                DeleteAction.ID,
                ReleaseAction.ID 
            );
    }
}
ReleasedState.java
/**
 * From initial state or draft state, data can be published.
 */
public class ReleasedState extends State
{
    @Override
    public State edit() {
        final State followerState = new DraftState();
        performAction(EditAction.ID, followerState);
        return followerState;
    }
    
    @Override
    public State delete() {
        final State followerState = new DeletedState();
        performAction(DeleteAction.ID, followerState);
        return followerState;
    }


    @Override
    public Set<String> actionIds() {
        return extend(
                super.actionIds(),    // read
                EditAction.ID,
                DeleteAction.ID
            );
    }
}
StateMachine.java
public class StateMachine
{
    /**
     * Transitions from given state according to given action, performing that action.
     * @return the follower state resulting from done transition.
     */
    public State transition(State state, String actionId)    {
        switch (actionId)    {
            case CreateAction.ID:
                return state.create();
            case DeleteAction.ID:
                return state.delete();
            case EditAction.ID:
                return state.edit();
            case ReadAction.ID:
                return state.read();
            case ReleaseAction.ID:
                return state.release();
            case SaveAction.ID:
                return state.save();
            default:
                throw new IllegalArgumentException("No action found for id "+actionId);
        }
    }

    /** @return the ids of actions that can be performed on given state. */
    public Set<String> followerActionIds(State followerState)    {
        return followerState.actionIds();
    }
}
AbstractAction.java
/**
 * Action events move a system from one state to another.
 * They always perform some work.
 */
public abstract class AbstractAction
{
    public abstract String getId();
    
    public abstract Object perform(AbstractState oldState, AbstractState newState);
}
AbstractState.java
/**
 * Reusable superclass for all states.
 */
public abstract class AbstractState
{
    /** @return all actions from all states. */
    protected abstract Set<Action> getActions();
    
    /** Performs given action in context of this state and given follower-state. */
    protected final Object performAction(String actionId, AbstractState followerState)    {
        for (Action action : getActions())
            if (action.getId().equals(actionId))
                return action.perform(this, followerState);
        
        throw new IllegalArgumentException("Action not found: "+actionId);
    }
    
    /**
     * Convenience method to avoid code duplications.
     * @param inheritedActions required, use <i>super.actionIds()</i> for that.
     * @param moreTransitions required, further actions to add.
     * @return the union of <i>inheritedActions</i> and <i>moreActions</i>.
     */
    protected final Set<String> extend(Set<String> inheritedActions, String... moreActions)    {
        final Set<String> extended = new HashSet<>();
        
        extended.addAll(inheritedActions);
        
        for (String moreAction : moreActions)
            extended.add(moreAction);
        
        return Collections.unmodifiableSet(extended);
    }
    
    /**
     * Convenience method to avoid code duplications.
     * @param inheritedActions required, use <i>super.actionIds()</i> for that.
     * @param actionsToRemove required, action-ids to remove.
     * @return a new set consisting of the <i>inheritedActions</i> minus those
     *         transitions that reference an action contained in <i>actionsToRemove</i>.
     */
    protected final Set<String> remove(Set<String> inheritedActions, String... actionsToRemove)    {
        final Set<String> toRemove = Set.of(actionsToRemove);
        final Set<String> reduced = new HashSet<>();
        
        for (String inheritedAction : inheritedActions)
            if (false == toRemove.contains(inheritedAction))
                reduced.add(inheritedAction);
        
        return Collections.unmodifiableSet(reduced);
    }
    
    protected final RuntimeException exception(String actionId)    {
        return new IllegalStateException("Illegal action '"+actionId+"' in state "+getClass().getSimpleName());
    }
    
    @Override
    public int hashCode() {
        return getClass().hashCode();
    }
    
    @Override
    public boolean equals(Object o) {
        return getClass().equals(o.getClass());
    }
}

Before talking about sources above, here is a snippet showing how this state machine can be used:

    @Test
    public void matchFollowerActionsOfReleaseOnInitialState_shouldSucceed()    {
        final StateMachine stateMachine = new StateMachine();
        final State followerState = stateMachine.transition(new InitialState(), ReleaseAction.ID);
        final Set<String> followerActionIds = stateMachine.followerActionIds(followerState);
        
        assertEquals(3, followerActionIds.size());
        assertTrue(followerActionIds.contains(ReadAction.ID));
        assertTrue(followerActionIds.contains(EditAction.ID));
        assertTrue(followerActionIds.contains(DeleteAction.ID));
    }

Mind that it is an important feature of a state machine to provide follower-actions to a caller. Such a caller may be a UI-client that wants to build action buttons with these action-ids.

Source Descriptions

The most important thing to know is that a state machine is understandable and controllable much better from its table representation than from source code. You always should have some tech-doc where you think about and perform your changes, only then transfer the change to source code.

The core concept of this implementation is that every state extends a class that defines all possible action-methods and throws an exception from them (see State.java). Every state class that extends it then overrides only those methods that it actually supports. All other action-method calls on that state would cause an exception. This handles illegal state-action combinations, rendered as empty "X" table cells.

Actions

The Action classes may not be very interesting, because they just provide their IDs, here they do nothing. A real world action would need a constructor context that enables it to read and write a documentation and its state.

Keep in mind that any Action class holds a static ID and provides it through a getId() instance method, and that this class lives in a method of same name that performs that action. (You can see this e.g. in State.read(), which is a default action available in any state.) Thus an action is represented three times, by XxxAction.ID (usable as "event"), by XxxAction.class (actual work), and by action-method xxx() in 1-n states. (Not really beautiful.)

States

The states extend the State class that again extends AbstractState, which can be reused by different state machines. States represent table columns. If an action-event arrives, you would construct the documentation's state, call a method relating to that action-event on it, and get back a follower state that also provides follower actions. Thus you have to relate the arriving action-event (ID) to an action-method in current state. How do that? You see it in StateMachine.transition() where a big switch-case statement associates the two. (Not really beautiful.)

Next station is the method's body that determines a follower state and performs an action according to the method's name. Look at all the action-methods in InitialState like create(). They all do the same but use different action classes and IDs. (Not really beautiful.) The follower state is returned to the calling state machine, which then ends by returning that to its caller.

Another thing is the actionIds() responsibility in State. This is needed to determine the follower action IDs of a state. So any state class must enumerate all its action-methods also as an action-ID set. (Not really beautiful.)

Utilities

There are utilities in AbstractState that let us reuse super-implementations without looking into them: extend() and remove(). The first allows to add all supported action-ids of the super-class, the second allows to remove certain action-ids from the set defined by super-class. This is demonstrated in InitialState.actionIds() and DraftState.actionIds().

InitialState inherits "read" from super-class:

    @Override
    public Set<String> actionIds() {
        return extend(
                super.actionIds(),    // read
                CreateAction.ID, 
                SaveAction.ID, 
                DeleteAction.ID,
                ReleaseAction.ID 
            );
    }

DraftState supports all action-methods of the super-class except "create":

    @Override
    public Set<String> actionIds() {
        return remove(
                super.actionIds(), 
                CreateAction.ID
            );
    }

Resume

This state machine example implementation cearly shows the weaknesses of the state design pattern: there are too many symbolic references.

  1. Action-ID to action-method in StateMachine.transition()
  2. Action-method back to action-ID in perform-calls of create(), save(), release() etc.
  3. Action-methods enumerated as action-IDs in State.actionIds()
  4. When a state inherits an action-method from super-class but can not allow it, it must (1) override the action-method to throw an exception, and (2) remove the according action-ID from the set of supported action-IDs, as to be seen in DraftState.

It would be even worse if there were no utility methods like extend() and remove() that make it possible to reuse enumerations of super-classes, like provided in AbstractState.

In my next Blog I will introduce an implementation that may look more difficult but removes most of the redundancies listed here and thus actually is simpler.