Archives 2023

What is dependency injection? – Dependency Injection

Before you begin: Join our book community on Discord

Give your feedback straight to the author himself and chat to other early readers on our Discord server (find the “architecting-aspnet-core-apps-3e” channel under EARLY ACCESS SUBSCRIPTION).

https://packt.link/EarlyAccess

This chapter explores the ASP.NET Core Dependency Injection (DI) system, how to leverage it efficiently, and its limits and capabilities.We learn to compose objects using DI and delve into the Inversion of Control (IoC) principle. As we traverse the landscape of the built-in DI container, we explore its features and potential uses.Beyond practical examples, we lay down the conceptual foundation of Dependency Injection to understand its purpose, its benefits, and the problems it solves and to lay down the ground for the rest of the book as we rely heavily on DI.We then return to the first three Gang of Four (GoF) design patterns we encountered, but this time, through the lens of Dependency Injection. By refactoring these patterns using DI, we gain a more holistic understanding of how this powerful design tool influences the structure and flexibility of our software.Dependency Injection is a cornerstone in your path toward mastering modern application design and its transformative role in developing efficient, adaptable, testable, and maintainable software.In this chapter, we cover the following topics:

  • What is dependency injection?
  • Revisiting the Strategy pattern
  • Understanding guard clauses
  • Revisiting the Singleton pattern
  • Understanding the Service Locator pattern
  • Revisiting the Factory pattern

What is dependency injection?

DI is a way to apply the Inversion of Control (IoC) principle. IoC is a broader version of the dependency inversion principle (the D in SOLID).The idea behind DI is to move the creation of dependencies from the objects themselves to the composition root. That way, we can delegate the management of dependencies to an IoC container, which does the heavy lifting.

An IoC container and a DI container are the same thing—they’re just different words people use. I use both interchangeably in real life, but I stick to IoC container in the book because it seems more accurate than DI container.

IoC is the concept (the principle), while DI is a way of inverting the flow of control (applying IoC). For example, you apply the IoC principle (inverting the flow) by injecting dependencies at runtime (doing DI) using a container. Feel free to use any or both.

Next, we define the composition root.

The composition root

A critical concept behind DI is the composition root. The composition root is where we tell the container about our dependencies and their expected lifetime: where we compose our dependency trees. The composition root should be as close to the program’s starting point as possible, so from ASP.NET Core 6 onward, the composition root is in the Program.cs file. In the previous versions, it was in the Program or Startup classes.Next, we explore how to leverage DI to create highly adaptable systems.

Conclusion – Strategy, Abstract Factory, and Singleton Design Patterns

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 achieve 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:

  • S: The singleton violates this principle because it has two clear responsibilities:
    • It has the responsibility for which it has been created (not illustrated here), like any other class.
    • It has the responsibility of creating and managing itself (lifetime management).
  • O: The Singleton pattern also violates this principle. It enforces a single static instance, locked in place by itself, which limits extensibility. It is impossible to extend the class without changing its code.
  • L: There is no inheritance directly involved, which is the only good point.
  • I: No C# interface is involved, which violates this principle. However, we can look at the class interface instead, so building a small targeted singleton instance would satisfy this principle.
  • D: The singleton class has a rock-solid hold on itself. It also suggests using its static property (or method) directly without using an abstraction, breaking the DIP with a sledgehammer.

As you can see, the Singleton pattern violates all the SOLID principles but the LSP and should be used cautiously. Having only a single instance of a class and always using that same instance is a common concept. However, we explore more proper ways to 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.

I suggest avoiding static members that create global states as a general good practice. They 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 you can replace that static member or class 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, I am afraid I have to disagree: we have a powerful mechanism to do it differently, called dependency injection. When using other technologies, maybe, but not with modern .NET.

Summary

In this chapter, we explored our first GoF design patterns. These patterns expose some of the essential basics of software engineering, not necessarily the patterns themselves, but the concepts behind them:

  • The Strategy pattern is a behavioral pattern that we use to compose most of our future classes. It allows swapping behavior at runtime by composing an object with small pieces and coding against interfaces, following the SOLID principles.
  • The Abstract Factory pattern brings the idea of abstracting away object creation, leading to a better separation of concerns. More specifically, it aims to abstract the creation of object families and follow the SOLID principles.
  • Even if we defined it as an anti-pattern, the Singleton pattern brings the application-level objects to the table. It allows the creation of a single instance of an object that lives for the whole lifetime of a program. The pattern violates most SOLID principles.

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 brings a global state object to the program.The next chapter explores how dependency injection helps us compose complex yet maintainable systems. We 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.

