C# Corner

Make Your Types Report Their State

You probably write a lot of code to test object state. A better approach might be to make your objects report their own state.

TECHNOLOGY TOOLBOX: C#

Throughout the course of an application, you probably write quite a bit of code to test whether objects are valid before you use them. That's a lot of grunt work. Unfortunately, you often end up distributing this code throughout many applications because you don't have a strategy to manage any object's valid or invalid state. I'll show you a way around that, explaining how you can save time by creating code that enables objects to self-report their state. Along the way, I'll explain the basic concepts behind managing object validity.

Before you can add the code to make objects report their own state, you need to define what it means for an object of one of your classes to be valid. Until that definition is clear to you, you can't report if any object is in a valid state. Let's begin with a discussion of two important concepts: object invariants and object validity. Once you define those, you can go on to techniques that you can use to make every object report its state.

Object invariants are those conditions that must be true. Invariants are usually simple to express in English: A person object must have a non-empty last name and first name. A bank account must have a valid account number and a positive bank balance. A Customer must have a valid name, address, and phone number. Notice that the last statement on a customer defines a valid customer in terms of having valid member properties (name, address, and phone number). This will be true for all but the simplest types; you define a valid object by examining all of its members. If its members are valid, then the object itself is valid.

Every type you define has a set of valid object invariants. The object is valid when those invariants are satisfied. For every type you define, you should stop and think a bit about what that type's object invariants are. If you think about the object invariants as you write class members, you can create classes that are easier to use and harder to misuse.

One of the simplest ways to enforce proper object invariants is to create immutable types. An immutable type cannot change its state once it has been created. This means you can localize your checks on the object invariants to one place: in its constructors. If you construct an immutable object in a valid state, it will always have a valid state.

Immutable objects are also easy to use. All accessor methods can be written so that they never throw any exceptions. There should be no reason for any of those methods to throw an exception. The object is in a valid state, so there's no reason it shouldn't be able to return any property.

Other types require more work to enforce object invariants. Even in these cases, methods that don't change the state of the object will preserve any object invariants. You can define an API so that all public methods preserve object invariants. If the parameters would violate the object invariants, the method would throw an exception. For example, assume you have a FirstName property setter for a Person type. This type would throw an exception if the new value for FirstName were invalid.

Unfortunately, you might not always be able to define an API that preserves object invariants. For example, you might be reading objects from a file where you can't guarantee that the file contents are valid. Or, you might need to support a public API where the order of different calls could leave an object in an invalid state (temporarily). This can happen when you're creating components that must work with a designer model, where different properties might be interrelated.

You might not be able to refactor your class to remove every case where your object invariants can be broken. You might need to support a default constructor for serialization, and you might need certain fields for your object to function. During the period of time between construction of your object and the time a user gives it those other important resources, the object isn't valid. For most types, you'll likely find that you need to allow developers who use your code to put an object in an invalid state, even though it's wrong.

At this point, you should pay close attention to the difference between an object invariant, and the fact that an object is valid. The Person class mentioned previously is valid when the LastName and FirstName contain valid values (non null, non empty). That's not the same as an object invariant. You can create the Person type with empty (null) FirstName and LastName values. An invariant object isn't the same thing as a valid object. Types that have invariants that also allow an invalid state are much harder to code correctly. You can count on the invariants being true, but that doesn't mean it's a valid object.

Object invariants are properties of a type that must always be true. You can assume that object invariants are true. If any object invariant is ever false, something is horribly amiss in your application. Object validity is a bit weaker in some cases. For example, consider the code for the Person type: Its invariants allow null values for the name, but the Person type itself isn't valid.

Report Object Validity
Now that the definitions are out of the way, let's look at the simplest way to convey to other developers that an object is valid.

For C# developers, the easiest way to provide a simple method for developers to test an object's state is to override operator bool(). Consider this example for a Person type:

public class Person:
{
    // other elements elided
    public static implicit operator
       bool(Person p)
    {
    return !(string.IsNullOrEmpty(
		p.FirstName) ||
    	string.IsNullOrEmpty(
		p.LastName));
    }
}

This addition lets developers test for the validity of an object:

Person p1 = new Person();
if (p1)
{
    // do work
}
else
{
    // p1 is not valid
}

You overloaded operator bool, rather than operator true and operator false, so you can create more complicated tests when you need to test the validity of more than one object at a time:

Person p1 = new Person();
Person p2 = new Person();
if (p1 && p2)
{
    // do work
}
else
{
    // p1 is not valid
}

There's another reason to use operator bool instead of operator true and operator false. Overloading operator true and operator false gives you the ability to create values that represent three different states: true, false, and neither. This could model a database Boolean value, if you so choose. You could also write a type that returned both true and false, but that's obviously meaningless. Most developers who see a class that overrides operator true and operator false would think that such a class represents some form of a tri-state value.

Enforce Object Validity
Once you've got an operator bool in your type, you can call it from inside a specific public method of your type. This lets you throw exceptions immediately when a method needs to check whether the current object is valid before trying to execute any actions.

Enforcement is where it's important to realize the difference between a valid object and an object invariant. Depending on the type, invalid objects exist. You are always safe enforcing invariants, but you'd also like to enforce validity. If possible, this is the case where you would like to modify the invariants to match a type's concept of validity. So, you can change the way you express your object invariant: "A person who has been given a valid name can never subsequently have an empty or null name." At this point, you're ready to create a more structured Person object that enforces that invariant (see Listing 1). There's more code in this listing, all of it intended to help you enforce the object invariant.

That's an important design point: It's not necessarily worth the effort required to create validation code for all possible types. For example, some types have invariants that are already satisfied by the language or the environment. Immutable types automatically satisfy their invariants if their initial state is valid. Some types have a small set of mutator methods. In those cases (such as Person), it would be more efficient to write validation code around each transaction. In fact, the Person type in Listing 1 does this. The set accessors for both properties validate the parameters before proceeding with the change. (Note that production code would throw an exception if this check failed.) More complicated types would benefit from creating a single method to check all state assumptions. You can also choose to expose that method as part of your public API if you desire.

About the Author

Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at [email protected].

comments powered by Disqus

Featured

  • Compare New GitHub Copilot Free Plan for Visual Studio/VS Code to Paid Plans

    The free plan restricts the number of completions, chat requests and access to AI models, being suitable for occasional users and small projects.

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

Subscribe on YouTube