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 object creation using a few classic, simple, and yet powerful design patterns from the Gang of Four (GoF). These patterns allow developers to encapsulate and reuse behaviors, centralize object creation, add flexibility to our designs, or control object lifetime. Moreover, you will most likely use some of them in all 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), known as the Gang of Four (GoF). In that book, they introduce 23 design patterns, some of which we revisit in this book.
Why are they that important? Because they are the building blocks of robust object composition and help create flexibility and reliability. Moreover, in Chapter 8, Dependency Injection, we make those patterns even more powerful!But first things first. In this chapter, we cover the following topics:
- The Strategy design pattern
- The Abstract Factory design pattern
- The Singleton design pattern
The Strategy design pattern
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. Moreover, it 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. The next chapter covers how to use the Strategy pattern to compose systems dynamically.
Goal
The Strategy pattern aims to extract an algorithm (a strategy) from the host class needing it (the context or consumer). 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 to that data and use the same user interface to display it. To achieve this, we could use the Strategy pattern to create two strategies, one named FetchDataFromSql and the other FetchDataFromCosmosDb. Then we could plug the strategy we need at runtime in the context class. That way, when the consumer calls the context, the context does not need to know 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 (an interface).
Design
Before any further explanation, let’s take a look at the following class diagram:
Figure 7.1: Strategy pattern class diagram
Based on the preceding diagram, the building blocks of the Strategy pattern are the following:
- Context is a class that depends on the IStrategy interface and leverages an implementation of the IStrategy interface to execute the ExecuteAlgo method.
- IStrategy is an interface defining the strategy API.
- ConcreteStrategy1 and ConcreteStrategy2 represent one or more different concrete implementations of the IStrategy interface.
In the following diagram, we explore what happens at runtime. The actor represents any code consuming the Context object.
Figure 7.2: Strategy pattern sequence diagram
When the consumer calls the Context.SomeOperation() method, it does not know which implementation is executed, which is an essential part of this pattern. The Context class should not be aware of the strategy it uses either. It should run the strategy through the interface, unaware of the implementation. That is the strength of the Strategy pattern: it abstracts the implementation away from both the Context class and its consumers. Because of that, we can change the strategy during either the object creation or at runtime without the object knowing, changing its behavior on the fly.
Note
We could even generalize that last sentence and extend it to any interface. Depending on an interface breaks the ties between the consumer and the implementation by relying on that abstraction instead.