Fun fact – Strategy, Abstract Factory, and Singleton Design Patterns

Many years ago, before the JavaScript frameworks era, I fixed 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 applies to 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 it was possible back then.

Now that we’ve discussed global objects, an ambient context is a global instance, usually available through a static property. The Ambient Context pattern can bring good things, 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, that are scoped to a thread instead of being purely global like most static members. An ambient context does not have to be a singleton, but that is what they are most of the time. Creating a non-global (scoped) ambient context is harder and requires more work.

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. Most of the time, it could and should be designed differently to reduce its drawbacks.There are many ways of implementing an ambient context, but to keep it brief and straightforward, we are focusing only on the singleton version of the ambient context. The following code is a good example:

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:

  • Instance is named Current.
  • The WriteSomething method is new but has nothing to do with the Ambient Context pattern itself; it is just to make the class do something.

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 or support more complex logic. Building the right classes and exposing the right behaviors is up to you and your specifications.To conclude this interlude, avoid ambient contexts and use instantiable classes instead. We 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. We can also create a single instance per HTTP request, which saves us the trouble of coding it while eliminating the disadvantages.

An alternate (better) way – Strategy, Abstract Factory, and Singleton Design Patterns

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 code and even get rid of the Create() method like this:

public class MySimpleSingleton
{
    public static MySimpleSingleton Instance { get; } = new MySimpleSingleton();
    private MySimpleSingleton() { }
}

The preceding code relies on the static initializer to ensure that only one instance of the MySimpleSingleton class is created and assigned to the Instance property.

This simple technique should do the trick unless the singleton’s constructor executes some heavy processing.

With the property instead of a method, we can use the singleton class 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);
}

It is usually best to delegate responsibilities to the language or the framework whenever possible like we did here with the property initializer. Using a static constructor would also be a valid, thread-safe alternative, once again delegating the job to language features.

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 runs only once.

The arrow operator makes the Instance property an expression-bodied member, equivalent to creating the following getter: get { return new MySimpleSingleton(); }. You can consult Appendix A for more information about expression-bodies statements.

Before we conclude the chapter, the Singleton (anti-)pattern also leads to a code smell.

Code smell – Ambient Context

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 do not recommend using ambient contexts for multiple reasons. First, I do my best to avoid anything global; an ambient context is a global state. Globals, like static members in C#, can look very convenient because they are easy to access and use. They are always there and accessible whenever needed: easy. However, they bring many drawbacks in terms of flexibility and testability.When using an ambient context, the following occurs:

  • Tight coupling: global states lead to less flexible systems; consumers are tightly coupled with the ambient context.
  • Testing difficulty: global objects are harder to replace, and we cannot easily swap them for other objects, like a mock.
  • Unforseen impacts: if some part of your system messes up your global state, that may have unexpected consequences on other parts of your system, and you may have difficulty finding out the root cause of those errors.
  • Potential misuse: developers could be tempted to add non-global concerns to the ambient context, leading to a bloated component.

The Singleton design pattern – Strategy, Abstract Factory, and Singleton Design Patterns

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, a static class can’t implement an interface. We can’t pass an instance of a static class as an argument because there is no instance. We can only use static classes directly, which leads to tight coupling every time.The Singleton pattern in C# is an anti-pattern, and we should rarely use it, if ever, and use dependency injection instead. That said, it is a classic design pattern worth learning to at least avoid implementing it. We explore a better alternative in the next chapter.Here are a few reasons why we are covering this pattern:

  • It translates into a singleton scope in the next chapter.
  • Without knowing about it, you cannot locate it, try to remove it, or avoid its usage.
  • It is a simple pattern to explore.
  • It leads to other patterns, such as the Ambient Context pattern.

Goal

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 the program instantiates it only once.Can you think of a SOLID principle that gets broken right there?The Singleton pattern promotes that one object must have two responsibilities, breaking the Single Responsibility Principle (SRP). A singleton is the object itself and its own factory.

Design

This design pattern is straightforward and is limited to a single class. Let’s start with a class diagram:

 Figure 7.6: Singleton pattern class diagramFigure 7.6: Singleton pattern class diagram 

