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:
- A primitive type (int, string, etc) representing a domain concept (megabytes, phone number, etc).
- 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:
- EmailAddress is a new type that is now a Whole Value for e-mails.
- 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:
- We've completely separated the concerns. UserSignup has no validation calls or implementations. It just uses an EmailAddress like it should.
- We've improved cohesion. E-maily things live on EmailAddress. UserSignupy things live on UserSignup. No overlap.
- 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!
Footnotes
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.
This is a great article. The team that I am currently on have a large number of classes providing primitive support. This article will be a big help in keeping the conversation alive on this topic that we have struggled with.
ReplyDeleteCool!
ReplyDeleteAnd if you're going down this route you should also explicitly implement equality so you can compare two instances like a pro. Resharper can generate all the overrides for you.
ReplyDeleteExcellent post! So much easier to follow your examples than understand what "reason to change" means. :-)
ReplyDeleteNo doubt its a good example of eliminating unnecessary dependancy, but at the same time it reminds me of an example of overcomplicated Hello World programme written in OOP (i think it was somewhere in wikipedia): you can use and abuse any principle (like SOLID) to make something better or worse. Here its clear that the first way of fixing the problem is not an optimal (more to that, one can argue that its still breaks single responsibility since it calls some validation function, so its not only signing up, but validates too). So here is important not to delve in sophistry too much and choose "whatever works best for you" :)
ReplyDelete