Value Objects and Code Contracts
A while ago I came across an excellent presentation by Dan Bergh Johnsson on the topic of value objects. There is nothing revolutionary in this talk, but it was a good reminder of what value objects actually are and where they should be used. While the ideas in the video stand on their own, it is interesting to see how they complement and simplify pre- and post-condition checks on methods with code contracts.
Consider the following example (using Microsoft code contracts) in which a domain name is added to a collection that does not allow duplicates:
So for the privilege of using a string value instead of a value type, the following must be done in multiple places:
The DomainName type doesn't have any code contract pre-conditions other than the validation checks in the constructor. And because the type is immutable, once a DomainName is successfully constructed then it is guaranteed to be valid--it is impossible to have a DomainName instance that is not valid. The implicit operator functions also allow strings to be implicitly converted to DomainNames and vice versa, so changes to the DomainNameList class will be immediately backward-compatible with built in validation checks:
Since Equals and GetHashCode are now provided, the Entire DomainNameList class could be reduced to ISet<DomainName>.
Domain names and phone numbers are somewhat obvious targets for value objects, especially if they are a significant part of the primary domain. However, there are many, many cases where providing some encapsulated validation, normalization, and equals overloads makes life much better. For example, product and service codes; account, quote, invoice, and order numbers; quantities; prices; percentages; user or actor IDs; distinguished names (LDAP); and so on. Compare the following two method signatures and contracts:
Without value types:
With value types:
Using value types throughout allows complex preconditions to be removed from many methods, which simplifies the static analysis and avoids a whole category of contract warnings and other issues. The Equals and GetHashCode overrides enable the use of stock framework classes like lists and sets without having to implement special EqualityComparers and the like. Furthermore, the implicit operators make migration smooth, as conversions back and forth will occur automatically, allowing a refactoring to occur over time.
But most importantly, the validation, equality, hash code, and all other operations are now encapsulated in one type, which decreases coupling and increases maintainability. For example, suppose that user IDs need to change from E-mail address to LDAP DNs -- all that would need to change is the UserId type and anything that makes use of the value(s) it exposes.
So while contracts do a decent job of making sure you dot your Is and cross your Ts, runtime checks cannot be eliminated, and encapsulating them in strong types basically reduces the static analysis to a bunch of null checks that are easy to process and prove.
Consider the following example (using Microsoft code contracts) in which a domain name is added to a collection that does not allow duplicates:
public class DomainNameList { private IList<string> _domainNames = new List<string>(); public void AddDomainName(string domainName) { Contract.Requires(!string.IsNullOrWhitespace(domainName)); Contract.Requires(!Contains(domainName)); Contract.Requires(ValidationUtil.IsValid(domainName)); _domainNames.Add(domainName.Trim()); // Trim to ensure whitespace does not affect Equals } public bool Contains(string domainName) { Contract.Requires(!string.IsNullOrWhitespace(domainName)); domainName = domainName.Trim(); // Trim to ensure whitespace does not affect Equals return _domainNames.Any(dn => dn.Equals(domainName, StringComparison.OrdinalIgnoreCase)); } }
So for the privilege of using a string value instead of a value type, the following must be done in multiple places:
- Trim the domain name string so that string.Equals works properly
- Specify a special case-insensitive ordering so that mixed case does not affect equality
- Call to an external utility class to verify that the domain name is valid
public class DomainName : IEquatable<DomainName> { public const string DomainNamePattern = @"(?i)[a-z][a-z0-9\-_]{62}(\.[a-z][a-z0-9\-_]{62})*"; private readonly string _domainName; public DomainName(string domainName) { if(string.IsNullorWhiteSpace(domainName)) throw new ArgumentNullException("domainName"); if(!IsValid(domainName)) throw new ArgumentException("Invalid domain name", "domainName"); _domainName = domainName.Trim(); } public void IsValid(string domainName) { return domainName != null && Regex.IsMatch(domainName, DomainNamePattern); } public bool Equals(DomainName other) { if(ReferenceEquals(this, other)) return true; if(ReferenceEquals(other, null)) return false; return _domainName.Equals(other._domainName, StringComparison.OrdinalIgnoreCase); } public override bool Equals(object obj) { return Equals(obj as DomainName); } public override int GetHashCode() { return _domainName.ToLowerCase().GetHashCode(); } public override string ToString() { return _domainName; } public static implicit operator DomainName(string domainName) { return domainName == null ? null : new DomainName(domainName); } public static implicit operator string(DomainName domainName) { return domainName == null ? null : domainName.ToString(); } public static operator ==(DomainName left, DomainName right) { return Equals(left, right); } public static operator !=(DomainName left, DomainName right) { return !Equals(left, right); } }
The DomainName type doesn't have any code contract pre-conditions other than the validation checks in the constructor. And because the type is immutable, once a DomainName is successfully constructed then it is guaranteed to be valid--it is impossible to have a DomainName instance that is not valid. The implicit operator functions also allow strings to be implicitly converted to DomainNames and vice versa, so changes to the DomainNameList class will be immediately backward-compatible with built in validation checks:
public class DomainNameList { private IList<DomainName> _domainNames = new IList<DomainName>(); public void AddDomainName(DomainName domainName) { Contract.Requires(domainName != null); // No call to ValidationUtil! Contract.Requires(!Contains(domainName)); _domainNames.Add(domainName); // No trim! } public bool Contains(DomainName domainName) { // No !string.IsNullOrEmpty contract is needed because Equals is now overridden // No trim! return _domainNames.Contains(domainName); } }
Since Equals and GetHashCode are now provided, the Entire DomainNameList class could be reduced to ISet<DomainName>.
Domain names and phone numbers are somewhat obvious targets for value objects, especially if they are a significant part of the primary domain. However, there are many, many cases where providing some encapsulated validation, normalization, and equals overloads makes life much better. For example, product and service codes; account, quote, invoice, and order numbers; quantities; prices; percentages; user or actor IDs; distinguished names (LDAP); and so on. Compare the following two method signatures and contracts:
Without value types:
public void AddProduct(string userId, string accountNumber, string orderNumber, string productCode, int quantity, decimal price, decimal? discount) { Contract.Requires(!string.IsNullOrWhitespace(userId)); Contract.Requires(ValidationUtil.IsValidEmail(userId)); Contract.Requires(!string.IsNullOrWhitespace(accountNumber)); Contract.Requires(ValidationUtil.IsValidAccountNumber(accountNumber)); Contract.Requires(!string.IsNullOrWhitespace(orderNumber)); Contract.Requires(ValidationUtil.IsValidOrderNumber(orderNumber)); Contract.Requires(!string.IsNullOrWhitespace(productCode)); Contract.Requires(ValidationUtil.IsValidProductCode(productCode)); Contract.Requires(quantity > 0); Contract.Requires(price > 0); Contract.Requires(discount == null || discount >= 0); Contract.Requires(discount == null || discount <= 100); ... }
With value types:
public void AddProduct(UserId userId, AccountNumber accountNumber, OrderNumber orderNumber, ProductCode productCode, Quantity quantity, Price price, Discount discount) { Contract.Requires(userId != null); Contract.Requires(accountNumber != null); Contract.Requires(orderNumber != null); Contract.Requires(productCode != null); Contract.Requires(quantity != null); Contract.Requires(price != null); ... }
Using value types throughout allows complex preconditions to be removed from many methods, which simplifies the static analysis and avoids a whole category of contract warnings and other issues. The Equals and GetHashCode overrides enable the use of stock framework classes like lists and sets without having to implement special EqualityComparers and the like. Furthermore, the implicit operators make migration smooth, as conversions back and forth will occur automatically, allowing a refactoring to occur over time.
But most importantly, the validation, equality, hash code, and all other operations are now encapsulated in one type, which decreases coupling and increases maintainability. For example, suppose that user IDs need to change from E-mail address to LDAP DNs -- all that would need to change is the UserId type and anything that makes use of the value(s) it exposes.
So while contracts do a decent job of making sure you dot your Is and cross your Ts, runtime checks cannot be eliminated, and encapsulating them in strong types basically reduces the static analysis to a bunch of null checks that are easy to process and prove.
Comments
Post a Comment