The Singleton class is composed of the following:

  • A private static field that holds its unique instance.
  • A public static Create() method that creates or returns the unique instance.
  • A private constructor, so external code cannot instantiate it without passing by the Create method.

You can name the Create() method anything or even get rid of it, as we 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.

We can translate the preceding diagram to the following code:

public class MySingleton
{
    private static MySingleton?
_instance;
    private MySingleton() { }
    public static MySingleton Create()
    {
        _instance ??= new MySingleton();
        return _instance;
    }
}

The null-coalescing assignment operator ??= assigns the new instance of MySingleton only if the _instance member is null. That line is equivalent to writing the following if statement:

if (_instance == null)
{
    _instance = new MySingleton();
}

Before discussing the code more, let’s explore our new class’s behavior. We can see in the following unit test that MySingleton.Create() always returns the same instance as expected:

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:

  1. The first time that a consumer calls MySingleton.Create(), it creates the first instance of MySingleton. Since the constructor is private, it can only be created from the inside.
  2. The Create method then persists that first instance to the _instance field for future use.
  3. When a consumer calls MySingleton.Create() a second time, it returns the _instance field, reusing the class’s previous (and only) instance.

Now that we understand the logic, there is a potential issue with that design: it is not thread-safe. If we want our singleton to be thread-safe, we can lock the instance creation like this:

public class MySingletonWithLock
{
    private static readonly object _myLock = new();
    private static MySingletonWithLock?
_instance;
    private MySingletonWithLock() { }
    public static MySingletonWithLock Create()
    {
        lock (_myLock)
        {
            _instance ??= new MySingletonWithLock();
        }
        return _instance;
    }
}

In the preceding code, we ensure two threads are not attempting to access the Create method simultaneously to ensure they are not getting different instances. Next, we improve our thread-safe example by making it shorter.

Project – The mid-range vehicle factory – Strategy, Abstract Factory, and Singleton Design Patterns

To prove the flexibility of our design based on the Abstract Factory pattern, let’s add a new concrete factory named MidRangeVehicleFactory. That factory should return a MidRangeCar or a MidRangeBike 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 MidRangeVehicleFactory looks pretty much the same as the other two:

public class MidRangeVehicleFactory : IVehicleFactory
{
    public IBike CreateBike() => new MiddleGradeBike();
    public ICar CreateCar() => new MiddleGradeCar();
}

Now, to test the mid-range factory, we declare the following test class:

namespace Vehicles.MidRange;
public class MidRangeVehicleFactoryTest : BaseAbstractFactoryTest<MidRangeVehicleFactory, MidRangeCar, MidRangeBike>
{
}

Like the low-end and high-end factories, the mid-range test class depends on the BaseAbstractFactoryTest class and specifies the types to test for (highlighted).If we run the tests, we now have the following six passing tests:

 Figure 7.5: Visual Studio Test Explorer showcasing the six passing tests.Figure 7.5: Visual Studio Test Explorer showcasing the six passing tests. 

So, without updating the consumer (the AbstractFactoryTest class), we added a new family of vehicles, the middle-end cars and bikes; kudos to the Abstract Factory pattern for that wonderfulness!

Impacts of the Abstract Factory

Before concluding, what would happen if we packed everything in a large interface instead of using an Abstract Factory (breaking the ISP along the way)? We could have created something like the following interface:

public interface ILargeVehicleFactory
{
    HighEndBike CreateHighEndBike();
    HighEndCar CreateHighEndCar();
    LowEndBike CreateLowEndBike();
    LowEndCar CreateLowEndCar();
}

As we can see, the preceding interface contains four specific methods and seems docile. However, the consumers of that code would be tightly coupled with those specific methods. For example, to change a consumer’s behavior, we’d need to update its code, like changing the call from CreateHighEndBike to CreateLowEndBike, which breaks the OCP. On the other hand, with the factory method, we can set a different factory for the consumers to spit out different results, which moves the flexibility out of the object itself and becomes a matter of composing the object graph instead (more on that in the next chapter).Moreover, when we want to add mid-range vehicles, we must update the ILargeVehicleFactory interface, which becomes a breaking change (the implementation(s) of the ILargeVehicleFactory must be updated). Here’s an example of the two new methods:

