Now that we have explored various techniques to break dependencies, such as using interfaces and dependency injection, we will focus on a specific type of dependency: global state. Global state can introduce hidden dependencies and make our code difficult to test and maintain. In this lesson, we will learn how to identify global state and refactor it using dependency injection, enhancing the modularity and testability of our code.
Global state refers to variables or data that are accessible from anywhere in our application. These variables often reside in a global scope, making them available to any part of the code. While this might seem convenient, it can lead to several issues. For instance, global state can create hidden dependencies between different parts of our code, making it difficult to understand how changes in one area might affect another. Additionally, global state can complicate testing, as it introduces shared data that can lead to unpredictable test results.
Identifying global state in our code is the first step toward refactoring it. We should look for variables that are declared at the top level of our application or within static classes. These variables are often accessed directly by various parts of our code, indicating a reliance on global state. Take a look at the OrderProcessor
class below:
C#1public class OrderProcessor 2{ 3 public const decimal BULK_ORDER_THRESHOLD = 1000; 4 public const decimal BULK_ORDER_DISCOUNT = 0.1m; // 10% discount 5 public const int MAX_ITEMS_PER_ORDER = 50; 6 7 public bool ProcessOrder(Order order) 8 { 9 // ... order processing logic ... 10 }
Constants like BULK_ORDER_THRESHOLD
and MAX_ITEMS_PER_ORDER
are examples of global state. They are used throughout the class without being passed as parameters, making them potential candidates for refactoring. They could also be used in other parts of the system for other logic.
To refactor global state, we can use dependency injection to pass configuration data as parameters or through interfaces. This approach allows us to encapsulate the state within a configuration object, which can be injected into classes that need it. Maintaining backward compatibility is crucial during refactoring to ensure the established codebase continues to function without modification. We can achieve this by providing a default constructor that initializes the configuration with the original global constants. Here's a streamlined example:
C#1public class OrderProcessor 2{ 3 public const decimal BULK_ORDER_THRESHOLD = 1000; 4 public const decimal BULK_ORDER_DISCOUNT = 0.1m; // 10% discount 5 public const int MAX_ITEMS_PER_ORDER = 50; 6 private readonly IOrderConfiguration OrderConfiguration; 7 8 // Default constructor for backward compatibility 9 public OrderProcessor() 10 { 11 OrderConfiguration = new OrderConfiguration 12 { 13 BulkOrderThreshold = BULK_ORDER_THRESHOLD, 14 BulkOrderDiscount = BULK_ORDER_DISCOUNT, 15 MaxItemsPerOrder = MAX_ITEMS_PER_ORDER 16 }; 17 } 18 19 // Constructor with dependency injection 20 public OrderProcessor(IOrderConfiguration orderConfiguration) 21 { 22 OrderConfiguration = orderConfiguration; 23 } 24 25 public bool ProcessOrder(Order order) 26 { 27 // Order processing logic using OrderConfiguration 28 } 29}
In this example, the OrderProcessor
class provides a default constructor that initializes the OrderConfiguration
with the original global constants, ensuring backward compatibility. Additionally, a constructor with dependency injection is available for more flexible configuration, allowing different configurations to be provided for testing or other scenarios.
Implementing dependency injection for global state involves creating interfaces or configuration objects that encapsulate the state. These objects are then passed to classes that require them, either through constructors or method parameters. This approach not only reduces the reliance on global state but also improves the modularity of our code. By decoupling configuration data from the classes that use it, we can easily swap out different configurations for testing or other purposes.
In this lesson, we explored the concept of global state and its drawbacks, such as hidden dependencies and testing challenges. We learned how to identify global state in code and refactor it using dependency injection. By encapsulating state within configuration objects and injecting them into classes, we can improve the modularity and testability of our code. As we move on to the practical exercises, we'll have the opportunity to apply these concepts and reinforce our understanding of removing global state. Good luck, and enjoy the practice!