Lesson 8 | Singleton Implementation |
Objective | Write a Class that uses the Singleton Pattern. |
Write Class that uses the Singleton Pattern
Write a Class that uses the Singleton Pattern in the C# programming language.
The Singleton pattern in C# can be implemented in several ways. However, one of the most straightforward and thread-safe ways to create a Singleton in C# is by using the .NET Lazy<T> type. Here's an example:
public sealed class Singleton
{
// Use Lazy<T> for thread safety
private static readonly Lazy<Singleton> lazyInstance = new Lazy<Singleton>(() => new Singleton());
// Private constructor to prevent instantiation
private Singleton() { }
// Public static method to get the instance
public static Singleton Instance { get { return lazyInstance.Value; } }
}
In this code, we use the Lazy<T> type to handle the thread-safety aspect of the Singleton implementation, ensuring that only one instance of Singleton will be created, even in a multithreaded environment.
The class is declared as sealed to prevent inheritance, which can introduce complications to the Singleton pattern. The constructor is private, ensuring that the Singleton class can only be instantiated from within the class itself.
The Instance property is the global access point for the Singleton instance. When Instance is called, the Lazy<Singleton> object ensures that the Singleton is created at most once.
Remember to be cautious when using the Singleton pattern. While it can be helpful, it can also introduce global state into your application, which may lead to issues related to maintainability, testability, and concurrency.
Singleton Pattern in Java and C++
The implementation is the most language-dependent part of the pattern.
- Singleton Java
- Singleton C++
The trick to the Singleton is threefold:
- A nonpublic constructor so that arbitrary instances of the class can not be created.
- A field that holds a single unique instance of the class.
- A method that returns a reference (Java) or a pointer (C++) to the field.
The Singleton pattern is an easy to understand design pattern. Sometimes, there may be a need to have one and only one instance of a given class during the lifetime of an application. This may be due to necessity or due to the fact that only a single instance of the class is sufficient.
For example, we may need a single database connection object in an application.
The Singleton pattern is useful in such cases because it ensures that there exists one and only one instance of a particular object ever. Furthermore, it suggests that client objects should be able to access the single instance in a consistent manner.
Why the Singleton Pattern is Unique
The Singleton design pattern is unique in that it is a strange combination: its description is simple,
yet its implementation issues are complicated. This is proven by the abundance of Singleton discussions and implementations in books and magazine articles. The Singleton's description in the GoF book is described simply as:
Ensure a class only has one instance, and provide a global point of access to it.
A singleton is an improved global variable. The improvement that Singleton brings is that you cannot create a secondary object of the singleton's type. Therefore, you should use Singleton when you model types that conceptually have a unique instance in the application, such as Keyboard, Display, PrintManager, and SystemClock. Being able to instantiate these types more than once is unnatural at best, and often dangerous.
Providing a global point of access has a subtle implication—from a client's standpoint, the Singleton object
owns itself. There is no special client step for creating the singleton. Consequently, the Singleton object is
responsible for creating and destroying itself. Managing a singleton's lifetime causes the most
implementation headaches.
This chapter discusses the most important issues associated with designing and implementing various
Singleton variants in C++:
- The features that set apart a singleton from a simple global object
- The basic C++ idioms supporting singletons
- Better enforcement of a singleton's uniqueness
- Destroying the singleton and detecting postdestruction access
- Implementing solutions for advanced lifetime management of the Singleton object
- Multithreading issues
We will develop techniques that address each issue. In the end, we will use these techniques for
implementing a generic SingletonHolder class template.
There is no "best" implementation of the Singleton design pattern. Various Singleton implementations, including nonportable ones, are most appropriate depending on the problem at hand. This chapter's approach is to develop a family of implementations on a generic skeleton, following a policy-based design. SingletonHolder also provides hooks for extensions and customizations.
By the end of this chapter, we will have developed a SingletonHolder class template that can generate many different types of singletons. SingletonHolder gives you fine-grained control over how the Singleton object is allocated, when it is destroyed, whether it is thread safe, and what happens if a client
attempts to use it after it's been destroyed. The SingletonHolder class template provides Singletonspecific
services and functionality over any user-defined type.
Static Data and Static Functions is not the same as a Singleton
At first glance, it seems that the need for Singleton can be easily averted by using
- static member functions
- and static member variables:
class Font { ... };
class PrinterPort { ... };
class PrintJob { ... };
class MyOnlyPrinter{
public:
static void AddPrintJob(PrintJob& newJob){
if (printQueue_.empty() && printingPort_.available()){
printingPort_.send(newJob.Data());
}
else{
printQueue_.push(newJob);
}
}
private:
// All data is static
static std::queue<PrintJob> printQueue_;
static PrinterPort printingPort_;
static Font defaultFont_;
};
PrintJob somePrintJob("MyDocument.txt");
MyOnlyPrinter::AddPrintJob(somePrintJob);
However, this solution has a number of disadvantages in some situations. The main problem is that static
functions cannot be virtual, which makes it difficult to change behavior without opening
MyOnlyPrinter's code.
A subtler problem of this approach is that it makes initialization and cleanup difficult. There is no central
point of initialization and cleanup for MyOnlyPrinter's data. Initialization and cleanup can be nontrivial
tasks. For instance, defaultFont_ can depend on the speed of printingPort_.
Singleton implementations therefore concentrate on creating and managing a unique object while not
allowing the creation of another one.
Singleton Pattern - Exercise