This is the first article in a small series on C # programming in a functional style. The series is not about LINQ, as one might think, but about more fundamental things. Inspired by F # th.
Immutability (immutability)
The biggest problem in the world of enterprise development is the struggle with complexity. Code readability is probably the first thing we should try to achieve when writing any more or less complex project. Without this, our ability to understand the code and make reasonable decisions based on this is significantly impaired.
Do mutable objects help us in reading the code? Let's look at an example:
')
// Create search criteria var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10); // Search customers IReadOnlyCollection<Customer> customers = Search(queryObject); // Adjust criteria if nothing found if (customers.Count == 0) AdjustSearchCriteria(queryObject, name); // Is queryObject changed here? Search(queryObject);
Has queryObject changed by the time of searching for customers a second time? Maybe yes. Or maybe not. It depends on whether this object was modified by the AdjustSearchCriteria method. To find out, we need to look inside this method, its signature does not give us enough information.
Compare this with the following code:
// Create search criteria var queryObject = new QueryObject<Customer>(name, page: 0, pageSize: 10); // Search customers IReadOnlyCollection<Customer> customers = Search(queryObject); if (customers.Count == 0) { // Adjust criteria if nothing found QueryObject<Customer> newQueryObject = AdjustSearchCriteria(queryObject, name); Search(newQueryObject); }
In this example, it is clear that AdjustSearchCriteria creates a new criterion object and then uses it for a new search.
So what's the problem with variable data structures?
- It is difficult to think about the code if there is no certainty whether the data transferred from one method to another changes.
- It is difficult to do the program execution if you have to go down several levels down the stack.
- In the case of a multithreaded application, understanding and debugging code is complicated many times.
How to create immutable types
In future versions of C #, the immutable keyword may appear. With its help, it will be possible to understand whether a type is immutable by simply looking at its signature. In the meantime, we have to use what we have.
If you have a relatively simple class, consider making it immutable. This guideline correlates with the concept of
Value Objects .
Take, for example, the ProductPile class, which describes a number of products for sale:
public class ProductPile { public string ProductName { get; set; } public int Amount { get; set; } public decimal Price { get; set; } }
To make it immutable, we can mark its properties as read-only and add a constructor:
public class ProductPile { public string ProductName { get; private set; } public int Amount { get; private set; } public decimal Price { get; private set; } public ProductPile(string productName, int amount, decimal price) { Contracts.Require(!string.IsNullOrWhiteSpace(productName)); Contracts.Require(amount >= 0); Contracts.Require(price > 0); ProductName = productName; Amount = amount; Price = price; } }
Now suppose you need to reduce the Amount property by one each time you sell one of the products. Instead of changing the existing object, we can create a new one based on the existing one:
public class ProductPile { public string ProductName { get; private set; } public int Amount { get; private set; } public decimal Price { get; private set; } public ProductPile(string productName, int amount, decimal price) { Contracts.Require(!string.IsNullOrWhiteSpace(productName)); Contracts.Require(amount >= 0); Contracts.Require(price > 0); ProductName = productName; Amount = amount; Price = price; } public ProductPile SubtractOne() { return new ProductPile(ProductName, Amount – 1, Price); } }
What does this give us?
- Having an immutable type, we need to validate its contracts only once, in the constructor. After that we can be absolutely sure that the object is in the correct state.
- Objects of immutable types are thread safe.
- Improved readability of the code, because it is no longer necessary to go into the stack in order to make sure that the variables with which the method works are not changed.
Restrictions
Of course, every good practice has its price. While small classes take full advantage of immutability, such an approach is not always applicable in the case of large types.
In the first place, immutability carries potential performance problems. If the object is quite large, the need to create a copy of it with each change can be a problem.
A good example here will be
unchangeable collections . The authors took into account potential performance problems and added a special Builder class that allows you to change the state of collections. After the collection is brought to the required state, it can be finalized by converting it into immutable:
var builder = ImmutableList.CreateBuilder<string>(); builder.Add(“1”); // Adds item to the existing object ImmutableList<string> list = builder.ToImmutable(); ImmutableList<string> list2 = list.Add(“2”); // Creates a new object with 2 items
Conclusion
In most cases, immutable types (especially if they are fairly simple) make the code better.
The remaining articles in the series:
English version of the article:
Functional C #: Immutability