public interface ILargeVehicleFactory
{
    HighEndBike CreateHighEndBike();
    HighEndCar CreateHighEndCar();
    LowEndBike CreateLowEndBike();
    LowEndCar CreateLowEndCar();
    MidRangeBike CreateMidRangeBike();
    MidRangeCar CreateMidRangeCar();
}

From there, once the implementation(s) are updated, if we want to consume the new mid-range vehicles, we need to open each consumer class and apply the changes there, which once again breaks the OCP.

The most crucial part is understanding and seeing the coupling and its impacts. Sometimes, it’s okay to tightly couple one or more classes together as we don’t always need the added flexibility the SOLID principles and some design patterns can bring.

Now let’s conclude before exploring the last design pattern of the chapter.

Conclusion

The Abstract Factory pattern is excellent for abstracting away the creation of object families, isolating each family and its concrete implementation, leaving the consumers unaware of the family created at runtime by the factory.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:

  • S: Each concrete factory is solely responsible for creating a family of objects. You could combine Abstract Factory with other creational patterns, such as the Prototype and Builder patterns for more complex creational needs.
  • O: We can create new families of objects, like the mid-range vehicles, without breaking existing client code.
  • L: We aim at composition, so there’s no need for any inheritance, implicitly discarding the need for the LSP. If you use abstract classes in your design, you must ensure you don’t break the LSP when creating new abstract factories.
  • I: Extracting a small abstraction with many implementations where each concrete factory focuses on one family makes that interface very focused on one task instead of having a large interface that exposes all types of products (like the ILargeVehicleFactory interface).
  • D: By depending only on interfaces, the consumer is unaware of the concrete types it uses.

Next, we explore the last design pattern of the chapter.

Project – Abstract Factory – Strategy, Abstract Factory, and Singleton Design Patterns

Context: We need to support the creation of multiple models of vehicles. We also need to be able to add new models as they become available without impacting the system. To begin with, we only support high-end and low-end models, but we know this will change sooner rather than later. The program must only support the creation of cars and bikes.For the sake of our demo, the vehicles are just empty classes and interfaces because learning how to model vehicles is not necessary to understand the pattern; that would be noise. The following code represents those entities:

public interface ICar { }
public interface IBike { }
public class LowEndCar : ICar { }
public class LowEndBike : IBike { }
public class HighEndCar : ICar { }
public class HighEndBike : IBike { }

Next, we look at the part that we want to study—the factories:

public interface IVehicleFactory
{
    ICar CreateCar();
    IBike CreateBike();
}
public class LowEndVehicleFactory : IVehicleFactory
{
    public IBike CreateBike() => new LowEndBike();
    public ICar CreateCar() => new LowEndCar();
}
public class HighEndVehicleFactory : IVehicleFactory
{
    public IBike CreateBike() => new HighEndBike();
    public ICar CreateCar() => new HighEndCar();
}

The factories are simple implementations that describe the pattern well:

  • LowEndVehicleFactory creates low-end models.
  • HighEndVehicleFactory creates high-end models.

The consumer of this code is an xUnit test project. Unit tests are often your first consumers, especially if you are doing test-driven development (TDD).To make the tests easier, I created the following base test class:

using Xunit;
namespace Vehicles;
public abstract class BaseAbstractFactoryTest<TConcreteFactory, TExpectedCar, TExpectedBike>
    where TConcreteFactory : IVehicleFactory, new()
{
    // Test methods here
}

The key to that class is the following generic parameters:

  • The TConcreteFactory parameter represents the type of concrete factory we want to test. Its generic constraint specifies that it must implement the IVehicleFactory interface and have a parameterless constructor.
  • The TExpectedCar parameter represents the type of ICar we expect from the CreateCar method.
  • The TExpectedBike parameter represents the type of IBike we expect from the CreateBike method.

The first test method contained by that class is the following:

[Fact]
public void Should_create_a_ICar_of_type_TExpectedCar()
{
    // Arrange
    IVehicleFactory vehicleFactory = new TConcreteFactory();
    var expectedCarType = typeof(TExpectedCar);
    // Act
    ICar result = vehicleFactory.CreateCar();
    // Assert
    Assert.IsType(expectedCarType, result);
}

The preceding test method creates a vehicle factory using the TConcreteFactory generic parameter, then creates a car using that factory. Finally, it asserts ICar instance is of the expected type.The second test method contains by that class is the following:

