Photo by Chetan Menaria on Unsplash
Singleton has a bad rep for being an anti-pattern.
You might have been burned bad with it.
I learned from Michael Outlaw from Coding Blocks podcast episode 16 that Singleton pattern can come in handy in conjunction with Null object pattern.
Let’s dive in.
🤔 Why implement a Null object as a singleton?
42 minutes into the Coding Blocks Episode 16, Michael Outlaw explains two reasons.
- “If you have two versions of null objects, they should be identical, why waste memory to have same nothingness repeated”.
- For equality checks – Each null object can be compared by reference.
Depending on a situation you might not even need to do a equality check as a null object usually does nothing (as you can see in the example below.)
TL;DR – You can stop reading here.
✍️ An Example Implementation
Here is an example of how you can use a Singleton pattern with a Null object pattern.
Suppose that there is a simple factory (PaymentStrategyFactory) that returns a strategy object instance for processing a payment given a provider name.
public static class PaymentStrategyFactory | |
{ | |
public static IPaymentStrategy Create(string paymentProvider) | |
{ | |
switch (paymentProvider) | |
{ | |
case "Paypal": return new PaypalPaymentStrategy(); | |
case "ApplePay": return new ApplePayPaymentStrategy(); | |
default: return NullPaymentStrategy.Instance; | |
} | |
} | |
} |
PayPal & ApplePay strategies return “Successful” or “Failed” process status at random.
public abstract class RandomPaymentStrategy : IPaymentStrategy | |
{ | |
public ProcessStatus Process(double amount) | |
{ | |
var random = new Random(); | |
var randomValue = random.Next(0, 2); | |
return (ProcessStatus)Math.Min(randomValue, 2); | |
} | |
} | |
public class PaypalPaymentStrategy : RandomPaymentStrategy { } | |
public class ApplePayPaymentStrategy : RandomPaymentStrategy { } |
// "NoOp" means No Operation: https://en.wikipedia.org/wiki/NOP | |
// First value is assigned a value of 0 by default. | |
public enum ProcessStatus { Successful, Failed, NoOp } |
If a provider name is not supported, the PaymentStrategyFactory
simply returns an object of type NullPaymentStrategy
, which implements IPaymentStrategy
.
Singleton instance is achieved with a private constructor and a static Instance
property.
public class NullPaymentStrategy : IPaymentStrategy | |
{ | |
public static readonly IPaymentStrategy Instance = new NullPaymentStrategy(); | |
private NullPaymentStrategy() { } | |
public ProcessStatus Process(double amount) | |
{ | |
return ProcessStatus.NoOp; | |
} | |
} |
public interface IPaymentStrategy | |
{ | |
ProcessStatus Process(double amount); | |
} |
Let’s put them together and process payments.
private static void ProcessPayments() | |
{ | |
var paymentProviders = new[] { | |
"Paypal", "ApplePay", "GooglePay", | |
"Braintree", "PayPal", "ApplePay", | |
"ApplePay", "BadPaymentProvider" }; | |
foreach (var paymentProvider in paymentProviders) | |
{ | |
var paymentProcessor = PaymentStrategyFactory.Create(paymentProvider); | |
WriteLine($"===== Processing with '{paymentProvider}' Provider ====="); | |
// There is no need to check if the payment processor is null here | |
// Reduced Cyclomatic Complexity. | |
var paymentStatus = paymentProcessor.Process(111); | |
WriteLine($"--> {paymentStatus}"); | |
} | |
} |
Results of ProcessPayments()
.
===== Processing with 'Paypal' Provider ===== | |
--> Successful | |
===== Processing with 'ApplePay' Provider ===== | |
--> Failed | |
===== Processing with 'GooglePay' Provider ===== | |
--> Successful | |
===== Processing with 'Braintree' Provider ===== | |
--> NoOp | |
===== Processing with 'PayPal' Provider ===== | |
--> Successful | |
===== Processing with 'ApplePay' Provider ===== | |
--> Successful | |
===== Processing with 'ApplePay' Provider ===== | |
--> Failed | |
===== Processing with 'BadPaymentProvider' Provider ===== | |
--> NoOp |
You can see that Braintree
& BadPaymentProvider
returns NoOp
because there PaymentStrategyFactory
returns a singleton instance NullPaymentStrategy.Instance
as there is no matching payment strategy.
👋 Conclusion
Googling Singleton Anti Pattern results in many reasons why Singleton pattern is bad.
But when used judiciously, it can improve your code quality/memory/speed.
Source code is available on GitHub.
Top comments (4)
Thanks for your cool post.
100% agree that there are not a lot of scenarios where a singleton is a good choice. But the "Null Object Pattern" is a good example for a useful use-case.
Keep up the great work.
You're welcome & thank you, Lars, for the comment.
I've heard lots of how Singleton is an anti-pattern for any cases. Just thought this was a refreshing concept to learn so shared :)
My boy singleton!!!
Great point as always, as much fun as I have picking on singletons they have their place and are (still) popular despite the downsides!
Every pattern (or combination thereof) has a place in our heart ❤️/Code :)