One singleton can be an extremely valuable tool for enhancing performance. This is particularly true when there is always just one physical object. In traditional programming, you would instantiate a class as an object every time you need a specific instance of an object. For instance, you would create your own product "12" object if you needed access to the information about product "12."
However, in many cases, such as the case in a product catalog, there is one, single persistence of the object in the database, and therefore there only needs to be one instance of the object in memory at one time. In this article, we'll explore how to convert your objects into single instance objects to improve performance and reduce resource usage within the .NET development framework.
A downloadable version of this article in PDF form, including embedded code, is available in the TechRepublic Download Center.
The problem with construction
The idea of a constructor for a class has been around since the dawn of object-oriented programming. The object must have a chance to initialize itself before it's handed back to the calling program. This is great since it allows internal data structures to be populated and configured.
The challenge is that the compiler or the runtime environment has already allocated memory for the object. It has, in essence, already created the new object and is just waiting to be configured.
This prevents the constructor from being used with a singleton. The object has already been partially instantiated by the time the code is called and because of this, we have to stop the developer from using the constructor. The constructor must be made protected, private, or internal. Once this is done, you'll be able to call the constructor from within your class, but the outside world won't be able to call it directly.
By creating even one constructor, even if marked protected, private, or internal, the default constructor isn't created. This ensures that no code, except the scope you've specified, can create the object.
The role of static methods
If the developer can't use the constructor, she can't create her own object, which will make using the object very difficult. It's hard to use something that you can't create, which is where static methods come in. Since static methods operate directly off of the class, an instance isn't required. This allows the developer to call the methods before he has an instance of the class.
The role of the static methods becomes the process of looking up some internal cache to determine if the requested instance of the object exists. If the instance exists, the calling program is provided a reference to that instance. If the instance doesn't exist, the static method automatically creates it, stores it in the internal cache, and returns a reference to the object.
We now know how to prevent a constructor from being created and how to allow developers to create instances through static methods. However, the missing piece is the cache which holds the instances of the class that you want to save and offer up to future calls. That's the role of the collection. The key to the whole program is being able to look up and find previous instances of the class that you've instantiated. Collections can store objects and index them for the lookup the static method will need to do.
A collection is a necessary component of being able to cache the objects, and therefore is a necessary part of creating a singleton class.
Putting it all together
The prerequisite pieces are in place for creating your own singleton class. Listing A shows a Method class, and Listing B shows the MethodCollection class. These two classes cause a single instance of any given data to be created in the Method class. This code, which was extracted from a larger IIS Log Parsing application, has been designed to take the relatively few request methods that are encountered in a log file and prevent literally millions of instances from being created. The Pseudo code comments in the listings are in place to remove unnecessary details, such as database access.
The collection design has two basic retrieval methods. These methods, both of which are GetMethod() overrides, fetch a method by either its integer ID or the string representation. At this level, a null is returned if the method isn't found. On that are layered two GetOrCreateMethod() overrides. These call GetMethod() but create a method if one is not found. This allows the calling code to not worry whether the method exists in the database or not—if not, it is automatically created.
Each of these functions has a corresponding static counterpart. These static methods work on a single global list of methods. This works by a static method which returns or creates a global instance of the Method collection. The static prefixed methods use the global instance and call the methods described above.
The net effect is that the collection can be used directly. This is in case there is some need to have a subset of the methods available, but it also has a global instance, so it isn't necessary for the developer to maintain his own list if he doesn't want to.
On the Method class side, it also supports a set of GetMethod static functions. These are the ones that the MethodCollection calls. These either return the method, or they will return null. An oddity you may see in the Method class is that there's an internal constructor which takes in a data row. This is designed as a performance enhancer. It allows the object to be instantiated with a pre-populated row of data. This isn't a big enhancement with such a small class, but in larger classes with a potentially large number of instances, it allows the creation without another database query and does improve performance.
The challenge of multithreading
For the scope of this singleton class, multithreading would be a problem because it would be possible to create multiple instances of the method name as objects—even in the database. In many cases creating a singleton doesn't require much thought towards multithreading; however, when it does, you'll need to add your own critical section handling to prevent two simultaneous attempts from running the same addition code, which will prevent the addition of duplicate records.