[Fact]
public void Should_create_a_IBike_of_type_TExpectedBike()
{
    // Arrange
    IVehicleFactory vehicleFactory = new TConcreteFactory();
    var expectedBikeType = typeof(TExpectedBike);
    // Act
    IBike result = vehicleFactory.CreateBike();
    // Assert
    Assert.IsType(expectedBikeType, result);
}

The preceding test method is very similar and creates a vehicle factory using the TConcreteFactory generic parameter but then creates a bike instead of a car using that factory. Finally, it asserts IBike instance is of the expected type.

I used the ICar and IBike interfaces to type the variables instead of var, to clarify the result variable type. In another context, I would have used var instead. The same applies to the IVehicleFactory interface.

Now, to test the low-end factory, we declare the following test class:

namespace Vehicles.LowEnd;
public class LowEndVehicleFactoryTest : BaseAbstractFactoryTest<LowEndVehicleFactory, LowEndCar, LowEndBike>
{
}

That class solely depends on the BaseAbstractFactoryTest class and specifies the types to test for (highlighted).Next, to test the high-end factory, we declare the following test class:

namespace Vehicles.HighEnd;
public class HighEndVehicleFactoryTest : BaseAbstractFactoryTest<HighEndVehicleFactory, HighEndCar, HighEndBike>
{
}

Like the low-end factory, that class depends on the BaseAbstractFactoryTest class and specifies the types to test for (highlighted).

In a more complex scenario where we can’t use the new() generic constraint, we can leverage an IoC container to create the instance of TConcreteFactory and optionally mock its dependencies.

With that test code, we created the following two sets of two tests:

  • A LowEndVehicleFactory class that should create a LowEndCar instance.
  • A LowEndVehicleFactory class that should create a LowEndBike instance.
  • A HighEndVehicleFactory class that should create a HighEndCar instance.
  • A HighEndVehicleFactory class that should create a HighEndBike instance.

We now have four tests: two for bikes and two for cars.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 without knowing what they are testing but the abstraction. That shows how loosely coupled the consumers (tests) and the factories are.

We would use the ICar or the IBike instances in a real-world program to do something relevant based on the specifications. 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 test code (consumer) is not aware of the implementations.Next, we extend our implementation.

Conclusion– Strategy, Abstract Factory, and Singleton Design Patterns

The Strategy design pattern is very effective at delegating responsibilities to other objects, allowing you to hand over the responsibility of an algorithm to other objects while keeping its usage trivial. It also allows having a rich interface (context) with behaviors that can change at runtime.As we can see, the Strategy pattern is excellent at helping us follow the SOLID principles:

  • S: It helps extract responsibilities from external classes and use them interchangeably.
  • O: It allows extending classes without updating its code by changing the current strategy at runtime, which is pretty much the actual definition of the OCP.
  • L: It does not rely on inheritance. Moreover, it plays a large role in the composition over inheritance principle, helping us avoid inheritance altogether and the LSP.
  • I: By creating smaller strategies based on lean and focused interfaces, the Strategy pattern is an excellent enabler of the ISP.
  • D: The creation of dependencies is moved from the class using the strategy (the context) to the class’s consumer. That makes the context depend on abstraction instead of implementation, inverting the flow of control.

C# Features

If you noticed C# features you are less familiar with, Appendix A explains many of them briefly.

Next, let’s explore the Abstract Factory pattern.

The Abstract Factory design pattern

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 Strategy pattern is the backbone of dependency injection, enabling the composition of complex object trees, while factories are used to create some of those complex objects that can’t be assembled automatically by a dependency injection library. More on that in the next chapter.

Goal

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 automotive vehicles. There are multiple vehicle types, and there are multiple models and makes for each type. We can use the Abstract Factory pattern to model this sort of scenario.

Note

The Factory Method pattern also 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.

Design

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 creation of an object from its consumers.That allows adding or removing families of objects produced together without impacting the consumers (all actors communicate through abstractions).In our case, the family (the set of objects the factory can produce) is composed of a car and a bike, and each factory (family) must produce both objects.If we think about vehicles, we could have the ability to create low- and high-end models of each vehicle type. Here is a diagram representing how to achieve that using the Abstract Factory pattern:

 Figure 7.4: Abstract Factory class diagramFigure 7.4: Abstract Factory class diagram 

