tags:

views:

204

answers:

6

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.

+3  A: 

I would go for your separate objects. These need not allow the client to do anything untoward.

IPage Job.getPage()

The IPage interface exposes only waht you need. It's not the "real" Page object, more of a proxy for a Page. Deleting (or going out of scope) of the proxy need have no effect on the real thing.

--- extending in response to comments ---

The first question is: can the object we "hold" change under our feet. The page our proxy referred to is finished printing, does that make our proxy invalid or does it quietly become a proxy for the next page?

The design that makes sense very much depends upon the deep spcefics of the problem domain, argung from my personal knowledge of printers is probably not helpful.

Instead, lets try to abstract the design principles.

So first: separate interfaces for different states does tend to make code easier to write. We just avoid silliness like asking caterpillers to fly and pupae to mate. However, we run in to the problem of how many substates to have ... are hungry caterpillers different from sleeping caterpillers different from very hungry caterpillars?

Hence we proabably still will get some "Can't walk, I'm sleeping" exceptions.

So take the idea futher, don't design an interface that says "Before you call bethod A, you must call method B" ie. a stateful interface. Instead the interface only allows you to call A, and returns a new interface that exposes B.

In the butterfly example this pattern works quite well.

The Page example seems to me to have one extra piece: state changes can happen due to internal events. Hence we had a caterpiller and suddenly it's a butterfly. Now I think we're into a whole different paradigm. It's more event driven. So I think we have a completely different set of design challenges.

Job.registerPageEventListener( me )

and me implements

boolean pageStarted(IPage)

Where perhaps I can return true to say "print it" and false to say "hold it", and then work on it.

djna
+1 because I think this is in the right direction. But there's still the sequencing problem of having one page object (per job) at a time. If the caller calls `getPage` a second time, I could fail because there's a page outstanding, I could return the existing page interface again, or I could finish up the current page and return a new one. All of those options feel like candy-machine interfaces. It seems like there should be a way to make the one-page-at-a-time constraint inherent in the interface.
Adrian McCarthy
+4  A: 

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();
    }
}
Eric Petroelje
Thanks. I agree this is a good pattern for implementation of my first option. (I've been using this pattern without knowing the name of it.) But the drawbacks remain. The interface presented to the user is overwhelming and has lots of non-obvious points of failure when things are called out of sequence. I'm more concerned about presenting a rational interface to the user than I am about how to implement the internals.
Adrian McCarthy
@Adrian - one question I would have then - is it necessary to present an interface at all? I realize the printer example is contrived, but to understand what I'm talking about, what if you created a Document class that the caller would populate with whatever data they wanted to print. Then they simply call a Print() method and let the Printer class worry about properly sequencing the calls. Not sure if that is applicable for your actual application though.
Eric Petroelje
@Eric - Even if a Document class is in charge of printing itself, it still needs an interface to accomplish that.
Adrian McCarthy
A: 

From a protocol view, both possibilities are similar: With both implementations certain calls will be invalid at certain times. In the first case its the call to a function, in the second case its the call to get a linked object.

From a architectural view though its always better, to break big classes apart into smaller and independent ones. This will be better maintainable etc. So i 'd also advice this approach.

RED SOFT ADAIR
+1  A: 

Basically, you seem to need the State pattern: different behavior is modeled by different implementations.

On top of that, you want a convenient interface: the Printer -> Page -> Job separation is a fairly good one, clearly showing the sequence of actions. You can model the fact that they can get invalidated by making them a Proxy to the main object.

The main object can then, upon going to a different state, invalidate all previous proxies. This way you decouple the object's lifetime from it's validity. Deletion of a Proxy would of course also remove it from the main object's list-of-proxies.

xtofl
+1. Good food for thought.
Adrian McCarthy
A: 

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.

This.

You split the object into smaller objects, for each bit of state that is relevant. The problem with Page#delete is that your Page object is an internal component. In that case, you shouldn't expose it directly. Instead create a new class to represent just the state, with any methods that you want to expose. Yes, you'll end up with many fine-grained classes, rather than a few (or a single) big one. That is a good thing (tm).

troelskn
+1  A: 

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

Just to address this point in particular, aside from the rest of the question.

You seem to be talking about an API that looks a bit like this:

Page *Printer::newPage();

I would recommend against that, and in favour of a constructor that looks like this:

Page::Page(Printer &);

That is, do not allocate a Page object in the printer, return it to the caller, and then have to worry about object lifecycle. Instead, surrender control of the lifecycle of objects as a matter of principle, to give your users flexibility. You want the user to start a page, draw stuff to it, then finish the page. So let them do exactly that: create a Page object, draw stuff, see whether it worked, perhaps provide flush and cancel functions, perhaps even also blockUntilDonePrinting and getFailureCode and so on. Then when they're done with the Page they destroy it (or, more likely, they just let it fall out of scope), and then they can create another.

If you do need factories:

Page *PageFactory::newPage(Printer &);

Either way, have the Page itself know what to do with a Printer in order to print things. A Printer is not the same thing as a factory for Pages. Well, actually, it literally is in the real world, but that doesn't mean it should be in software too, since our Page object is not actually a physical page, it's the process of drawing a page. If our Page object just represented a page, then it wouldn't need to interact with the printer object at all - we could construct our Pages, serialise them to postscript, and then worry about how the Printer prints them.

Anyway, the Printer could serve double-duty as a PageFactory, but there are two separate concerns here: (1) manage access to a hardware resource that prints things, and (2) manage the workflow of users creating printable objects in software. Printer doesn't need to do both, so you could separate them.

Likewise for any object with states - separate the object itself (having two or more states), from a session or workflow which progresses through those states.

I don't want them to delete it

It's perfectly fine for an API to say, "the user must not delete the referand of the pointer returned by newPage, but instead must call Printer::close(Page *) with the pointer as a parameter". But as I say in C++, unlike C, you don't have to create APIs like that.

I can't give them another one until they return the first.

I would try to design out this restriction (print queue, anybody?), so that although only one Page is actually printing at any one time, multiple Pages can be created and simultaneously communicate with the Printer driver. But printing is just the example. If a Page really does require exclusive use of the Printer throughout the Page's life (as for example would be the case if we were talking about Mutex and MutexSession rather than Printer and Page), then the Printer should have an API (perhaps public, perhaps accessed via friend, according to whether the Page implementation is intended to be unique for the Printer implementation). Page uses this to acquire exclusive access (call it the "printer token"). If you try to create a Page when another one already exists with the same Printer, then that fails (or blocks, or whatever is appropriate for the problem domain).

Steve Jessop
+1. I started thinking along this lines after I posted the question. I like the idea of having the caller "own" the page object with a constructor that takes the printer (or print job) as an argument.
Adrian McCarthy
I'm surprised this answer didn't get more votes. I'm still working through the one-page-at-a-time issue, but I agree with everything else.
Adrian McCarthy
I'd call it an incomplete answer - everything I say is true, and hopefully useful, but one of the things you asked about was making Printer behave differently according to what state it's in. I haven't addressed that, just the issue of sessions.
Steve Jessop
Perhaps I could have been clearer. My (intended) question was not how to adjust the behavior based on state, but rather how to present an interface that reflects the state. Your response got me on the right track.
Adrian McCarthy