In this blog post, we will give a brief overview of the singleton pattern, which is the most contentious pattern but still has some applications that can help us optimize our code.
Why Singleton?
We can use this pattern when some code components in real-world scenarios are intended to be declared only once. For instance, since factories do not have states, as we discussed, we can declare them as singletons and share a single instance of that factory across the entire code base, which will handle object creation everywhere. You can also use this when you want to limit the number of constructor calls.
This pattern can also be used when you want to create a single object that supports lazy initialization and thread safety and prevents users from creating multiple copies of the same object.
Let’s understand with an example.
namespace DesignPatterns.Singleton
{
public interface IWeather
{
public (double min, double max) GetWeather(string city);
}
public class Weather : IWeather
{
private double _min, _max;
public Weather()
{
//we are using thread.sleep to mimic
//the behavior but you can call actual
//weather API here to get minimum & maximum temperature
Thread.Sleep(200);
_min = 29.59;
_max = 31.60;
}
public (double min, double max) GetWeather(string city)
{
return (_min, _max);
}
}
}
As you can see from the code snippet above, this class is in charge of calling the weather API to obtain the minimum and maximum weather temperatures. We then use the "GetWeather" function, which returns a tuple, to return those values. However, as you can see, we are calling an API in the constructor of a Weather class, which means that each time we create a new object, it will also call the constructor and an API. This can lead to a number of issues because, typically, weather APIs are rate-limited and only certain requests are permitted to be made within a given window of time. However, if we use them frequently, there is a chance that we will go over the limit and incur additional fees as a result of the extra API calls.
You might be wondering how we can address this issue. Making that class a singleton allows us to share a single instance across all requests and eliminates the need for additional calls, which is the straightforward solution.
namespace DesignPatterns.Singleton
{
public interface IWeather
{
public (double min, double max) GetWeather(string city);
}
public class WeatherSingleton : IWeather
{
private double _min, _max;
private WeatherSingleton()
{
//we are using thread.sleep to mimic
//the behavior but you can call actual
//weather API here to get minimum & maximum temperature
Thread.Sleep(200);
_min = 29.59;
_max = 31.60;
}
public (double min, double max) GetWeather(string city)
{
return (_min, _max);
}
private static Lazy<WeatherSingleton> _instance = new Lazy<WeatherSingleton>(() => new WeatherSingleton());
public static WeatherSingleton Instance => _instance.Value;
}
}
As you can see, we have now made our constructor private, exposed the static field so that anyone can obtain a single instance, and initialized the constructor in a lazy way so that it will only be called if someone accesses the static "Instance" field.
Why does everyone hate “Singleton”?
Since we are aware that singletons are closely coupled with the underlying data, this makes them less testable, which is the single biggest problem with singletons. Let's use an example to better understand this. Suppose you have a repository pointing to a particular database, and you are writing tests against it. In some cases, you might need to hardcode data in some of the tests, but what if the data in the database were to change? If so, you will also need to update your tests, which is a time-consuming task in real life. In TDD-based systems, tests serve as the cornerstone for writing code and should not be altered. This is the singleton pattern's well-known limitation. In other words, once we start hardcoding the reference to the same instance of the specific component everywhere, we can't really change it to something else.
The issue is that you need to introduce DI & mocks even for the simplest solutions, making this issue completely irrelevant. However, this is not a problem that we cannot solve; we can do so by introducing the DI libraries and then replacing the actual DB with some mock.
Monostate Singleton
By focusing on the name, we can deduce that this pattern shares some characteristics with the singleton design pattern in that "a class sharing the same state amongst all instances is called a monostate class."
namespace DesignPatterns.MonostateSingleton
{
public class SelectedColor
{
private static string _color;
public string Color
{
get => _color;
set => _color = value;
}
public override string ToString()
{
return $"You have selected {_color} color";
}
}
public class TestMonostate
{
public static void Main(string[] args)
{
SelectedColor clr = new SelectedColor();
clr.Color = "Orange";
SelectedColor clr2 = new SelectedColor();
Console.WriteLine(clr2);
}
}
}
As you can see, we're using static fields in the class and updating them with regular properties, which update the static values that are shared by all instances. As a result, we are sharing a class's state with all of its instances.
Per-Thread Singleton
When we need a single object per thread that is shared across all of the operations carried out by that thread, such as when we need a singleton per thread, we can use the Pre-Thread singleton.
namespace DesignPatterns.PerThreadSingleton
{
public class RandomValue
{
public int Value;
private RandomValue()
{
Console.WriteLine($"Creating new instance for theread : {Thread.CurrentThread.ManagedThreadId}");
Value = new Random().Next(int.MaxValue);
}
private static ThreadLocal<RandomValue> _instance = new ThreadLocal<RandomValue>(() => new RandomValue());
public static RandomValue Instance => _instance.Value;
}
public class TestClass
{
public static void Main(string[] args)
{
var thread1 = new Thread(() =>
{
Console.WriteLine("Thread1 random value : " + RandomValue.Instance.Value);
});
thread1.Start();
var thread2 = new Thread(() =>
{
Console.WriteLine("Thread2 1st random value : " + RandomValue.Instance.Value);
Console.WriteLine("Thread2 2nd random value : " + RandomValue.Instance.Value);
});
thread2.Start();
}
}
}
Therefore, as you can see, we are creating a singleton instance per thread in this case, which is why the constructor's print statement was called twice. Additionally, to demonstrate that we only create one instance per thread, take a look at the output. Both instances inside of thread 2 are returning the same values, proving that even if we create two instances in “thread2,” only one object is created, and since we execute both operations on a single thread, this object is shared amongst all operations.
This concludes our blog on singleton design patterns; in subsequent blogs, we will examine the following patterns under the creational category.
Happy Coding…!!!
Top comments (0)