In the diagram, we have the following elements:

  • The IVehicleFactory interface represents the Abstract Factory. It defines two methods: one that creates cars of type ICar and another that creates bikes of type IBike.
  • The HighEndVehicleFactory class is a concrete factory implementing the IVehicleFactory interface. It handles high-end vehicle model creation, and its methods return HighEndCar or HighEndBike instances.
  • The LowEndVehicleFactory is a second concrete factory implementing the IVehicleFactory interface. It handles low-end vehicle model creation, and its methods return LowEndCar or LowEndBike instances.
  • LowEndCar and HighEndCar are two implementations of ICar.
  • LowEndBike and HighEndBike are two implementations of IBike.

Based on that diagram, consumers use the concrete factories through the IVehicleFactory interface and should not be aware of the implementation used underneath. Applying this pattern abstracts away the vehicle creation process.

Project – Strategy – Strategy, Abstract Factory, and Singleton Design Patterns-2

The _items field is an ImmutableArray<string>, which makes changing the list impossible from the outside. For example, a consumer cannot pass a List<string> to the constructor, then change it later. Immutability has many advantages.

Let’s experiment with this by looking at the Consumer.API project a REST API application that uses the previous code. Next is a breakdown of the Program.cs file:

using MySortingMachine;
SortableCollection data = new(new[] {
    “Lorem”, “ipsum”, “dolor”, “sit”, “amet.”
});

The data member is the context, our sortable collection of items. Next, we look at some boilerplate code to create the application and serialize enum values as strings:

var builder = WebApplication.CreateBuilder(args);
builder.Services.ConfigureHttpJsonOptions(options => {
    options.SerializerOptions.Converters
        .Add(new JsonStringEnumConverter());
});
var app = builder.Build();

Finally, the last part represents the consumer of the context:

app.MapGet(“/”, () => data);
app.MapPut(“/”, (ReplaceSortStrategy sortStrategy) =>
{
    ISortStrategy strategy = sortStrategy.SortOrder == SortOrder.Ascending
        ?
new SortAscendingStrategy()
        : new SortDescendingStrategy();
    data.SetSortStrategy(strategy);
    data.Sort();
    return data;
});
app.Run();
public enum SortOrder
{
    Ascending,
    Descending
}
public record class ReplaceSortStrategy(SortOrder SortOrder);

In the preceding code, we declared the following endpoints:

  • The first endpoint returns the data object when a client sends a GET request.
  • The second endpoint allows changing the sort strategy based on the SortOrder enum when a client sends a PUT request. Once the strategy is modified, it sorts the collection and returns the sorted data.

The highlighted code represents the consumption of this implementation of the strategy pattern.

The ReplaceSortStrategy class is an input DTO. Combined with the SortOrder enum, they represent the data contract of the second endpoint.

When we run the API and request the first endpoint, it responds with the following JSON body:

[
  “Lorem”,
  “ipsum”,
  “dolor”,
  “sit”,
  “amet.”
]

As we can see, the items are in the order we set them because the code never called the Sort method. Next, let’s send the following HTTP request to the API to change the sort strategy to “descending”:

PUT https://localhost:7280/
Content-Type: application/json
{
    “sortOrder”: “Descending”
}

After the execution, the endpoint responds with the following JSON data:

[
  “sit”,
  “Lorem”,
  “ipsum”,
  “dolor”,
  “amet.”
]

As we can see from the content, the sorting algorithm worked. Afterward, the list will remain in the same order if we query the GET endpoint. Next, let’s look at this use case using a sequence diagram:

 Figure 7.3: Sequence diagram sorting the items using the “sort descending strategy”Figure 7.3: Sequence diagram sorting the items using the “sort descending strategy” 

The preceding diagram shows the Program creating a strategy and assigning it to SortableCollection using its SetSortStrategy method. Then, when the Program calls the Sort() method, the SortableCollection instance delegates the sorting computation to the underlying implementation of the ISortStrategy interface. That implementation is the SortDescendingStrategy class (the strategy) which was set by the Program at the beginning.

Sending another PUT request but specifying the Ascending sort order end up in a similar result, but the items would be sorted alphabetically.

The HTTP requests are available in the Consumer.API.http file.

From a strategy pattern perspective, the SortableCollection class (the context) is responsible for referencing and using the current strategy.