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.
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();
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With