GotchaDifficulty: Advanced3 min

Transitions Scattered Across Context and States

Some state changes are made by the context and some by the states, and nobody can draw the diagram.

A transition can be decided by the state classes or by the context, and either works when it is chosen once for the whole design. When some are made in one place and some in the other, nobody can read the state diagram from the code, and a rule in one place runs on a path the other place skips. This card shows that happening, and the fix.

The document's states decide their own transitions. Later, a rule is added: a trusted author's document skips review. A developer adds it to the context, after the state has run.

py
def publish(self) -> None:
    self.state.publish(self)
    if self.trusted_author and isinstance(self.state, Review):
        self.state = Published()

Draft.publish emails the editor to ask for a review, then moves to Review.

What happens when a trusted author publishes a draft? Decide, then look.
py
class Draft:
    def publish(self, document: "Document") -> None:
        print("  email: editor, please review this document")
        document.state = Review()


class Review:
    def publish(self, document: "Document") -> None:
        document.state = Published()


class Published:
    def publish(self, document: "Document") -> None:
        pass


class Document:
    def __init__(self, trusted_author: bool) -> None:
        self.trusted_author = trusted_author
        self.state: Draft | Review | Published = Draft()

    def publish(self) -> None:
        self.state.publish(self)
        if self.trusted_author and isinstance(self.state, Review):
            self.state = Published()


for trusted in (False, True):
    print(f"trusted={trusted}:")
    doc = Document(trusted_author=trusted)
    doc.publish()
    print(f"  ends in {type(doc.state).__name__}")
OutputRan with python:3.12-slim (Python 3.12.13)
trusted=False:
  email: editor, please review this document
  ends in Review
trusted=True:
  email: editor, please review this document
  ends in Published
The trusted document is published, and the editor is still asked to review it.

The document ends in Published, but the editor receives a review request for it. Draft ran first and did its part of a transition to Review, and then the context overrode the result.

The cost is that the transitions now live in two places. Reading the state classes gives a diagram with no arrow from Draft to Published, and the code takes that arrow. Every rule tied to a transition, such as an email, a log line or a permission check, can run for an arrow the object never really took.

The fix is to choose one place. Here the states already decide, so the new rule belongs in Draft.publish, which now chooses between two next states.

py
class Draft:
    def publish(self, document: "Document") -> None:
        if document.trusted_author:
            document.state = Published()
        else:
            print("  email: editor, please review this document")
            document.state = Review()


class Review:
    def publish(self, document: "Document") -> None:
        document.state = Published()


class Published:
    def publish(self, document: "Document") -> None:
        pass


class Document:
    def __init__(self, trusted_author: bool) -> None:
        self.trusted_author = trusted_author
        self.state: Draft | Review | Published = Draft()

    def publish(self) -> None:
        self.state.publish(self)


for trusted in (False, True):
    print(f"trusted={trusted}:")
    doc = Document(trusted_author=trusted)
    doc.publish()
    print(f"  ends in {type(doc.state).__name__}")
OutputRan with python:3.12-slim (Python 3.12.13)
trusted=False:
  email: editor, please review this document
  ends in Review
trusted=True:
  ends in Published
One class decides, so the email is sent only on the path to Review.

Letting the context decide every transition is also sound: the states return a value, and the context holds a table of transitions. It keeps the diagram in one readable place, at the cost of the states knowing less about what follows them. Either choice works when it is made once for the design, and mixing them is what breaks it.