Let’s say that we’re developing an application to manage invoices. Invoices will be created, modified, sent, (hopefully) paid or rejected, …
Depending on the stage that an invoice is in, we can perform different actions on it. For example: it will be very rare that an invoice that was rejected will be paid. Or it shouldn’t be allowed to modify an invoice once it has been sent. So how can we model this in such a way that it becomes easy to develop it, and – in extension – to test it?
Let’s first define some things:
What is a State
A state is a condition during which:
- an object satisfies some condition
- an object performs some activity
- an object waits for some event
Examples of states in the invoicing example can be:
- Creating – We are creating the invoice
- Modifying – After the invoice was created, we modify it
- Sent – The invoice has been sent to the customer, in some way (like e-mail, snail mail)
- Accepted – The customer has accepted the invoice
- Rejected
- Expired – The expiration date of the invoice is passed
- Paid – This should be the end state of an invoice
An event is a significant occurrence:
- Something that happens at a point in time.
- Has no duration.This is not always true. When we send an invoice to the customer, this will take some time. But we consider this as an immediate action.
- One-way transmission of information. Typically nothing is returned from an event.
State Machine Diagram
This simplest form of this diagram is composed of the following elements:
- States: initial state, final state, simple state
- Transitions: are indicated by a labeled arrow
As you can see, with only these 2 elements, we can express a lot already. We see immediately that when an invoice has been sent, it can’t be modified anymore. We also see that there currently are no actions when an invoice is expired or rejected, which may indicate that our analysis is incomplete.
Composite State
When an invoice is expired, we may want to send a reminder, then maybe a second reminder, and then send the invoice to our lawyer. This can all be part of the expired state. So we can make this a composite state, containing sub states. The diagram shows it all. And of course, we can do the same with the rejected state.
Actions
When entering or leaving a state, you may want to perform some actions. In the diagram above, when we enter the “Paid
” state, we always mark the invoice as paid. We put this on all 3 arrows, indicating that this must be done in 3 instances in our code. Not so DRY, is it?
So instead, we create an entry action on the “Paid
” state, that says: “every time we enter this state, we will “Mark Paid
” the invoice.” This has several advantages:
- The diagram becomes simpler (KISS), it contains less overhead.
- We can’t forget the “
Mark Paid
” on one of the arrows. - For the implementation, we now know that entering the “
Paid
” state also means marking the invoice as paid.
- The implementation just became easier. This code can easily be derived from the diagram.
- The tests just became more atomic. We just need to test that the method is called when the state is entered, and we’re good.
This makes the most sense when there are multiple paths into the “Paid
” state.
We can do the same with the exit action, which will be called when we leave the “Paid
” state.
The “do
” action will be called (repeatedly) while in the “Paid
” state.
Conclusion
There is much more to say about State diagrams, but that will be for another post.
Happy drawing!
References