Using Custom Value Types To Protect System From Invalid Data

Using Custom Value Types To Protect System From Invalid Data

There are plenty of resources where you can learn to use right built-in value type, such as decimal when dealing with money. Unfortunately, picking a right value type is only the first step to prevent invalid data corrupting the system. For example, in a certain business case, only values from 1 to 100 could be valid. In this blog post, I'll cover using custom value types to increase trust in the system. The examples are in C#, but the information can be applied to many languages.

Not trusting the input

The (object-oriented) programmers have a good understanding when to create class. If the system would have data age, firstName and lastName then it is very likely that the developer creates class Person that will have those three as properties, instead of moving each of them separately inside the software.

When the class is created then some sanity check can be done in the constructor, such as, first or last name should not be empty or null.

Unfortunately, when it comes to the primitive types (boolean, decimal, double, etc.) too much data is placed into a variable that doesn't describe the business rule. Let's take integer as an example. It could have 10 correct values (1-10) and around 4,294,967,285 wrong ones!

As an example, in accounting software, you might have a business rule that account number can be between 1000 - 19999. If I write a function to get account type, I might need to do some sanity check, so that I won't do a lot of work to resolve the account type if we already know that the input is invalid.

public AccountType GetAccountType(int accountNumber)
{
	if (accountNumber < 1000 || accountNumber >= 20000)
	{
		throw new ArgumentException($"{nameof(accountNumber)} is not a valid account number");
	}
	// TODO: crazy logic
	return AccountType.Normal;
}

There are several problems. The account number validation rule doesn't "belong" to the GetAccountType as it violates the single-responsibility principle. Also, duplicate code will be created when other account number related functions are created.

Constraining inputs with custom types

Here is a C# example of the custom type (adapted version of an example from Functional Programming in C# by Enrico Buonanno).

public class AccountNumber
{
	public int Value { get; }

	public AccountNumber(int value)
	{
		if (!IsValid(value))
			throw new ArgumentException($"{value} is not a valid account number");

		Value = value;
	}

	private static bool IsValid(int accountNumber)
	   => accountNumber >= 1000 && accountNumber < 20000;
}

Converting the earlier example to accept AccountNumber is easy.

public AccountType GetAccountType(AccountNumber accountNumber)
{
	// TODO: crazy logic
	return AccountType.Normal;
}

The first thing to catch attention is the cleaner GetAccountType body, but there are many other benefits when the system gets bigger.

  • a developer can be sure that if there is an account number then it must be valid
  • data validation in one spot
  • erroneous data is caught early

I didn't write the function body which would include the logic, as it would distract the reader. There is one thing to consider, so let's write some logic.

public static AccountType GetAccountType(AccountNumber accountNumber)
{
	if (accountNumber == 15000) {
		return AccountType.NotSoNormal;
	}
	
	return AccountType.Normal;
}

The code doesn't compile, the error I receive in the LINQPad: CS0019: Operator '==' cannot be applied to operands of type 'UserQuery.AccountNumber' and int

To put it simply, the custom type is not compatible with the value type (int).

Here is a version of the AccountNumber that allows comparing equality against integer.

public class AccountNumber
{
	private int Value { get; }

	public AccountNumber(int value)
	{
		if (!IsValid(value))
			throw new ArgumentException($"{value} is not a valid account number");

		Value = value;
	}

	private static bool IsValid(int accountNumber)
	   => accountNumber >= 1000 && accountNumber < 20000;

	public static bool operator ==(AccountNumber accountNumber, int value) 
		=> accountNumber.Value == value;
		
	public static bool operator !=(AccountNumber accountNumber, int value)
		=> accountNumber.Value != value;
}

Conclusion

Even though defining a custom value type has clear benefits it has also downsides. For example, writing those operator overrides to support built-in value types doesn't feel time well-spent. I am quite confident that in every system there are some domain specific entities that are critical to be correct and are used often in the business logic, that's where you should consider defining the custom value type.

When you go back to your software project, maybe you can spot some important business values placed in integers, floats, decimals, etc. and you can then consider could it be more "stricter" definition.