The "avoid primitive obsession" version is more complex"
Be very careful about your definition of "complex". You seem to be judging it more by the class count than by the tight coupling that the original snippet contains.
Additional separation, when appropriate, breaks down dense complexity into pieces that are individually more easily digestible. The benefits of doing so is a tight balance. The individual classes become easier to digest; but the orchestration between those classes can become more complex. This kind of "complexity" is a balancing act that requires case-by-case evaluation.
However, that is not the main focus of the primitive obsession advice. The main focus is change-friendliness.
Postal code is a really good example here in my experience, as I come from a place where postal codes are made up of numeric digits. The allure was to store them as integers (because they are, in effect, a number).
But if your codebase tomorrow becomes an international product, which also gets deployed to locations that include non-numeric characters in its postal codes, then what?
Obviously, whether you had a dedicated class for PostalCode
or not, it wouldn't have helped you predict the future nor pre-emptively steer away from using an integer type to represent it.
However, what would be different between these snippets is that in the second snippet, the blast radius from having to change the implementation details of a postal code would've been contained to the PostalCode
class and not each and every class that might contain a postal code.
You don't see that benefit in a single example, because examples are (a) overly simplistic for the sake of simplicity and (b) static, i.e. they don't show you a living, breathing codebase that has to deal with requirement changes over time.
The snippets you've shown here aren't trying to tell you why you need to avoid primitive obsession, they're telling you how to do it. The why is explained to you in the text that surrounds the snippet, which you've omitted from your explanation of the problem you see before you.
There's another consideration here. Suppose you wanted to validate your postal codes. Where would you put that code? If UserData
, CompanyData
and ShopData
all had a postal code, would they all implement their own validations?
Or would you reuse the logic? Well, where would you put the reusable logic? The only remaining option would be some kind of helper class. The end result here is that you'd have separated definition of what is a postal code (as copy/pasted across the UserData
, CompanyData
and ShopData
classes) and the logic surrounding a postal code (as implemented by this helper class validation).
The better way is to combine these into the PostalCode
class, because they belong together.
But I only have one class with a postal code, i.e. UserData
. I don't reuse it across other classes. Also, I don't even do postal code validation.
This is me pre-empting a very common response to the above answer.
This is where we can get into a YAGNI discussion. I cannot argue your specific application's requirements, because I am not you or your product owner. I cannot know if you'll ever need to change anything w.r.t. postal codes.
What I will claim, however, is that it would be a very painful process to have to retroactively insert a PostalCode
in a codebase where it previously did not exist, because you have to rewrite all the old usages into the new ones.
Comparatively, it will be less effort to separate it into a PostalCode
class from the get to, even on the off chance that you "waste" this effort by never having to make changes to it in the future.
I'm not in the business of predicting the future and trying to definitely be right the first time. I'm in the business of building resilient and change-friendly codebases so that I can adapt my code in case the requirements have suddenly changed on me.
And in pursuit of that resilience and change-friendliness, I strongly recommend you err towards separating early instead of doing it retroactively.
As with all common sense advice, there is a line of reasonability, this should not become a dogmatic exercise of turtles all the way down.
typedef String ZipCode;