Design patterns in examples - State

Design patterns in examples - State

Active, inactive, saved, verified, sent, completed. Do they seem familiar? If so, you have had a potential use case for State design pattern. Would you like to learn it? Then, let's do it.

 

 

Application

It can be applied in case when you have an object that can be in different states. A transaction can be registered, confirmed or processed. A user can be active or disabled. A document can be draft, signed or archived. A draft document can be signed. A signed one can be archived but not all transitions are allowed. For example a signed document cannot become a draft.

That is not all about rules. Each state have some allowed and disallowed actions, e.g. an already signed document cannot be signed again. A document in draft cannot be printed as an official document with a "signed" stamp.

 

Implementation

State design pattern of course requires a few states. In my case it they are DraftState and SignedState. It is convenient to have an abstract state to have a common interface - I created DocState.

public abstract class DocState {
    protected final Document document;

    public DocState(Document document) {
        this.document = document;
    }

    public abstract void sign();
    public abstract void printOfficial();
}

It has a field to store a context - an object that can exist in different states. In this case it is a document. There are also two abstract methods, you can have more - those are possible operations on the context object (a document) that depend on a state.

 

For simplicity, only two states are possible: draft and signed. Implementation of the first one is as follows:

public class DraftState extends DocState {
    public DraftState(Document document) {
        super(document);
    }

    @Override
    public void sign() {
        document.signDocument();
        document.changeState(new SignedState(document));
    }

    @Override
    public void printOfficial() {
        throw new RuntimeException("Cannot officially print the document in draft state.");
    }
}

When you take a look at the implementation, you can notice that signing a document is possible in the draft state but it cannot be officially printed. Important element to highlight is a fact that the state does not know how to sign the document - it calls signDocument method on the document.

In turn, SignedState looks like this:

public class SignedState extends DocState {
    public SignedState(Document document) {
        super(document);
    }

    @Override
    public void sign() {
        throw new RuntimeException("Already signed");
    }

    @Override
    public void printOfficial() {
        document.printDocumentOfficially();
    }
}

Again, signing is impossible if the document has already been signed. Logic of printing is Document's responsibility.

Would you like to learn Liquibase? Enroll to my course on Udemy.

Promo code: LIQUIBASE_SPRING

liquibase course promo

Curious how Document looks?

// Context
public class Document {
    private DocState state;

    public Document() {
        this.state = new DraftState(this);
    }

    public void sign() {
        state.sign();
    }

    public void printOfficial() {
        state.printOfficial();
    }

    void changeState(DocState newState) {
        this.state = newState;
    }

    void signDocument() {
        // implementation of signing a document
    }

    void printDocumentOfficially() {
        // implementation of printing a document
    }
}

Document exposes methods that are natural actions on a document like sign() and printOfficial(). The document does not call the logic of these actions at this moment, it relies on the states to do what is needed. Those actions are public, anyone can call them. At the same time, three methods are restricted to a package - changeState, signDocument and printDocumentOfficially. These methods are used by states and they should contain real logic of these operations.

Notice that transitions between states are fully handled by states, a document has no clue what is possible and what not - that is natural. Document is responsible for logic of actions: signing and printing - the states know only when these actions are allowed but they don't know how to do it - that is also in line with a common sense.

To make this article complete, see how a document can be used from outside the package.

Document document = new Document();
document.sign();
document.printOfficial();

 

Next time when you encounter this classic use case for state design pattern, use it. Good luck!