Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to change an object's interface based on its state?

Tags:

c++

oop

Given a fairly complex object with lots of state, is there a pattern for exposing different functionality depending on that state?

For a concrete example, imagine a Printer object.

  • Initially, the object's interface lets you query the printer's capabilities, change settings like paper orientation, and start a print job.

  • Once you start a print job, you can still query, but you can't start another job or change certain printer settings. You can start a page.

  • Once you start a page, you can issue actual text and graphics commands. You can "finish" the page. You cannot have two pages open at once.

  • Some printer settings can be changed only between pages.

One idea is to have one Printer object with a large number of methods. If you call a method at an inappropriate time (e.g., try to change the paper orientation in the middle of a page), the call would fail. Perhaps, if you skipped ahead in the sequence and start issuing graphics calls, the Printer object could implicitly call the StartJob() and StartPage() methods as needed. The main drawback with this approach is that it isn't very easy for the caller. The interface could be overwhelming, and sequence requirements aren't very obvious.

Another idea is to break things up into separate objects: Printer, PrintJob, and Page. The Printer object exposes the query methods and a StartJob() method. StartJob() returns a PrintJob object that has Abort(), StartPage(), and methods for changing just the changeable settings. StartPage() returns a Page object that offers an interface for making the actual graphics calls. The drawback here is one of mechanics. How do you expose the interface of an object without surrendering control of that object's lifetime? If I give the caller a pointer to a Page, I don't want them to delete it, and I can't give them another one until they return the first.

Don't get too hung up on the printing example. I'm looking for the general question of how to present different interfaces based on the object's state.

like image 759
Adrian McCarthy Avatar asked Jan 24 '23 04:01

Adrian McCarthy


1 Answers

Yes, it's called the state pattern.

The general idea is that your Printer object contains a PrinterState object. All (or most) methods on the Printer object simply delegate to the contained PrinterState. You would then have multiple PrinterState classes the implement the methods in different ways depending on what is allowed/not allowed while in that state. The PrinterState implementations would also be provided with a "hook" that allowed them to change the current state of the Printer object to another state.

Here's an example with a couple states. It seem's complicated, but if you have complex state-specific behavior, it actually makes things much easier to code and maintain:

public abstract class PrinterState {
    private PrinterStateContext stateContext;

    public PrinterState( PrinterStateContext context ) {
        stateContext = context;
    }

    void StartJob() {;}
}

public class PrinterStateContext {
     public PrinterState currentState;
}


public class PrinterReadyState : PrinterState {

    public PrinterReadyState( PrinterStateContext context ) {
        super(context);
    }

    void StartJob() {
        // Do whatever you do to start a job..

        // Switch to "printing" state.
        stateContext.currentState = new PrinterPrintingState(stateContext);
    }
}

public class PrinterPrintingState : PrinterState {

    public PrinterPrintingState( PrinterStateContext context ) {
         super(context);
    }

    void StartJob() {
        // Already printing, can't start a new job.
        throw new Exception("Can't start new job, already printing");
    }
}


public class Printer : IPrinter {
    private PrinterStateContext stateContext;

    public Printer() {
        stateContext = new PrinterStateContext();
        stateContext.currentState = new PrinterReadyState(stateContext);
    }

    public void StartJob() {
        stateContext.currentState.StartJob();
    }
}
like image 119
Eric Petroelje Avatar answered Jan 31 '23 18:01

Eric Petroelje