The MONOSTATE pattern is another way to achieve singularity. It works through a completely different mechanism. We can see how that mechanism works by studying the Monostate test case in Listing 24-5. The first test function simply describes an object whose x variable can be set and retrieved. But the second test case shows that two instances of the same class behave as though they were one. If you set the x variable on one instance to a particular value, you can retrieve that value by getting the x variable of a different instance. It's as though the two instances are simply different names for the same object. Listing 24-5. Monostate test fixture
If we were to plug the Singleton class into this test case and replace all the new Monostate statements with calls to Singleton.Instance, the test case should still pass. So this test case describes the behavior of Singleton without imposing the constraint of a single instance! How can two instances behave as though they were a single object? Quite simply, it means that the two objects must share the same variables. This is easily achieved by making all the variables static. Listing 24-6 shows the Monostate implementation that passes the preceding test case. Note that the itsX variable is static but that none of the methods are. This is important, as we'll see later. Listing 24-6. Monostate implementation
I find this to be a delightfully twisted pattern. No matter how many instances of Monostate you create, they all behave as though they were a single object. You can even destroy or decommission all the current instances without losing the data. Note that the difference between the two patterns is one of behavior versus structure. The SINGLETON pattern enforces the structure of singularity, preventing any more than one instance from being created. MONOSTATE, by contrast, enforces the behavior of singularity without imposing structural constraints. To underscore this difference, consider that the MONOSTATE test case is valid for the Singleton class but that the SINGLETON test case is not even close to being valid for the Monostate class. Benefits
Costs
MONOSTATE in ActionConsider implementing the simple finite state machine (FSM) for the subway turnstile shown in Figure 24-1. The turnstile begins its life in the Locked state. If a coin is deposited, the turnstile transitions to the Unlocked state and unlocks the gate, resets any alarm state that might be present, and deposits the coin in its collection bin. If a user passes through the gate at this point, the turnstile transitions back to the Locked state and locks the gate. Figure 24-1. Subway turnstile finite state machineThere are two abnormal conditions. If the user deposits two or more coins before passing through the gate, they will be refunded, and the gate will remain unlocked. If the user passes through without paying, an alarm will sound, and the gate will remain locked. The test program that describes this operation is shown in Listing 24-7. Note that the test methods assume that the Turnstile is a monostate and expects to be able to send events and gather queries from different instances. This makes sense if there will never be more than one instance of the Turnstile. The implementation of the monostate Turnstile is in Listing 24-8. The base Turnstile class delegates the two event functions, coin and pass, to two derivatives of Turnstile, Locked and Unlocked, that represent the states of the FSM. Listing 24-7. TurnstileTest
Listing 24-8. Turnstile
This example shows some of the useful features of the MONOSTATE pattern. It takes advantage of the ability for monostate derivatives to be polymorphic and the fact that monostate derivatives are themselves monostates. This example also shows how difficult it can sometimes be to turn a monostate into a nonmonostate. The structure of this solution strongly depends on the monostate nature of Turnstile. If we needed to control more than one turnstile with this FSM, the code would require some significant refactoring. Perhaps you are concerned about the unconventional use of inheritance in this example. Having Unlocked and Locked derived from Turnstile seems a violation of normal OO principles. However, since Turnstile is a monostate, there are no separate instances of it. Thus, Unlocked and Locked aren't really separate objects but instead are part of the Turnstile abstraction. Unlocked and Locked have access to the same variables and methods that Turnstile does. |