24. Singleton and Monostate

image

© Jennifer M. Kohnke

Infinite beatitude of existence! It is; and there is none else beside It.

—Edwin A. Abbott, Flatland (1884)

Usually, there is a one-to-many relationship between classes and instances. You can create many instances of most classes. The instances are created when they are needed and are disposed of when their usefulness ends. They come and go in a flow of memory allocations and deallocations.

But some classes should have only one instance. That instance should appear to have come into existence when the program started and should be disposed of only when the program ends. Such objects are sometimes the roots of the application. From the roots, you can find your way to many other objects in the system. Sometimes, these objects are factories, which you can use to create the other objects in the system. Sometimes, these objects are managers, responsible for keeping track of certain other objects and driving them through their paces.

Whatever these objects are, it is a severe logic failure if more than one of them is created. If more than one root is created, access to objects in the application may depend on a chosen root. Programmers, not knowing that more than one root exists, may find themselves looking at a subset of the application objects without knowing it. If more than one factory exists, clerical control over the created objects may be compromised. If more than one manager exists, activities that were intended to be serial may become concurrent.

It may seem that mechanisms to enforce the singularity of these objects is overkill. After all, when you initialize the application, you can simply create one of each and be done with it.1 In fact, this is usually the best course of action. Such a mechanism should be avoided when there is no immediate and significant need. However, we also want our code to communicate our intent. If the mechanism for enforcing singularity is trivial, the benefit of communication may outweigh the cost of the mechanism.

This chapter is about two patterns that enforce singularity. These patterns have very different cost/benefit trade-offs. In most contexts, their cost is low enough to more than balance the benefit of their expressiveness.

Singleton

SINGLETON is a very simple pattern.2 The test case in Listing 24-1 shows how it should work. The first test function shows that the Singleton instance is accessed through the public static method Instance and that if Instance is called multiple times, a reference to the exact same instance is returned each time. The second test case shows that the Singleton class has no public constructors, so there is no way for anyone to create an instance without using the Instance method.


Listing 24-1. Singleton test case

using System;
using System.Reflection;
using NUnit.Framework;

[TestFixture]
public class TestSimpleSingleton

{
  [Test]
  public void TestCreateSingleton()
  {
    Singleton s = Singleton.Instance;
    Singleton s2 = Singleton.Instance;
    Assert.AreSame(s, s2);
  }

  [Test]
  public void TestNoPublicConstructors()
  {
    Type singleton = typeof(Singleton);
    ConstructorInfo[] ctrs = singleton.GetConstructors();
    bool hasPublicConstructor = false;
    foreach(ConstructorInfo c in ctrs)
    {
      if(c.IsPublic)
      {
        hasPublicConstructor = true;
        break;
      }
    }
    Assert.IsFalse(hasPublicConstructor);
  }
}


This test case is a specification for the SINGLETON pattern and leads directly to the code shown in Listing 24-2. By inspecting this code, it should be clear that there can never be more than one instance of the Singleton class within the scope of the static variable Singleton.theInstance.


Listing 24-2. Singleton implementation

public class Singleton
{
  private static Singleton theInstance = null;
  private Singleton() {}

  public static Singleton Instance
  {
    get
    {
      if (theInstance == null)
        theInstance = new Singleton();
      return theInstance;
    }
  }
}


Benefits

Cross-platform: Using appropriate middleware (e.g., Remoting), SINGLETON can be extended to work across many CLRs (Common Language Runtime) and many computers.

Applicable to any class: You can change any class into a SINGLETON simply by making its constructors private and adding the appropriate static functions and variable.

Can be created through derivation: Given a class, you can create a subclass that is a SINGLETON.

Lazy evaluation: If the SINGLETON is never used, it is never created.

Costs

Destruction undefined: There is no good way to destroy or decommission a SINGLETON. If you add a decommission method that nulls out theInstance, other modules in the system may still be holding a reference to the SINGLETON. Subsequent calls to Instance will cause another instance to be created, causing two concurrent instances to exist. This problem is particularly acute in C++, in which the instance can be destroyed, leading to possible dereferencing of a destroyed object.

Not inherited: A class derived from a SINGLETON is not a SINGLETON. If it needs to be a SINGLETON, the static function and variable need to be added to it.

Efficiency: Each call to Instance invokes the if statement. For most of those calls, the if statement is useless.

