by Zoran Horvat
Internal consistency is fundamental requirement for all objects. Without consistency guarantee, all clients of an object would have to defend from unpredictable circumstances whenever they try to do anything. In consistent world, client calls an operation on an object like this:
obj.DoSomethingForMe();
But if client is uncertain about object's status, it must plug all the holes:
if (obj.AreYouFine())
obj.DoSomethingForMe();
else
this.GoBellyUp();
This piece of code has two fundamental flaws. First, it tests object's status explicitly - something that heavily relies on programmer's consciousness. Poorly informed coder, or just a sleepy one, could easily forget to test the object before accessing it. Second flaw is that the code has a negative branch which is activated when subordinate object is not fine. This leaves the ultimate consumer of this code in a very unpleasant situation because there is no certainty that the operation is going to succeed at any given time. Even worse, we cannot say whether the desired operation is going to be done, or is it going to explode back into our face.
Bottom line is that any object we reference must be fine at all times. Object with incomplete content, missing references, mismatched internal values, etc. is inconceivable.
We can generally divide all objects by their ability to change own internal state. Those objects that do not change after they have been initialized are referred to as immutable objects. On the opposite end there are mutable objects, which expose operations that change their internal state.These two kinds of objects are kept consistent in different ways.
Example of immutable objects are instances of the System.String class. Once a string is initialized, it doesn't allow changes to its underlying buffer. Any operation that sounds like changing the string actually instantiates another string object and returns it as a result. For example, Insert method lets us insert one string into another at specific position. It does so by creating another string with appropriate content:
string a = "sing";
string b = a.Insert("ometh", 1);
// b: "something"
Constructor is the only place where internal state of an immutable object can be set. The question how to ensure consistency of such an object then points in constructor's direction. Constructor is in charge to validate its arguments and to make sure that object is properly and fully initialized from them.
As a practical demonstration, consider a class which represents an item in a purchase receipt:
public class PurchaseItem
{
public string UPC { get; private set; }
public decimal UnitCost { get; private set; }
public int Quantity { get; private set; }
public decimal TotalCost
{
get
{
return this.UnitCost * this.Quantity;
}
}
public PurchaseItem(string upc, decimal unitCost, int quantity)
{
if (string.IsNullOrEmpty(upc)
|| unitCost <= 0.0M
|| quantity <= 0)
throw new ArgumentException();
this.UPC = upc;
this.UnitCost = unitCost;
this.Quantity = quantity;
}
}
This class is immutable. Product code (UPC), unit cost and quantity purchased cannot be changed after initialization. Now suppose that we have a requirement to let the user change quantity before the receipt is printed. That can easily be done in brigh tradition of immutable objects - by creating a new object from the existing one:
public class PurchaseItem
{
...
public PurchaseItem ChangeQuantity(int quantity)
{
return new PurchaseItem(this.UPC, this.UnitCost, quantity);
}
}
Observe that in this method we have not checked whether quantity passed to the method is positive or not. That validation is performed by the constructor when new object is initialized. Current object really doesn't care about validity of this argument. It is the new object responsible to make itself valid or to fail to initialize if consistency rules have been violated.
Objects that are allowed to change after initialization still have to be consistent. This means that their constructor is responsible for validation none the less. But after the initialization, client is free to call some operations that modify this established consistent state. Those operations must ensure that consistency is preserved over state changes.
Take a purchase receipt as an example. Receipt class will aggregate a set of purchased items. Its responsibilities will be in the domain of manipulating the items and providing basic aggregation services, such as summing up item costs. Here is the first look at the Receipt class:
public class Receipt
{
private IList<PurchaseItem> Items;
public Receipt()
{
this.Items = new List<PurchaseItem>();
}
public void AddItem(PurchaseItem item)
{
if (item == null)
throw new ArgumentNullException("item");
AddItemInternal(item);
}
public PurchaseItem RemoveItem(string upc)
{
if (string.IsNullOrEmpty(upc))
throw new ArgumentException();
PurchaseItem item =
this
.Items
.Where(i => i.UPC == upc)
.Single(); // may throw exception
this.Items.Remove(item);
return item;
}
public decimal TotalCost
{
get
{
return this.Items.Sum(item => item.TotalCost);
}
}
private void AddItemInternal(PurchaseItem item)
{
if (ContainsUpc(item.UPC))
AddQuantityToItem(item.UPC, item.Quantity);
else
AddNewItem(item);
}
private bool ContainsUpc(string upc)
{
return this.Items.Any(item => item.UPC == upc);
}
private void AddQuantityToItem(string upc, int quantity)
{
PurchaseItem item = RemoveItem(upc);
item = item.ChangeQuantity(item.Quantity + quantity);
AddNewItem(item);
}
private void AddNewItem(PurchaseItem item)
{
this.Items.Add(item);
}
}
Observe how we had to make many tests before any operation was done which alters internal state of the object. Actually, most of the Receipt class code is dedicated to preserving internal consistency of the object. That goal is far from trivial to achieve. Therefore, before deciding to let the object be mutable, consider cost of that decision. In many cases it is not worth the price. Instead of trying to predict all issues that may arise while changing internal state of the object, it might be quite sufficient to let the final state be specified through the constructor and then seal the object for changes, i.e. make it immutable.
In this article we have demonstrated two ways in which objects cope with consistency issues. One is simpler and it is fully implemented in the constructor. The other is more complicated and it copes with state transformations.
In practice, it is easier to write immutable objects because with them we do not have to consider all possible state changes. That saves many lines of code and reduces chance that we have forgot some viable scenario. Only if it is absolutely necessary shall we design a mutable class. And in that case, we must ensure that all valid transitions are covered with logic which protects object from corruption.
If you wish to learn more, please watch my latest video courses
In this course, you will learn the basic principles of object-oriented programming, and then learn how to apply those principles to construct an operational and correct code using the C# programming language and .NET.
As the course progresses, you will learn such programming concepts as objects, method resolution, polymorphism, object composition, class inheritance, object substitution, etc., but also the basic principles of object-oriented design and even project management, such as abstraction, dependency injection, open-closed principle, tell don't ask principle, the principles of agile software development and many more.
More...
In this course, you will learn how design patterns can be applied to make code better: flexible, short, readable.
You will learn how to decide when and which pattern to apply by formally analyzing the need to flex around specific axis.
More...
This course begins with examination of a realistic application, which is poorly factored and doesn't incorporate design patterns. It is nearly impossible to maintain and develop this application further, due to its poor structure and design.
As demonstration after demonstration will unfold, we will refactor this entire application, fitting many design patterns into place almost without effort. By the end of the course, you will know how code refactoring and design patterns can operate together, and help each other create great design.
More...
In four and a half hours of this course, you will learn how to control design of classes, design of complex algorithms, and how to recognize and implement data structures.
After completing this course, you will know how to develop a large and complex domain model, which you will be able to maintain and extend further. And, not to forget, the model you develop in this way will be correct and free of bugs.
More...
Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.