Tuesday, April 14, 2015

Dependency Elimination Example: Primitive Support

Last post, I showed a canonical example for the Dependency Elimination Principle. I want to show another Dependency Elimination example that solves another common pattern I see. I've given this pattern a name: Primitive Support.

Introducing: Primitive Support

Primitive Support is the natural outworking of the Code Smell called Primitive Obsession. Our code suffers from Primitive Support when it has these two parts:

  1. A primitive type (int, string, etc) representing a domain concept (megabytes, phone number, etc).
  2. A helper class that does something meaningful with that primitive (e.g. MegabyteConverter or PhoneNumberParser or FooHelper).

If we have classes named similar to MegabyteConverter or PhoneNumberParser or FooHelper, we might be suffering from Primitive Support. Now let's look at an example.

A Simple Example: E-mail Address Validation

Let's say we're working on a user signup experience. A valid e-mail address is required for new users. We need our API to enforce that the e-mail address is valid. One way we might do this is like the below1:

This is actually pretty good as is. It's relatively easy to test and easy to read. There is, however, a lack of cohesion. First, UserSignup is worrying about validating e-mails when it really should just sign up the user. Second, email validation is off on its own in a general EmailHelper class. We will improve this soon, but first we'll take a detour in the wrong direction.

A Slight Rant

Unfortunately, some of the SOLID and TDD literature will lead you away from this design towards a more complicated design. In the name of separating concerns (good), standard practice will tell you to extract e-mail validation to its own concern and pass it as an interface to UserSignup (less good). Similar to this:

Now we have an IEmailValidator and an EmailValidator to separate the validation concern. To test UserSignup, we'll mock IEmailValidator and make sure it's called with the given emailAddress. Standard unit testing practice.

Testable, But Complicated

Let's evaluate the code. It's testable. We've separated our concerns. But it's more complicated. And we still have a cohesion problem: Email address validation is still a concern of UserSignup, and the validation implementation lives in its own EmailValidator world. We can do better than this.

Address the Root Cause: Kill the Primitive

The fundamental problem is that our system cares about e-mail addresses, but it doesn't represent e-mail addresses as first class citizens. We've split the e-mail address concern across UserSignup and EmailValidator. To fix this cohesion problem, we need to stop using string for e-mail addresses and start using EmailAddress for e-mail addresses. Watch how the complexity melts away:

First, note two important pieces:

  1. EmailAddress is a new type that is now a Whole Value for e-mails.
  2. The EmailAddress constructor throws when it is invalid. This makes it impossible to create an invalid e-mail address.

Now, note the supreme wins we get from this approach:

  1. We've completely separated the concerns. UserSignup has no validation calls or implementations. It just uses an EmailAddress like it should.
  2. We've improved cohesion. E-maily things live on EmailAddress. UserSignupy things live on UserSignup. No overlap.
  3. EmailAddress validation is now a compile-time concern. You can't make a bad one, so type-checking will guarantee that the thing you pass in is valid2.

What We Learned

Code with Primitive Obsession will usually lead to Primitive Support: introducing dependencies to do meaningful things on primitives. We can eliminate these dependencies by replacing the primitive with a Whole Value for the concept it represents. The supporting dependencies go away, the code is more clear, and the compiler catches bugs for us. Wins all around!


1Note: you might do this more simply via your library/framework validation system, but I'm leaving those out of this conversation for educational purposes.

2I know, I know we can still technically pass null. But if null makes it this far into our system we have bigger problems.