Nontransparent: Users of a SINGLETON know that they are using it, because they must invoke the Instance method.

SINGLETON in Action

Assume that we have a Web-based system that allows users to log in to secure areas of a Web server. Such a system will have a database containing user names, passwords, and other user attributes. Assume further that the database is accessed through a third-party API. We could access the database directly in every module that needed to read and write a user. However, this would scatter usage of the third-party API throughout the code and would leave us no place to enforce access or structure conventions.

A better solution is to use the FACADE pattern and create a UserDatabase class that provides methods for reading and writing User objects.3 These methods access the third-party API of the database, translating between User objects and the tables and rows of the database. Within the UserDatabase, we can enforce conventions of structure and access. For example, we can guarantee that no User record gets written unless it has a nonblank username. Or, we can serialize access to a User record, making sure that two modules cannot simultaneously read and write it.

The code in Listings 24-3 and 24-4 show a SINGLETON solution. The SINGLETON class is named UserDatabaseSource and implements the UserDatabase interface. Note that the static Instance() method does not have the traditional if statement to protect against multiple creations. Instead, it takes advantage of the .NET initialization facility.


Listing 24-3. UserDatabase interface

public interface UserDatabase
{
  User ReadUser(string userName);
  void WriteUser(User user);
}



Listing 24-4. UserDatabase Singleton

public class UserDatabaseSource : UserDatabase
{
  private static UserDatabase theInstance =
    new UserDatabaseSource();

  public static UserDatabase Instance
  {
    get
    {
      return theInstance;
    }
  }

  private UserDatabaseSource()
  {
  }

  public User ReadUser(string userName)
  {
    // Some Implementation
  }

  public void WriteUser(User user)
  {
    // Some Implementation
  }
}


This is an extremely common use of the SINGLETON pattern. It ensures that all database access will be through a single instance of UserDatabaseSource. This makes it easy to put checks, counters, and locks in UserDatabaseSource to enforce the access and structure conventions mentioned earlier.

Monostate

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

using NUnit.Framework;

[TestFixture]
public class TestMonostate
{
  [Test]
  public void TestInstance()
  {
    Monostate m = new Monostate();
    for (int x = 0; x < 10; x++)
    {
      m.X = x;
      Assert.AreEqual(x, m.X);
    }
  }

  [Test]
  public void TestInstancesBehaveAsOne()
  {
    Monostate m1 = new Monostate();
    Monostate m2 = new Monostate();

    for (int x = 0; x < 10; x++)
    {
      m1.X = x;
      Assert.AreEqual(x, m2.X);
    }
  }
}


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

public class Monostate
{
  private static int itsX;

  public int X
  {
    get { return itsX; }
    set { itsX = value; }
  }
}


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

Transparency: Users do not behave differently from users of a regular object. The users do not need to know that the object is monostate.

Derivability: Derivatives of a monostate are monostates. Indeed, all the derivatives of a monostate are part of the same monostate. They all share the same static variables.

Polymorphism: Since the methods of a monostate are not static, they can be overridden in a derivative. Thus, different derivatives can offer different behavior over the same set of static variables.

Well-defined creation and destruction: The variables of a monostate, being static, have well-defined creation and destruction times.

Costs

No conversion: A nonmonostate class cannot be converted into a monostate class through derivation.

Efficiency: Because it is a real object, a monostate may go through many creations and destructions. These operations are often costly.

Presence: The variables of a monostate take up space, even if the monostate is never used.

Platform local: You can’t make a monostate work across several CLR instances or across several platforms.

MONOSTATE in Action

Consider 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 machine

image

There 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

using NUnit.Framework;

[TestFixture]
public class TurnstileTest
{
  [SetUp]
  public void SetUp()
  {
    Turnstile t = new Turnstile();
    t.reset();
  }

  [Test]
  public void TestInit()
  {
    Turnstile t = new Turnstile();
    Assert.IsTrue(t.Locked());
    Assert.IsFalse(t.Alarm());
  }

  [Test]
  public void TestCoin()
  {
    Turnstile t = new Turnstile();
    t.Coin();
    Turnstile t1 = new Turnstile();
    Assert.IsFalse(t1.Locked());
    Assert.IsFalse(t1.Alarm());
    Assert.AreEqual(1, t1.Coins);
  }

