Design by types: How to make invalid states inexpressible in C #
Typically, articles about type designing contain examples in functional languages - Haskell, F #, and others. This concept may not seem to apply to object-oriented languages, but it is not.
In this article, I will translate examples from an article by Scott Vlaschin Type Design: How to make invalid states inexpressible in idiomatic C #. I will also try to show that this approach is applicable not only as an experiment, but also in working code.
Create domain types
First you need to port the types from the previous article in the series , which are used in the examples in F #.
Wrap primitive types in domain
The F # examples use domain types instead of primitives for email address, US zip code, and state code. Let's try to wrap a primitive type in C #:
public sealed class EmailAddress
{
public string Value { get; }
public EmailAddress(string value)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
}
if (!Regex.IsMatch(value, @"^\S+@\S+\.\S+$"))
{
throw new ArgumentException("Email address must contain an @ sign");
}
Value = value;
}
public override string ToString()
=> Value;
public override bool Equals(object obj)
=> obj is EmailAddress otherEmailAddress &&
Value.Equals(otherEmailAddress.Value);
public override int GetHashCode()
=> Value.GetHashCode();
public static implicit operator string(EmailAddress address)
=> address?.Value;
}
var a = new EmailAddress("a@example.com");
var b = new EmailAddress("b@example.com");
var receiverList = String.Join(";", a, b);
I moved the address validation from the factory function to the constructor, since such an implementation is more typical for C #. We also had to implement a comparison and conversion to a string, which on F # would be done by the compiler.
On the one hand, the implementation looks quite voluminous. On the other hand, the specificity of the email address is expressed here only by checks in the constructor and, possibly, by the comparison logic. Most of this is infrastructure code, which, moreover, is unlikely to change. So, you can either make a template , or, at worst, copy the general code from class to class.
It should be noted that the creation of domain types from primitive values is not the specificity of functional programming. On the contrary, the use of primitive types is considered a sign of bad code in OOP . You can see examples of such wrappers, for example, in NLog and NBitcoin , and the standard type of TimeSpan is, in fact, a wrapper over the number of ticks.
Creating Value Objects
Now we need to create an analogue of the entry :
public sealed class EmailContactInfo
{
public EmailAddress EmailAddress { get; }
public bool IsEmailVerified { get; }
public EmailContactInfo(EmailAddress emailAddress, bool isEmailVerified)
{
if (emailAddress == null)
{
throw new ArgumentNullException(nameof(emailAddress));
}
EmailAddress = emailAddress;
IsEmailVerified = isEmailVerified;
}
public override string ToString()
=> $"{EmailAddress}, {(IsEmailVerified ? "verified" : "not verified")}";
}
Again, it took more code than F #, but most of the work can be done through refactoring in the IDE .
Like EmailAddress
, EmailContactInfo
this is a value object (in the sense of DDD , not value types in .NET ), which has long been known and used in object modeling.
Other types - StateCode
, ZipCode
, PostalAddress
and PersonalName
ported to C # in a similar manner.
Create contact
So, the code should express the rule "The contact must contain an email address or a postal address (or both addresses)." It is required to express this rule so that the correctness of the state is visible from the type definition and checked by the compiler.
Express various contact states
This means that a contact is an object containing the person’s name and either an email address, or a postal address, or both. Obviously, one class cannot contain three different sets of properties; therefore, three different classes must be defined. All three classes must contain the name of the contact and at the same time it should be possible to process contacts of different types in the same way, not knowing which addresses the contact contains. Therefore, the contact will be represented by an abstract base class containing the name of the contact, and three implementations with a different set of fields.
public abstract class Contact
{
public PersonalName Name { get; }
protected Contact(PersonalName name)
{
if (name == null)
{
throw new ArgumentNullException(nameof(name));
}
Name = name;
}
}
public sealed class PostOnlyContact : Contact
{
private readonly PostalContactInfo post_;
public PostOnlyContact(PersonalName name, PostalContactInfo post)
: base(name)
{
if (post == null)
{
throw new ArgumentNullException(nameof(post));
}
post_ = post;
}
}
public sealed class EmailOnlyContact : Contact
{
private readonly EmailContactInfo email_;
public EmailOnlyContact(PersonalName name, EmailContactInfo email)
: base(name)
{
if (email == null)
{
throw new ArgumentNullException(nameof(email));
}
email_ = email;
}
}
public sealed class EmailAndPostContact : Contact
{
private readonly EmailContactInfo email_;
private readonly PostalContactInfo post_;
public EmailAndPostContact(PersonalName name, EmailContactInfo email, PostalContactInfo post)
: base(name)
{
if (email == null)
{
throw new ArgumentNullException(nameof(email));
}
if (post == null)
{
throw new ArgumentNullException(nameof(post));
}
email_ = email;
post_ = post;
}
}
You may argue that you must use the composition, and not inheritance, and generally it is necessary to inherit behavior, not data. The remarks are fair, but, in my opinion, the use of the class hierarchy is justified here. First, subclasses not only represent special cases of the base class, the entire hierarchy is one concept - contact. Three contact implementations very accurately reflect the three cases stipulated by the business rule. Secondly, the relationship of the base class and its heirs, the division of responsibilities between them is easily traced. Thirdly, if the hierarchy really becomes a problem, you can separate the contact state into a separate hierarchy, as was done in the original example. In F #, inheritance of records is impossible, but new types are declared quite simply, so the splitting was performed immediately. In C #, a more natural solution would be to place the Name fields in the base class.
Create contact
Creating a contact is quite simple.
public abstract class Contact
{
public static Contact FromEmail(PersonalName name, string emailStr)
{
var email = new EmailAddress(emailStr);
var emailContactInfo = new EmailContactInfo(email, false);
return new EmailOnlyContact(name, emailContactInfo);
}
}
var name = new PersonalName("A", null, "Smith");
var contact = Contact.FromEmail(name, "abc@example.com");
If the email address is incorrect, this code will throw an exception, which can be considered an analogue of the return None
in the original example.
Contact update
Updating a contact is also straightforward - you just need to add an abstract method to the type Contact
.
public abstract class Contact
{
public abstract Contact UpdatePostalAddress(PostalContactInfo newPostalAddress);
}
public sealed class EmailOnlyContact : Contact
{
public override Contact UpdatePostalAddress(PostalContactInfo newPostalAddress)
=> new EmailAndPostContact(Name, email_, newPostalAddress);
}
public sealed class PostOnlyContact : Contact
{
public override Contact UpdatePostalAddress(PostalContactInfo newPostalAddress)
=> new PostOnlyContact(Name, newPostalAddress);
}
public sealed class EmailAndPostContact : Contact
{
public override Contact UpdatePostalAddress(PostalContactInfo newPostalAddress)
=> new EmailAndPostContact(Name, email_, newPostalAddress);
}
var state = new StateCode("CA");
var zip = new ZipCode("97210");
var newPostalAddress = new PostalAddress("123 Main", "", "Beverly Hills", state, zip);
var newPostalContactInfo = new PostalContactInfo(newPostalAddress, false);
var newContact = contact.UpdatePostalAddress(newPostalContactInfo);
As with option.Value in F #, throwing an exception from the constructors is possible if the email address, zip code, or state is incorrect, but for C # this is common practice. Of course, exception code must be provided in the working code here or somewhere in the calling code.
Handling contacts outside the hierarchy
It is logical to arrange the logic for updating the contact in the hierarchy itself Contact
. But what if you want to accomplish something that does not fit into her area of responsibility? Suppose you want to display contacts on the user interface.
You can, of course, add the abstract method to the base class again and continue to add a new method every time you need to process contacts somehow. But then the principle of sole responsibility will be violated , the hierarchy Contact
will be cluttered, and the processing logic will be spread between implementations Contact
and places responsible for, in fact, processing contacts. There was no such problem in F #, I would like the C # code to be no worse!
The closest equivalent to pattern matching in C # is the switch construct. We could add Contact
an enumerated type to the property, which would allow us to determine the actual type of contact and perform the conversion. It would also be possible to use the newer features of C # and perform switch by instance type Contact
. But we wanted the Contact
compiler to prompt itself when new correct states were added , where there is not enough processing for new cases, and switch does not guarantee the processing of all possible cases.
But OOP also has a more convenient mechanism for choosing logic depending on type, and we just used it when updating a contact. And since now the choice depends on the calling type, it must also be polymorphic. The solution is the Visitor template. It allows you to choose a handler depending on the implementation Contact
, unbinds the contact processing methods from their hierarchy, and if a new type of contact is added, and, accordingly, a new method in the Visitor’s interface, you will need to write it in all implementations of the interface. All requirements are met!
public abstract class Contact
{
public abstract void AcceptVisitor(IContactVisitor visitor);
}
public interface IContactVisitor
{
void Visit(PersonalName name, EmailContactInfo email);
void Visit(PersonalName name, PostalContactInfo post);
void Visit(PersonalName name, EmailContactInfo email, PostalContactInfo post);
}
public sealed class EmailOnlyContact : Contact
{
public override void AcceptVisitor(IContactVisitor visitor)
{
if (visitor == null)
{
throw new ArgumentNullException(nameof(visitor));
}
visitor.Visit(Name, email_);
}
}
public sealed class PostOnlyContact : Contact
{
public override void AcceptVisitor(IContactVisitor visitor)
{
if (visitor == null)
{
throw new ArgumentNullException(nameof(visitor));
}
visitor.Visit(Name, post_);
}
}
public sealed class EmailAndPostContact : Contact
{
public override void AcceptVisitor(IContactVisitor visitor)
{
if (visitor == null)
{
throw new ArgumentNullException(nameof(visitor));
}
visitor.Visit(Name, email_, post_);
}
}
Now you can write code to display contacts. For simplicity, I will use the console interface.
public sealed class ContactUi
{
private sealed class Visitor : IContactVisitor
{
void IContactVisitor.Visit(PersonalName name, EmailContactInfo email)
{
Console.WriteLine(name);
Console.WriteLine("* Email: {0}", email);
}
void IContactVisitor.Visit(PersonalName name, PostalContactInfo post)
{
Console.WriteLine(name);
Console.WriteLine("* Postal address: {0}", post);
}
void IContactVisitor.Visit(PersonalName name, EmailContactInfo email, PostalContactInfo post)
{
Console.WriteLine(name);
Console.WriteLine("* Email: {0}", email);
Console.WriteLine("* Postal address: {0}", post);
}
}
public void Display(Contact contact)
=> contact.AcceptVisitor(new Visitor());
}
var ui = new ContactUi();
ui.Display(newContact);
Further improvements
If it is Contact
declared in the library and the appearance of new heirs in the clients of the library is undesirable, then you can change the scope of the constructor Contact
to internal
, or even make its heirs nested with classes, declare the visibility of implementations and the constructor private
, and create instances through only static factory methods.
public abstract class Contact
{
private sealed class EmailOnlyContact : Contact
{
public EmailOnlyContact(PersonalName name, EmailContactInfo email)
: base(name)
{
}
}
private Contact(PersonalName name)
{
}
public static Contact EmailOnly(PersonalName name, EmailContactInfo email)
=> new EmailOnlyContact(name, email);
}
Thus, it is possible to reproduce the non-extensibility of the type-sum, although, as a rule, this is not required.
Conclusion
I hope I was able to show how to limit the correct state of business logic using types with OOP tools. The code turned out to be more voluminous than on F #. Somewhere this is due to the relative cumbersomeness of OOP decisions, somewhere due to the verbosity of the language, but solutions cannot be called impractical.
Interestingly, starting with a purely functional solution, we came up with the recommendation of subject-oriented programming and OOP patterns. In fact, this is not surprising, because the similarity of type-sums and the Visitor pattern has been known for quite some time . The purpose of this article was to show not so much a concrete trick as to demonstrate the applicability of ideas from the “ivory tower” in imperative programming. Of course, not everything can be transferred as easily, but with the advent of more and more functionalities in the mainstream programming languages, the boundaries of the applicable will expand.
→ Sample code is available on GitHub