Finite-state machines (FSMs) are awesome. I was first introduced to the concept through the XState JS package. This package is, obviously, geared towards front end work, but the concepts are the same.
Now, I'm no expert in the theory behind FSMs, so there's a good chance I'm getting some of the subtleties wrong here, but the basic idea is this: FSMs are a way to mathematically describe a system that 1) can only be in a single state at any given time, and 2) has a limited set of valid transitions between states.
Let's look at a simple example in Django, using django-fsm's FSMField. Let's say we're building a blog, and we have an
Article model that can be in one of three states:
from django_fsm import FSMField class Article(models.Model): DRAFT = "draft" UNPUBLISHED = "unpublished" PUBLISHED = "published" STATE_CHOICES = ( (DRAFT, "Draft"), (UNPUBLISHED, "Unpublished"), (PUBLISHED, "Published"), ) state = FSMField(choices=STATE_CHOICES, default=DRAFT)
Nothing out of the ordinary yet. But now let's define our transitions.
from django_fsm import FMSField, transition class Article(models.Model): state = FSMField(choices=STATE_CHOICES, default=DRAFT) def finish_draft(self): pass def publish(self): pass
Now we're getting somewhere! But here's the cool bit: the
pass in these methods is not pseudocode that I'm throwing in for this example. These methods will actually transition the model's state. You can also add whatever side effects you want to the methods, and the state will only change if the side effects don't raise an error, e.g.
def finish_draft(self): if not validate_blog_article(): raise ValidationError
(Note the above flow is better done with conditions, but you get the idea.)
You can take this all one step further by protecting your FSM fields:
from django_fsm import FSMField class Article(models.Model): state = FSMField(choices=STATE_CHOICES, default=DRAFT, protected=True)
Now, if you try to modify an
Article's state through direct assignment, you will raise an error.
FSMs are a great way to limit the set of possible states that your objects can be in. And this is a good thing. A small set of possible states means fewer edge cases to worry about (and write tests for). In general, it just becomes easier to reason about your application, what flows are possible, and what states it might be in at any given time.
A few gotchas to be aware of:
transitiondoes not call the model's
savemethod. You must do this manually.
- You cannot call
refresh_from_dbon a model instance with a protected FSMField
- FSMFields do nothing to limit
QuerySet.updatestate changes. Developer discipline (or overwriting the
updatemethod, but I wouldn't) still required.