  [Test]
  public void TestCoinAndPass()
  {
    Turnstile t = new Turnstile();
    t.Coin();
    t.Pass();

    Turnstile t1 = new Turnstile();
    Assert.IsTrue(t1.Locked());
    Assert.IsFalse(t1.Alarm());
    Assert.AreEqual(1, t1.Coins, "coins");
  }

  [Test]
  public void TestTwoCoins()
  {
    Turnstile t = new Turnstile();
    t.Coin();
    t.Coin();

    Turnstile t1 = new Turnstile();
    Assert.IsFalse(t1.Locked(), "unlocked");
    Assert.AreEqual(1, t1.Coins, "coins");

    Assert.AreEqual(1, t1.Refunds, "refunds");
    Assert.IsFalse(t1.Alarm());
  }

  [Test]
  public void TestPass()
  {
    Turnstile t = new Turnstile();
    t.Pass();
    Turnstile t1 = new Turnstile();
    Assert.IsTrue(t1.Alarm(), "alarm");
    Assert.IsTrue(t1.Locked(), "locked");
  }

  [Test]
  public void TestCancelAlarm()
  {
    Turnstile t = new Turnstile();
    t.Pass();
    t.Coin();
    Turnstile t1 = new Turnstile();
    Assert.IsFalse(t1.Alarm(), "alarm");
    Assert.IsFalse(t1.Locked(), "locked");
    Assert.AreEqual(1, t1.Coins, "coin");
    Assert.AreEqual(0, t1.Refunds, "refund");
  }

  [Test]
  public void TestTwoOperations()
  {
    Turnstile t = new Turnstile();
    t.Coin();
    t.Pass();
    t.Coin();
    Assert.IsFalse(t.Locked(), "unlocked");
    Assert.AreEqual(2, t.Coins, "coins");
    t.Pass();
    Assert.IsTrue(t.Locked(), "locked");
  }
}



Listing 24-8. Turnstile

public class Turnstile
{
  private static bool isLocked = true;
  private static bool isAlarming = false;
  private static int itsCoins = 0;
  private static int itsRefunds = 0;
  protected static readonly
    Turnstile LOCKED = new Locked();

  protected static readonly
    Turnstile UNLOCKED = new Unlocked();
  protected static Turnstile itsState = LOCKED;

  public void reset()
  {
    Lock(true);
    Alarm(false);
    itsCoins = 0;
    itsRefunds = 0;
    itsState = LOCKED;
  }

  public bool Locked()
  {
    return isLocked;
  }

  public bool Alarm()
  {
    return isAlarming;
  }

  public virtual void Coin()
  {
    itsState.Coin();
  }

  public virtual void Pass()
  {
    itsState.Pass();
  }

  protected void Lock(bool shouldLock)
  {
    isLocked = shouldLock;
  }

  protected void Alarm(bool shouldAlarm)
  {
    isAlarming = shouldAlarm;
  }

  public int Coins
  {
    get { return itsCoins; }
  }

  public int Refunds
  {
    get { return itsRefunds; }
  }

  public void Deposit()
  {
    itsCoins++;
  }

  public void Refund()
  {
    itsRefunds++;
  }
}

internal class Locked : Turnstile
{
  public override void Coin()
  {
    itsState = UNLOCKED;
    Lock(false);
    Alarm(false);
    Deposit();
  }

  public override void Pass()
  {
    Alarm(true);
  }
}

internal class Unlocked : Turnstile
{
  public override void Coin()
  {
    Refund();
  }

  public override void Pass()
  {
    Lock(true);
    itsState = LOCKED;
  }
}


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.

Conclusion

It is often necessary to enforce a single instantiation for a particular object. This chapter has shown two very different techniques. SINGLETON makes use of private constructors, a static variable, and a static function to control and limit instantiation. MONOSTATE simply makes all variables of the object static.

SINGLETON is best used when you have an existing class that you want to constrain through derivation and don’t mind that everyone will have to call the Instance() method to gain access. MONOSTATE is best used when you want the singular nature of the class to be transparent to the users or when you want to use polymorphic derivatives of the single object.

Bibliography

[Fowler03] Martin Fowler, Patterns of Enterprise Application Architecture, Addison-Wesley, 2003.

[GOF95] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995.

[PLOPD3] Robert C. Martin, Dirk Riehle, and Frank Buschmann, eds. Pattern Languages of Program Design 3, Addison-Wesley, 1998.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.130.31