The task is to configure a piece of hardware within the device, according to some input specification. This should be achieved as follows:
1) Collect the configuration information. This can happen at different times and places. For example, module A and module B can both request (at different times) some resources from my module. Those ‘resources’ are actually what the configuration is.
2) After it is clear that no more requests are going to be realized, a startup command, giving a summary of the requested resources, needs to be sent to the hardware.
3) Only after that, can (and must) detailed configuration of said resources be done.
4) Also, only after 2), can (and must) routing of selected resources to the declared callers be done.
A common cause for bugs, even for me, who wrote the thing, is mistaking this order. What naming conventions, designs or mechanisms can I employ to make the interface usable by someone who sees the code for the first time?
3
It’s a redesign but you can prevent misuse of many APIs but not having available any method that shouldn’t be called.
For example, instead of first you init, then you start, then you stop
Your constructor init
s an object that can be started and start
creates a session that can be stopped.
Of course if you have a restriction to one session at a time you need to handle the case where someone tries to create one with one already active.
Now apply that technique to your own case.
5
You can have the startup method return an object that is a required parameter to the configuration:
Resource *MyModule::GetResource(); MySession *MyModule::Startup(); void Resource::Configure(MySession *session);
Even if your MySession
is just an empty struct, this will enforce through type safety that no Configure()
method can be called before the startup.
3
Building on the Answer of Cashcow – why do you have to present a new Object to the caller, when you can just present a new Interface ?
Rebrand-Pattern:
class IStartable { public: virtual IRunnable start() = 0; };
class IRunnable { public: virtual ITerminateable run() = 0; };
class ITerminateable { public: virtual void terminate() = 0; };
You can also let ITerminateable implement IRunnable, if a session can be run multiple times.
Your object:
class Service : IStartable, IRunnable, ITerminateable
{
public:
IRunnable start() { ...; return this; }
ITerminateable run() { ...; return this; }
void terminate() { ...; }
}
// And use it like this:
IStartable myService = Service();
// Now you can only call start() via the interface
IRunnable configuredService = myService.start();
// Now you can also call run(), because it is wrapped in the new interface...
In this way you can only call the right methods, since you have only the IStartable-Interface in the beginning and will get the run() Method only accessible when you have called start(); From the outside it looks like a pattern with multiple classes and Objects, but the underlying class stays one class, which is always referenced.
6
There is a lot of valid approaches to solve your problem. Basile Starynkevitch proposed a “zero-bureaucracy” approach which leaves you with a simple interface and relies on the programmer using appropriately the interface. While I like this approach, I will present another one which has more eingineering but allows the compiler to catch some errors.
-
Identify the various states your device can be in, as
Uninitialised
,
Started
,Configured
and so on. The list has to be finite.¹ -
For each state, define a
struct
holding the necessary additional
information relevant to that state, e.g.DeviceUninitialised
,
DeviceStarted
and so on. -
Pack all treatments in one object
DeviceStrategy
where methods use
structures defined in 2. as inputs and outputs. Thus, you may have
aDeviceStarted DeviceStrategy::start (DeviceUninitalised dev)
method
(or whatever the equivalent might be according to your project conventions).
With this approach, a valid program must call some methods in the sequence enforced by the method prototypes.
The various states are unrelated objects, this is because of the substitution principle. If it is useful to you to have these structures share a common ancestor, recall that the visitor pattern can be used to recover the concrete type of the instance of an abstract class.
While I described in 3. a unique DeviceStrategy
class, there is situations where you may want to split the functionality it provides across several classes.
To summarise them, the key points of the design I described are:
-
Because of the substitution principle, objects representing device states
should be distinct and not have special inheritance relations. -
Pack device treatments in startegy objects rather than in the objects
representing devices themselves, so that each device or device state
sees only itself, and the strategy sees all of them and express possible
transitions between them.
I would swear I saw once a description of a telnet client implementation
following these lines, but I was not able to find it again. It would have
been a very useful reference!
¹: For this, either follow your intuition or find the equivalence classes of methods in your actual implementation for the relation “method₁ ~ method₂ iff. it is valid to use them on the same object” — assuming you have a big object encapsulating all the treatments on your device. Both methods of listing states give fantastic results.
1
Use a builder-pattern.
Have an object which has methods for all the operations you mentioned above. However, it doesn’t perform these operations right away. It just remembers each operation for later. Because the operations aren’t executed right away, the order in which you pass them to the builder doesn’t matter.
After you defined all the operations on the builder, you call an execute
-method. When this method is called, it performs all the steps you listed above in the correct order with the operations you stored above. This method is also a good place to perform some operation-spanning sanity-checks (like trying to configure a resource which wasn’t set up yet) before writing them to the hardware. This might save you from damaging the hardware with a nonsensical configuration (in case your hardware is susceptible to this).
You just need to document correctly how the interface is used, and give a tutorial example.
You may also have a debugging library variant which does some runtime checks.
Perhaps defining and documenting correctly some naming conventions (e.g. preconfigure*
, startup*
, postconfigure*
, run*
….)
BTW, a lot of existing interfaces follow a similar pattern (e.g. X11 toolkits).
1
This is indeed a common and insidious kind of error, because compilers can only enforce syntax conditions, while you need your client programs to be “grammatically” correct.
Unfortunately, naming conventions are almost entirely ineffective against this kind of error. If you really want to encourage people not to do ungrammatical things, you should pass out a command object of some kind that must be initialized with values for the preconditions, so that they can’t perform the steps out of order.
1
public class Executor {
private Executor() {} // helper class
public void execute(MyStepsRunnable r) {
r.step1();
r.step2();
r.step3();
}
}
interface MyStepsRunnable {
void step1();
void step2();
void step3();
}
Using this pattern you are sure that any implementor will execute in this exact order. You can go one step further and make an ExecutorFactory which will build Executors with custom execution paths.
3