Home .NET Designing with types:How to make invalid states unexpressible in C#

Designing with types:How to make invalid states unexpressible in C#

by admin

Typically, articles about designing with types contain examples in functional languages like Haskell, F#, and others.It may seem that this concept is not applicable to object-oriented languages, but it’s not.

In this article I will translate examples from the article Scott Wlaschin Designing with types : How to make uncorrected states unexpressible at 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 have to port the types from previous article in the series which are used in the examples in F#.

Wrap primitive types into domain types

The F# examples use domain types instead of primitives for email address, US postal code, and state code. Let’s try to make a primitive type wrapper 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 EmailAddressotherEmailAddress 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 this implementation is more typical for C#. I also had to implement comparison and conversion to string, which in F# would have been done by the compiler.

On the one hand, the implementation looks rather cumbersome. On the other hand, the specificity of the email address is only expressed here by the constructor checks and possibly comparison logic. Most of the code here is infrastructure code, which, moreover, is unlikely to change. So we can either do template , or, at the very least, copy the generic code from class to class.

It should be noted that creating domain types from primitive values is not a feature of functional programming. On the contrary, the use of primitive types is considered a feature of of bad code in OOP. Examples of such wrappers can be seen at e.g, in NLog and In NBitcoin , and the standard TimeSpan type is essentially a wrapper over the number of ticks.

Create value objects

Now we need to create an analogue of entries :

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 at the expense of refactoring in the IDE

As well as EmailAddress , EmailContactInfo – this object-value (meaning DDD rather than type-values in .NET ), long known and used in object modeling.

The other types are StateCode , ZipCode , PostalAddress and PersonalName are ported to C# in a similar way.

Create contact

So, the code must express the rule "Contactmust contain an email address or a mailing address (or both)". It is required to express this rule in such a way that the correctness of the state can be seen from the type definition and checked by the compiler.

Expressing different states of contact

So a contact is an object that contains a person’s name and either an email address, a mailing address, or both. Obviously, one class cannot contain three different sets of properties, hence three different classes must be defined. All three classes must contain the contact’s name and it must be possible to handle contacts of different types in a uniform way, without knowing which addresses the contact contains. Therefore, a contact will be represented by an abstract base class containing the contact name 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 might argue that you should use the composition , not inheritance, and in general you should inherit behavior, not data. The remarks are fair, but in my opinion, the use of class hierarchy is justified here. Firstly, the subclasses don’t just represent special cases of the base class, the whole hierarchy represents one concept – contact. The three contact implementations very accurately represent the three cases specified by the business rule. Second, the relationship between the base class and its successors, the division of responsibilities between them, is easy to trace. Third, if the hierarchy becomes a real problem, we can separate the contact state into a separate hierarchy, as was done in the original example. In F# it is impossible to inherit records, but it is rather easy to declare new types, so the separation was done immediately. In C# it is more natural to put Name fields in a base class.

Making contact

Creating a contact is quite simple.

public abstract class Contact{public static ContactFromEmail(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 e-mail address is invalid, this code will throw an exception, which can be considered analogous to the return None in the original example.

Contact update

Updating a contact is no problem either – you just have 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#, it is possible to throw an exception from constructors here if the email address, zip code, or state is specified incorrectly, but for C# this is common practice. Of course, there should be exception handling in the working code here or somewhere in the calling code.

Processing contacts outside the hierarchy

It makes sense to place the contact update logic in the hierarchy itself Contact But what if you need to do something that doesn’t fit into its area of responsibility? Suppose you want to display contacts on the user interface.

You could, of course, add an abstract method to the base class again and keep adding a new method every time you need something else to process the contacts. But then the the principle of sole responsibility , the hierarchy Contact will be cluttered, and the processing logic will be smeared between the implementations of Contact and the places responsible for the actual contact processing. We didn’t have this problem in F#, I wish the C# code was just as good!

The closest analog to pattern matching in C# is the switch construct. You could add in Contact an enumerated type property that would allow you to define the actual contact type and perform the conversion. You could also use the newer features of C# and perform a switch on an instance type Contact But after all, we wanted to make sure that when adding new correct states Contact compiler would tell us where the new cases are missing, and switch does not guarantee that all possible cases will be handled.

But the OOP has a more convenient mechanism for selecting logic depending on type, and we just used that when updating a contact. And since the choice now depends on the calling type as well, it needs to be polymorphic as well. The solution is the Visitor pattern. It allows you to select an implementation-specific handler Contact , detaches contact handling methods from their hierarchy, and if a new contact type is added, and therefore a new method in the Visitor interface, it will need to be written in all interface implementations. All requirements 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 the code to display the contacts. For simplicity, I will use the console interface.

public sealed class ContactUi{privatesealed 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 Contact is declared in the library and the appearance of new heirs in the library’s clients is undesirable, then you can change the scope of the constructor Contact to internal or make its descendants nested classes at all, declare visibility of implementations and constructor private and make creation of instances through static methods-factory only.

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);}

In this way you can reproduce type-sum unexpandability, although this is not usually required.

Conclusion

I hope I was able to show how to use OOP tools to limit the correct states of business logic by using types. The code is more voluminous than in F#. Somewhere it is due to relative unwieldiness of OOP solutions, somewhere it is due to verbose language, but solutions can not be called impractical.

What’s interesting is that starting with a purely functional solution, we’ve come to follow the recommendations of object-oriented programming and OOP patterns. In fact, this is not surprising because the similarity of type-sums and pattern Visitor known , and quite a long time ago The purpose of this article was not so much to show a particular technique as to demonstrate the applicability of ideas from "ivory tower" In imperative programming. Of course, not everything will transfer as easily, but as more and more features appear in mainstream programming languages, the boundaries of what is applicable will be extended.


→ Example codes are available at GitHub

You may also like