Primitive Obsession describes code in which the design relies too heavily on primitive types, rather than solution-specific abstractions. It often results in more verbose code with more duplication of logic, since logic cannot be embedded with the primitive types used.
Sponsor - DevIQ
Thanks to DevIQ for sponsoring this episode! Check out their list of available courses and how-to videos.
Show Notes / Transcript
Primitives refer to built-in types, like
string, etc. The primitive obsession code smell refers to overuse of primitive types to represent concepts that aren't a perfect fit, because the primitive supports values that don't make sense for the element they're representing. For example, it's not unusual to use a
string to represent a ZIP Code value or a Social Security Number. Many systems will use an
int to represent a value that cannot be negative, such as the number of items in a shopping basket. In such a case, if the system even bothers to enforce the invariant stating that shopping basket quantity must be positive, it must do so somewhere other than in the type representing the quantity. Ideally, the shopping basket or basket item type would enforce this, but again in many designs the shopping basket item quantity is simply a property that can be set to anything. In which case any service, UI call, etc. that manipulates a basket item would first need to ensure it was being set properly. This can result in a great deal of duplicate code, with the usual technical debt that arises when you violate the Don't Repeat Yourself principle. In some places, someone will forget to perform the checks, or they'll perform them differently, and bugs will creep in. Or the rules will be updated, but not everywhere, which results in the same inconsistent behavior. When you work with too primitive of an abstraction, you end up having to code around this deficiency every time you work with the type.
I've talked about encapsulation before - it's obviously an important concept in software design. By choosing to represent a concept with a primitive, you give up the ability to leverage encapsulation when working with this concept in your solution. The biggest problem with primitive obsession is that it results in a lot of behavior being added around the types in question, rather than encapsulated within them. Instead of having to check, probably in many places, that Quantity is positive or that a string represents a valid ZIP code, it's far better to create a type to represent the concept in question, along with its rules.
Such types should typically be immutable value objects that cannot be created in an invalid state (and thus need not be validated where they are passed in as parameters). It's useful to have easy ways to cast primitives to and from these value objects, but this should be done only at the edges of the application (user input/output, persistence). Try to use the value object as much as possible within your actual business logic or domain model, rather than a primitive representation of the type.
You can make working with your new type about as easy as working with the primitive it's replacing by making sure you override its
ToString method. You can also handle comparisons and equality, and configure implicit and explicit casting operators. Jimmy Bogard wrote an article about 10 years ago that describes how to do exactly this for a simple ZIP Code type in C# - there's a link in the show notes. Yes, you'll end up with a dozen or so lines of code in your ZIP Code class instead of just using a string, but any logic that relates to ZIP Codes will also live in this class, rather than being scattered throughout your application.
When you represent a concept in your system with a primitive type, you're asserting that the concept can be represented by any value that type can hold. If you expose method signatures that accept primitive values, the only clue you might offer to clients of that method could be the names of the parameters. Invalid values might no immediately be discovered, or if they are, the related errors might be buried within the behavior of the method, rather than immediately apparent. If instead you use a separate value object to represent a concept, a method that accepts parameters using this type will be much easier for clients to work with. If there are exceptions related to type conversion, they will be discovered immediately when the client attempts to create an instance of the value object, and this behavior will be consistent everywhere, unlike different methods that may or may not perform validity checks on their inputs.
You can learn more about the primitive obsession code smell and literally dozens of others, along with how to refactor them, in my Pluralsight course, Refactoring Fundamentals.