This chapter explores object creation using a few classic, simple, and yet powerful design patterns from the GoF. These patterns allow developers to encapsulate behaviors, centralize object creation, add flexibility to their design, or control object lifetime. Moreover, they will most likely be used in every software you build directly or indirectly in the future.
GoF
Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides are the authors of Design Patterns: Elements of Reusable Object-Oriented Software (1994), also known as the GoF (GoF). In that book, they introduce 23 design patterns, some of which we will look at in this book.
Why are they that important? Because they are the building blocks of robust object composition and they help to create flexibility and reliability. Moreover, in Chapter 7, Deep Dive into Dependency Injection, we will leverage Dependency Injection to make those patterns even more powerful!
But first things first. The following topics will be covered in this chapter:
The Strategy pattern is a behavioral design pattern that allows us to change object behaviors at runtime. We can also use this pattern to compose complex object trees and rely on it to follow the Open/Closed Principle (OCP) without much effort. As a follow up on that last point, the Strategy pattern plays a significant role in the composition over inheritance way of thinking. In this chapter, we focus on the behavioral part of the Strategy pattern. In the next chapter, we cover how to use the Strategy pattern to compose systems dynamically.
The Strategy pattern's goal is to extract an algorithm (strategy) away from the host class needing it (context). That allows the consumer to decide on the strategy (algorithm) to use at runtime.
For example, we could design a system that fetches data from two different types of databases. Then we could apply the same logic over that data and use the same user interface to display it. To achieve this, using the Strategy pattern, we could create two strategies, one named FetchDataFromSql and the other FetchDataFromCosmosDb. Then we could plug the strategy that we need at runtime in the context class. That way, when the consumer calls the context, the context does not need to know from where the data comes from, how it is fetched, or what strategy is in use; it only gets what it needs to work, delegating the fetching responsibility to an abstracted strategy.
Before any further explanation, let's take a look at the following class diagram:
The building blocks of the Strategy pattern go as follows:
In the following diagram, we explore what happens at runtime. The actor represents any code consuming the Context object.
When the consumer calls the Context.SomeOperation() method, it does not know which implementation is executed, which is an essential part of this pattern. Context should not be aware of the strategy being used either. It should execute it through the interface without any knowledge of the implementation past that point. That is the strength of the Strategy pattern: it abstracts the implementation away from both the Context and the consumer.
Note
We could even generalize that last sentence and extend it to the use of any interface. Using an interface removes the ties between the consumer and the implementation by relying on the abstraction instead.
Context: We want to sort a collection using different strategies. Initially, we want to support sorting the elements of a list in ascending or descending order.
To achieve this, we need to implement the following building blocks:
a) SortAscendingStrategy
b) SortDescendingStrategy
The consumer is a small program that allows the user to choose a strategy, sort the collection, and display the items. Let's start with the ISortStrategy interface:
public interface ISortStrategy
{
IOrderedEnumerable<string> Sort(IEnumerable<string> input);
}
That interface contains only one method that expects a collection of strings as input, and that returns an ordered collection of strings. Now let's inspect the two implementations:
public class SortAscendingStrategy : ISortStrategy
{
public IOrderedEnumerable<string> Sort(IEnumerable<string> input)
=> input.OrderBy(x => x);
}
public class SortDescendingStrategy : ISortStrategy
{
public IOrderedEnumerable<string> Sort(IEnumerable<string> input)
=> input.OrderByDescending(x => x);
}
Both implementations are super simple as well, using LINQ to sort the input and return the result directly. Both implementations use expression-bodied methods, which we talked about in Chapter 4, The MVC Pattern using Razor.
Tip
When using expression-bodied methods, please ensure that you do not make the method harder to read for your colleagues.
The next building block to inspect is the SortableCollection class. It is not a collection in itself (it does not implement IEnumerable or other collection interfaces), but it is composed of items and can sort them using an ISortStrategy, like this:
public sealed class SortableCollection
{
public ISortStrategy SortStrategy { get; set; }
public IEnumerable<string> Items { get; private set; }
public SortableCollection(IEnumerable<string> items)
{
Items = items;
}
public void Sort()
{
if (SortStrategy == null)
{
throw new NullReferenceException("Sort strategy not found.");
}
Items = SortStrategy.Sort(Items);
}
}
This class is the most complex one so far, so let's take a more in-depth look:
With that code, we can see the Strategy pattern in action. The SortStrategy property represents the current algorithm, respecting an ISortStrategy contract, which is updatable at runtime. The SortableCollection.Sort() method delegates the work to that ISortStrategy implementation (the concrete strategy). Therefore, changing the value of the SortStrategy property leads to a change of behavior of the Sort() method, making this pattern very powerful yet simple.
Let's experiment with this by looking at MyConsumerApp, a console application that uses the previous code:
public class Program
{
private static readonly SortableCollection _data = new SortableCollection(new[] { "Lorem", "ipsum", "dolor", "sit", "amet." });
The _data instance represents the context, our sortable collection of items. Next, an empty Main method:
public static void Main(string[] args) { /*...*/ }
To keep it focused on the pattern, I took away the console logic, which is irrelevant for now.
private static string SetSortAsc()
{
_data.SortStrategy = new SortAscendingStrategy();
return "The sort strategy is now Ascending!";
}
The preceding method sets the strategy to a new instance of SortAscendingStrategy.
private static string SetSortDesc()
{
_data.SortStrategy = new SortDescendingStrategy();
return "The sort strategy is now Descending!";
}
The preceding method sets the strategy to a new instance of SortDescendingStrategy.
private static string SortData()
{
try
{
_data.Sort();
return "Data sorted!";
}
catch (NullReferenceException ex)
{
return ex.Message;
}
}
The SortData method calls the Sort() method, which delegates the call to an optional ISortStrategy implementation.
private static string PrintCollection()
{
var sb = new StringBuilder();
foreach (var item in _data.Items)
{
sb.AppendLine(item);
}
return sb.ToString();
}
}
This last method displays the collection in the console to visually validate the correctness of the code.
When we run the program, the following menu appears:
When a user selects an option, the program calls the appropriate method, as described earlier.
When executing the program, if you display the items (1), they appear in their initial order. If you assign a strategy (3 or 4), sort the collection (2), then display the list again, the order will have changed and would now be different based on the selected algorithm.
Let's analyze the sequence of events when you select the following options:
Next, is a sequence diagram that represents this:
The preceding diagram shows the Program creating a strategy and assigning it to SortableCollection. Then, when the Program calls the Sort() method, the SortableCollection instance delegates the sorting computation to the underlying algorithm implemented by the SortAscendingStrategy class, a.k.a. the strategy.
From the pattern standpoint, the SortableCollection class, a.k.a. the context, is responsible for keeping a hold on the current strategy and to use it.
The Strategy design pattern is very effective at delegating responsibilities to other objects. It also allows having a rich interface (context) with behaviors that can change during the program's execution.
The strategy does not have to be exposed directly; it can also be private to the class, hiding its presence to the outside world (the consumers); we talk more about this in the next chapter. Meanwhile, the Strategy pattern is excellent at helping us follow the SOLID principles:
Before getting into the Abstract Factory pattern, we will look at a few C# features to help write cleaner code.
Let's get back to the Main method of the Strategy pattern code sample. There, I used a few newer C# features. I omitted the implementation there because it was not relevant to the pattern itself, but here is that missing code, to analyze it:
public static void Main(string[] args)
{
string input = default;
do
{
Console.Clear();
Console.WriteLine("Options:");
Console.WriteLine("1: Display the items");
Console.WriteLine("2: Sort the collection");
Console.WriteLine("3: Select the sort ascending strategy");
Console.WriteLine("4: Select the sort descending strategy");
Console.WriteLine("0: Exit");
Console.WriteLine("--------------------------------------");
Console.WriteLine("Please make a selection: ");
input = Console.ReadLine();
Console.Clear();
var output = input switch
{
"1" => PrintCollection(),
"2" => SortData(),
"3" => SetSortAsc(),
"4" => SetSortDesc(),
"0" => "Exiting",
_ => "Invalid input!"
};
Console.WriteLine(output);
Console.WriteLine("Press **enter** to continue.");
Console.ReadLine();
} while (input != "0");
}
This first C# feature to explore was introduced in C# 7.1 and is called default literal expressions. It allows us to reduce the amount of code required to use default value expressions.
Previously, we'd need to write this:
string input = default(string);
Or this:
var input = default(string);
Now, we can write this:
string input = default;
It can be very useful for optional parameters, like this:
public void SomeMethod(string input1, string input2 = default)
{
// …
}
In that code block, we can pass one or two arguments to the method. When we omit the input2 parameter, it is instantiated to default(string). The default value of a string is null.
The second C# feature to explore was introduced in C# 8 and is named switch expressions. Previously, we'd need to write this:
string output = default;
switch (input)
{
case "1":
output = PrintCollection();
break;
case "2":
output = SortData();
break;
case "3":
output = SetSortAsc();
break;
case "4":
output = SetSortDesc();
break;
case "0":
output = "Exiting";
break;
default:
output = "Invalid input!";
break;
}
Now, we can write this:
var output = input switch
{
"1" => PrintCollection(),
"2" => SortData(),
"3" => SetSortAsc(),
"4" => SetSortDesc(),
"0" => "Exiting",
_ => "Invalid input!"
};
That makes the code shorter and simpler. Once you get used to it, I find this new way even easier to read. You can think about a switch expression as if the switch returns a value.
The discards are the last C# feature that we'll explore here. It was introduced in C# 7. In this case, it became the default case of the switch (see the highlighted line):
var output = input switch
{
"1" => PrintCollection(),
"2" => SortData(),
"3" => SetSortAsc(),
"4" => SetSortDesc(),
"0" => "Exiting",
_ => "Invalid input!"
};
The discards (_) are also useable in other scenarios. It is a special variable that cannot be used, a placeholder, like a variable that does not exist. By using discards, you don't allocate memory for that variable, which help optimize your application.
It can also be useful when deconstructing a tuple where you only use some of its members. It is also very convenient when calling a method with an out parameter that you don't want to use, for example:
if (bool.TryParse("true", out _))
{
/* ... */
}
In that last code block, we only want to do something if the input is a Boolean, but we do not use the Boolean value itself, which is a great scenario for a discard variable.
I'll skip tuples for now as we discuss them in the next chapter.
The Abstract Factory design pattern is a creational design pattern from the GoF. We use creational patterns to create other objects, and factories are a very popular way of doing that.
The Abstract Factory pattern is used to abstract the creation of a family of objects. It usually implies the creation of multiple object types within that family. A family is a group of related or dependent objects (classes).
Let's think about creating vehicles. There are multiple types of vehicles, and for each type, there are multiple models. We can use the Abstract Factory pattern to make our life easier for this type of scenario.
Note
There is also the Factory Method pattern, which focuses on creating a single type of object instead of a family. We only cover Abstract Factory here, but we use other types of factories later in the book.
With Abstract Factory, the consumer asks for an abstract object and gets one. The factory is an abstraction, and the resulting objects are also abstractions, decoupling the object creation from the consumers. That also allows us to add or remove families of objects without impacting the consumers.
If we think about vehicles, we could have the ability to make low- and high-grade versions of each type of vehicle. Let's take a look at a class diagram representing this:
In the diagram, we have the following:
Based on that diagram, a consumer uses the IVehicleFactory interface and should not be aware of the concrete factory used underneath, abstracting away the vehicle creation process.
Context: We need to support the creation of multiple types of vehicles. We also need to be able to add new types as they become available without impacting the system. To begin with, we only support high-grade and low-grade vehicles. Moreover, the program only supports the creation of cars and bikes.
For the sake of our demo, the vehicles are just empty classes and interfaces:
public interface ICar { }
public interface IBike { }
public class LowGradeCar : ICar { }
public class LowGradeBike : IBike { }
public class HighGradeCar : ICar { }
public class HighGradeBike : IBike { }
Let's now look at the part that we want to study – the factories:
public interface IVehicleFactory
{
ICar CreateCar();
IBike CreateBike();
}
public class LowGradeVehicleFactory : IVehicleFactory
{
public IBike CreateBike() => new LowGradeBike();
public ICar CreateCar() => new LowGradeCar();
}
public class HighGradeVehicleFactory : IVehicleFactory
{
public IBike CreateBike() => new HighGradeBike();
public ICar CreateCar() => new HighGradeCar();
}
The factories are simple implementations that describe the pattern well:
The consumer is an xUnit test project. Unit tests are often your first consumers, especially if you are doing TDD.
The AbstractFactoryBaseTestData class encapsulates some of our test data classes' utilities and is not relevant to our pattern study. Nevertheless, it can be useful to have all of the code on hand, and it is a very small class; so let's start there:
public abstract class AbstractFactoryBaseTestData : IEnumerable<object[]>
{
private readonly TheoryData<IVehicleFactory, Type> _data = new TheoryData<IVehicleFactory, Type>();
protected void AddTestData<TConcreteFactory, TExpectedVehicle>()
where TConcreteFactory : IVehicleFactory, new()
{
_data.Add(new TConcreteFactory(), typeof(TExpectedVehicle));
}
public IEnumerator<object[]> GetEnumerator() => _data.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
That class is an IEnumerable<object[]> with a private collection of TheoryData<T1, T2>, and an AddTestData<TConcreteFactory, TExpectedVehicle>() method that is used by other classes, to feed our theories.
Let's take a look at the concrete test class and its theories:
public class AbstractFactoryTest
{
[Theory]
[ClassData(typeof(AbstractFactoryTestCars))]
public void Should_create_a_Car_of_the_specified_type(IVehicleFactory vehicleFactory, Type expectedCarType)
{
// Act
ICar result = vehicleFactory.CreateCar();
// Assert
Assert.IsType(expectedCarType, result);
}
[Theory]
[ClassData(typeof(AbstractFactoryTestBikes))]
public void Should_create_a_Bike_of_the_specified_type(IVehicleFactory vehicleFactory, Type expectedBikeType)
{
// Act
IBike result = vehicleFactory.CreateBike();
// Assert
Assert.IsType(expectedBikeType, result);
}
}
In the preceding code, we have two theories that each use the data contained in a class, defined by the [ClassData(...)] attribute (see highlighted code). That data is used by the test runner to populate the value of the test method's parameters. So the test runner executes a test once per set of data. In this case, each method runs twice (the test data is covered next).
The execution of each test method goes as follows:
Note
I used ICar and IBike to type the variables instead of var, to make the type of the result variable clearer. In another context, I would have used var instead.
Now to the Theory data:
public class AbstractFactoryTestCars : AbstractFactoryBaseTestData
{
public AbstractFactoryTestCars()
{
AddTestData<LowGradeVehicleFactory, LowGradeCar>();
AddTestData<HighGradeVehicleFactory, HighGradeCar>();
}
}
public class AbstractFactoryTestBikes : AbstractFactoryBaseTestData
{
public AbstractFactoryTestBikes()
{
AddTestData<LowGradeVehicleFactory, LowGradeBike>();
AddTestData<HighGradeVehicleFactory, HighGradeBike>();
}
}
With the implementation details abstracted, the code is straightforward. If we take a closer look at the AbstractFactoryTestCars class, it creates two sets of test data:
The same goes for the AbstractFactoryTestBikes data:
We now have four tests. Two bike tests (Vehicles.AbstractFactoryTest.Should_create_a_Bike_of_the_specified_type) executed with the following arguments:
(vehicleFactory: HighGradeVehicleFactory { }, expectedBikeType: typeof(Vehicles.Models.HighGradeBike))
(vehicleFactory: LowGradeVehicleFactory { }, expectedBikeType: typeof(Vehicles.Models.LowGradeBike))
And two car tests (Vehicles.AbstractFactoryTest.Should_create_a_Car_of_the_specified_type) executed with the following arguments:
(vehicleFactory: HighGradeVehicleFactory { }, expectedCarType: typeof(Vehicles.Models.HighGradeCar))
(vehicleFactory: LowGradeVehicleFactory { }, expectedCarType: typeof(Vehicles.Models.LowGradeCar))
If we review the tests' execution, both test methods are unaware of types. They use the abstract factory (IVehicleFactory) and test the result against the expected type.
In a real program, we would use the ICar or the IBike instances to execute some logic, compute statistics, or do anything relevant to that program. Maybe that could be a racing game or a rich person's garage management system, who knows!
The important part of this project is the abstraction of the object creation process. The consumer code was not aware of the implementations.
To prove our design's flexibility, based on the Abstract Factory pattern, let's add a new concrete factory named MiddleEndVehicleFactory. That factory should return a MiddleEndCar or a MiddleEndBike instance. Once again, the car and bike are just empty classes (of course, in your programs they will do something):
public class MiddleGradeCar : ICar { }
public class MiddleGradeBike : IBike { }
The new MiddleEndVehicleFactory looks pretty much the same as the other two:
public class MiddleEndVehicleFactory : IVehicleFactory
{
public IBike CreateBike() => new MiddleGradeBike();
public ICar CreateCar() => new MiddleGradeCar();
}
As for the test class, we don't need to update the test methods (the consumers); we only need to update the setup to add new test data (see the lines in bold):
public class AbstractFactoryTestCars : AbstractFactoryBaseTestData
{
public AbstractFactoryTestCars()
{
AddTestData<LowGradeVehicleFactory, LowGradeCar>();
AddTestData<HighGradeVehicleFactory, HighGradeCar>();
AddTestData<MiddleEndVehicleFactory, MiddleGradeCar>();
}
}
public class AbstractFactoryTestBikes : AbstractFactoryBaseTestData
{
public AbstractFactoryTestBikes()
{
AddTestData<LowGradeVehicleFactory, LowGradeBike>();
AddTestData<HighGradeVehicleFactory, HighGradeBike>();
AddTestData<MiddleEndVehicleFactory, MiddleGradeBike>();
}
}
If we run the tests, we now have six passing tests (two theories with three test cases each). So, without updating the consumer (the AbstractFactoryTest class), we were able to add a new family of vehicles, the middle-end cars and bikes; kudos to the Abstract Factory pattern for that wonderfulness!
The Abstract Factory is an excellent pattern to abstract away the creation of object families, isolating each family and its concrete implementation, leaving the consumers unaware (decoupled) of the family being created at runtime.
We talk more about factories in the next chapter; meanwhile, let's see how the Abstract Factory pattern can help us follow the SOLID principles:
The Singleton design pattern allows creating and reusing a single instance of a class. We could use a static class to achieve almost the same goal, but not everything is doable using static classes. For example, implementing an interface or passing the instance as an argument cannot be done with a static class; you cannot pass static classes around, you can only use them directly.
In my opinion, the Singleton pattern in C# is an anti-pattern. Unless I cannot rely on Dependency Injection, I don't see how this pattern could serve a purpose. That said, it is a classic, so let's start by studying it, then move to a better alternative in the next chapter.
Here are a few reasons why we are covering this pattern:
The Singleton pattern limits the number of instances of a class to one. Then, the idea is to reuse the same instance subsequently. A singleton encapsulates both the object logic itself and its creational logic. For example, the Singleton pattern could lower the cost of instantiating an object with a large memory footprint since it's instantiated only once.
Can you think of a SOLID principle that gets broken right there?
This design pattern is straightforward and is limited to a single class. Let's start with a class diagram:
The Singleton class is composed of the following:
Note
You can name the Create() method anything or even get rid of it, as we'll see in the next example. We could name it GetInstance(), or it could be a static property named Instance or bear any other relevant name.
Now, in code, it can be translated to the following:
public class MySingleton
{
private static MySingleton _instance;
private MySingleton() { }
public static MySingleton Create()
{
if(_instance == default(MySingleton))
{
_instance = new MySingleton();
}
return _instance;
}
}
We can see in the following unit test that MySingleton.Create() always returns the same instance:
public class MySingletonTest
{
[Fact]
public void Create_should_always_return_the_same_instance()
{
var first = MySingleton.Create();
var second = MySingleton.Create();
Assert.Same(first, second);
}
}
And voilà! We have a working Singleton pattern, which is extremely simple – probably the most simple design pattern that I can think of.
Here is what is happening under the hood:
If you want your singleton to be thread-safe, you may want to lock the instance creation, like this:
public class MySingletonWithLock
{
private readonly static object _myLock = new object();
private static MySingletonWithLock _instance;
private MySingletonWithLock() { }
public static MySingletonWithLock Create()
{
lock (_myLock)
{
if (_instance == default(MySingletonWithLock))
{
_instance = new MySingletonWithLock();
}
}
return _instance;
}
}
Previously, we used the "long way" of implementing the Singleton pattern and had to implement a thread-safe mechanism. Now that classic is behind us. We can shorten that to get rid of the Create() method, like this:
public class MySimpleSingleton
{
public static MySimpleSingleton Instance { get; } = new MySimpleSingleton();
private MySimpleSingleton() { }
}
This way, you can use the singleton instance directly through its Instance property, like this:
MySimpleSingleton.Instance.SomeOperation();
We can prove the correctness of that claim by executing the following test method:
[Fact]
public void Create_should_always_return_the_same_instance()
{
var first = MySimpleSingleton.Instance;
var second = MySimpleSingleton.Instance;
Assert.Same(first, second);
}
By doing this, our singleton becomes thread-safe as the property initializer creates the singleton instance instead of nesting it inside an if statement. It is usually best to delegate responsibilities to the language or the framework whenever possible.
Beware of the Arrow operator
It may be tempting to use the arrow operator => to initialize the Instance property like this: public static MySimpleSingleton Instance => new MySimpleSingleton();, but doing so would return a new instance every time. This would defeat the purpose of what we want to achieve. On the other hand, the property initializer is run only once.
The use of a static constructor would also be a valid, thread-safe alternative, once again delegating the job to the language.
That last implementation of the Singleton pattern led us to the Ambient Context pattern. We could even call the Ambient Context an anti-pattern, but let's just state that it is a consequential code smell.
I don't like ambient contexts for multiple reasons. First, I do my best to stay away from anything global. Globals can be very convenient at first because they are easy to use. They are always there and accessible whenever needed: easy. However, they can bring many drawbacks in terms of flexibility and testability.
When using an ambient context, the following occurs:
Fun fact
Many years ago, before the JavaScript frameworks era, I ended up fixing a bug in a system where some function was overriding the value of undefined due to a subtle error. This is an excellent example of how global variables could impact your whole system and make it more brittle. The same is true for the Ambient Context and Singleton patterns in C#; globals can be dangerous and annoying.
Rest assured that, nowadays, browsers won't let developers update the value of undefined, but back then, it was possible.
Now that we've talked about globals, an ambient context is a global instance, usually available through a static property. The Ambient Context pattern is not purely evil, but it is a code smell that smells bad. There are a few examples in .NET Framework, such as System.Threading.Thread.CurrentPrincipal and System.Threading.Thread.CurrentThread. In this last case, CurrentThread is scoped instead of being purely global like CurrentPrincipal. An ambient context does not have to be a singleton, but that is what they are most of the time. Creating a scoped ambient context is harder and is out of the scope of this book.
Is the Ambient Context pattern good or bad? I'd go with both! It is useful primarily because of its convenience and ease of use while it is usually global. Most of the time, it could and should be designed differently to reduce the drawbacks that globals bring.
There are many ways of implementing an ambient context; it can be more complicated than a simple singleton, and it can aim at another, more dynamic scope than a single global instance. However, to keep it brief and straightforward, we are focusing only on the singleton version of the ambient context, like this:
public class MyAmbientContext
{
public static MyAmbientContext Current { get; } = new MyAmbientContext();
private MyAmbientContext() { }
public void WriteSomething(string something)
{
Console.WriteLine($"This is your something: {something}");
}
}
That code is an exact copy of the MySimpleSingleton class, with a few subtle changes:
If we take a look at the test method that follows, we can see that we use the ambient context by calling MyAmbientContext.Current, just like we did with the last singleton implementation:
[Fact]
public void Should_echo_the_inputted_text_to_the_console()
{
// Arrange (make the console write to a StringBuilder
// instead of the actual console)
var expectedText = "This is your something: Hello World!" + Environment.NewLine;
var sb = new StringBuilder();
using (var writer = new StringWriter(sb))
{
Console.SetOut(writer);
// Act
MyAmbientContext.Current.WriteSomething("Hello World!");
}
// Assert
var actualText = sb.ToString();
Assert.Equal(expectedText, actualText);
}
The property could include a public setter (public static MyAmbientContext Current { get; set; }), and it could support more complex mechanics. As always, it is up to you and your specifications to build the right classes exposing the right behaviors.
To conclude this interlude: try to avoid ambient contexts and use instantiable classes instead. We'll see how to replace a singleton with a single instance of a class using Dependency Injection in the next chapter. That gives us a more flexible alternative to the Singleton pattern.
The Singleton pattern allows the creation of a single instance of a class for the whole lifetime of the program. It leverages a private static field and a private constructor to achieves its goal, exposing the instantiation through a public static method or property. We can use a field initializer, the Create method itself, a static constructor, or any other valid C# options to encapsulate the initialization logic.
Now let's see how the Singleton pattern can help us (not) follow the SOLID principles:
a) It has the responsibility for which it has been created (not illustrated here), like any other class.
b) It has the responsibility of creating and managing itself (lifetime management).
As you can see, the Singleton pattern does violate all the SOLID principles but the LSP and should be used with caution. Having only a single instance of a class and always using that same instance is a legitimate concept. However, we'll see how to properly do this in the next chapter, leading me to the following advice: do not use the Singleton pattern, and if you see it used somewhere, try refactoring it out. Another good idea is to avoid the use of static members as much as possible as they create global elements that can make your system less flexible and more brittle. There are occasions where static members are worth using, but try keeping their number as low as possible. Ask yourself if that static member or class could be replaced with something else before coding one.
Some may argue that the Singleton design pattern is a legitimate way of doing things. However, in ASP.NET Core 5, I cannot agree with them: we have a powerful mechanism to do it differently, called Dependency Injection. When using other technologies, maybe, but not with .NET.
In this chapter, we explored our first GoF design patterns. These patterns expose some of the essential basis of software engineering, not necessarily the patterns themselves, but the concepts behind them:
We also peeked at the Ambient Context code smell, which is used to create an omnipresent entity accessible from everywhere. It is often implemented as a singleton and is a global object usually defined using the static modifier.
In the next chapter, we'll finally jump into Dependency Injection to see how it helps us compose complex yet maintainable systems. We'll also revisit the Strategy, the Factory, and the Singleton patterns to see how to use them in a Dependency Injection oriented context and how powerful they really are.
Let's take a look at a few practice questions:
3.137.218.215