5

According to When is primitive obsession not a code smell? and answer in https://softwareengineering.stackexchange.com/a/365205, I know one of the advantages of avoiding primitive obsession is "make the domain model more explicit".

However, I'm not opposing this point, what I don't know is, what is the meaning of "make the domain model more explicit"? Because I think "avoid primitive obsession" seems making the domain model more implicit: it encapsulates what the actual thing is, for example:

Before "avoid primitive obsession":

public class UserData{
    private String zipCode;
}

After "avoid primitive obsession":

public class UserData{
    private ZipCode zipCode;
}

public class ZipCode{
    private String value;
}

The "avoid primitive obsession" has one more class than the original one, and when I want to know more about zipCode, I need to yo-yo to the ZipCode class source code, instead of just browsing UserData directly.

The "avoid primitive obsession" version is more complex : it adds a layer of abstraction, making the program less straightforward for me, the "learning curve" to understand the program is steeper. Also it makes the UML diagram more complex because it has more class units in the diagram.

While I agree there are tons of reasons that I need to remove primitive obsession and there are other advantages of "avoid primitive obsession" (eg: ensure the object is valid later) outweighs the disadvantage of making the program more complex, it doesn't mean "making the program more complex" itself is an advantage. So I think "avoid primitive obsession" is actually making the domain model more implicit because it makes the program need more time to understand.

So my question is, why does "remove primitive obsession" make the domain model more explicit?

And also I don't know why would be better to talk to the business analyst about ZipCode instead of "string with ZipCode", because I think the latter one is simpler and more concrete, which is more understandable, especially for the business analyst that have less IT background.

4
  • 6
    '"string with ZipCode" [...] is simpler and [...] more understandable, especially for the business analyst that have less IT background.' - you do know that in normal conversation (as well as in pretty much every other field) the word "string" does not mean "text"? Why would "string with zip code" be more understandable than "zip code"? That's like saying "here's a 25% linen and 75% cotton piece of paper with $10 drawn on it" instead of "here's $10". Who cares what the paper is made of, or that the zip code is specifically represented as a string? It's your job to care about that, not theirs. Commented Aug 5 at 17:02
  • 1
    You're right that the second version introduces pointless complexity (although it might become worth it if the ZipCode class did some validation to justify the overhead and extra code), but you're overlooking the third option: typedef String ZipCode;
    – Ray
    Commented Aug 6 at 17:42
  • Your example is rather incomplete. You haven't shown any of the code that is using the zip code, and in the second snippet you could just drop the private value if there's no method to access it.
    – Bergi
    Commented Aug 7 at 2:56
  • Being a pedant I have to point out that in the US a Zip code is not a string, but instead is a 5 digits number, with an optional 4 digit suffix Zip+4
    – Peter M
    Commented Aug 8 at 18:18

11 Answers 11

15

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.

2
  • 3
    90% of the time "more complex" means nothing except "not what I like". It's a judgement of the speaker to slander a solution, and the simpler solutions often are far more difficult in other ways. Object style Strings too complex for you? How about byte arrays! You only need to handle encodings yourself, which is simple, as long as you only permit ASCII, which of course, someone will eventually not do. (to provide a semi-silly example)
    – Edwin Buck
    Commented Aug 6 at 16:25
  • 1
    @EdwinBuck: The fact that strings themselves are already a wrapper around a more rudimentary implementation is a really good point to make.
    – Flater
    Commented Aug 7 at 0:27
13

String is not a real-world entity, even if it seems like it is. Computer Strings have different properties from the strings that writers, accountants, or even logicians work with. Declaring a String makes the computer implementation more obvious, but not the domain model.

The domain contains entities that a domain expert who is not a programmer would recognize. People in general know what a zip code is, but it would be hard to explain to them what a String is (e.g. the fact that it can contain only characters from a restricted set, that it has length limitations, etc.)

Calling something a ZipCode tells the program reader both what it probably is under the hood (i.e. some sort of structure that at least contains a String) and what it's for (for printing into an address or sorting records) and what it isn't for (e.g. concatenating or reversing). A well-defined ZipCode will express this by not even allowing such operations, in fact it will probably only have an equals() and a toString() method, and a ZipCode() constructor that allows only valid zip code formats to be represented. It is good that a ZipCode class can do less than an unrestricted String can do.

Having a zip field of class ZipCode is better than having a e.g. a code field of type String because it makes reasoning about the class easier and misusing it harder by making it more explicit what this data item is supposed to do. And we have to do this in domain terms because it is easier to teach programmers the rudiments of the address domain than to teach accountants and secretaries the weird limitations of computer data types.

2
  • 2
    Regards strings, non-technical people might be mildly surprised about what a string is in terms of stored data, but the real curveball is about how computers process strings very differently from the human brain. For example, a British postcode like AB1 2CD, most non-technical people would be very surprised that this is usually processed by a computer as a different postcode than AB1 2CD.
    – Steve
    Commented Aug 5 at 12:14
  • 1
    @Steve You're right, I was just quickly grabbing some differences that occurred to me, but really Strings are so weird, they almost deserve one of those "Falsehoods that programmers believe about X" posts. Commented Aug 5 at 12:26
3

First off, it should be fairly clear that in order to avoid "primitive obsession" you will have to create additional abstractions, so the new design will have more classes than the original.

The new design is more explicit because when looking at a single detail of the design, you are now talking about a Zip Code rather than a string that represents a zip code. The latter relies on the name of the member variable, and the use of that variable (spread across everywhere in the code that it is relevant) to ensure some sort of contract on the contents. This is not robust - many things can be contained in strings that are not valid zipcodes, and even the string type itself could be ambiguous - e.g. in certain languages the encoding of that string is implicit, which could cause strange issues depending on the input encoding of where the software is used. In fact, the entire interpretation of the primitive string ZipCode is implicit. The only way you can find out what is the right way to encode or interpret that string is to look at the code that uses it, or maybe the documentation if it is complete, up-to-date and without error.

The "learning curve" for the whole program might seem steeper, or at least more diffuse, but locally it's actually easier. If all I want to do is understand UserData, then seeing a ZipCode class (which also appears in the domain model) tells me enough. I don't necessarily need to know the implementation details of this sub-object to fully understand UserData, and I don't have to worry about how exactly the primitive is meant to encode the domain concept.

1

The domain model here can hold a zip code in a string. Unfortunately, it can hold a name, a birthday present, and a breed of dog in the string meant to hold the zip code.

They're asking for a ZipCode object that wraps a primitive and only holds zip codes. It's not about 100% getting rid of primitives, it's about getting rid of "primitives in your model". In other words, he wants the primitives encapsulated, to protect the model from someone putting their favorite kind of cake into the zip code field.

1

Making the domain model more expressive is achieved by facilitating to connect the dots.

If every zipCode property would be named zipCode, the benefit would not necessarily be obvious. In real world, however, complexity looks different. It is not unusual to have the same kind of information with different names. For example an Employee entity having a zipCode and a Customer entity, developed by someone else, having a postalCode.

Creating a ZipCode type allows to connect the dots. But it is in reality still too primitive because a zip code has its rules and meaning defined by country. An US zip code does not have the same structure than a UK or a Belgian zip code. So depending on the domain the ZipCode would emerge to be a more complex Location object that ties together a city, a zip code and a country code. So, identifying domain value objects or entities where primitives were used as shortcut, not only allows to connect the dots, but also facilitates better identifying of domain concepts that would otherwise remain unnoticed.

Another typical example are numbers. Some numbers represent financial amounts (which could be bound to a currency) and some other with physical qualtities. Again using Money and Quantity as types make it more explicit than Double and Double. Introducing Quantity (with or without unit, or more specific like weight, length, surface or count) helps to make more explicit what it's about.

But like every recommendation, care must be taken to keep the balance. The example with dates is very typical. Is the overhead of having separate types like DateOfBirth, DateOfDeath, DateOfDelivery really worth it? The optimal borderline remains fuzzy.

0

In the end, you need primitives. You can have a class “ZipCode”. Somehow that class will have to store the actual zip code. Can’t store it as a “zipcode” instance. You could store it as a “ZipCode string” which uses what? A string? Not an integer likely. So at some point there is a primitive.

Your decision is at which point exactly you use a primitive. If you have an address object, and code outside the implementation of “address” doesn’t use zip codes, it doesn’t matter how exactly it is implemented. If you use ZipCodes all over the place, you might make it a more abstract class. Or change your design not to use ZipCodes individually but only as parts of addresses.

0

To make something more explicit is to more clearly define the meaning. If someone says that my mother is a real deal dog, perhaps they mean my mother is ugly. If they say I am a son of a bitch, I can infer they mean I am a SOB. If they say I steal others credit, don’t do my fair share of work while at the same time being verbally abusive, they are no longer talking about my mother’s beauty or lack there of, they aren’t talking about my mother at all. They are explicitly describing things I do that they don’t like.

And so, I no longer need to wonder if floppy ears is what makes people dislike me.

To make the domain model more explicit means to add code that more clearly defines what properties and behavior does and does not exist.

It’s not OOP, but C#’s addition of nullable reference types, greatly expanded its users ability to make the domain model more explicit.

You are of course right, that it adds more code and more complexity, and may take longer to understand. Whether that is worth it in any particular instance is something of a judgement call, but none of those factors make it implicit.

0

ZipCode is not the best example, but by having seperate class would allow the correct editor to be automatically used by some UI frameworks etc. (The rules for a zipcode being valid also depends on the county thats in the address.)

Consider CustomerID that is stored as an integer, by using a separate type you can automatically detect errors when for example you mix up a CustomerId and an InvoiceID.

0

Fundamentally, UserData contains a zip code, not a disembodied "string". The role of the string is made explicit. This is the way in which the version that uses a custom type is more explicit.

If when reading that UserData contains a ZipCode, you feel that critical information is being hidden from you, are you sure you're focusing on the right thing?

You've just learned that there is a type called ZipCode, do you have any reason to bother looking at the implementation of ZipCode? Would you be surprised in any way to learn that ZipCode is a string? Does that matter in any way? Unless you don't intuitively understand what a ZipCode is, there is no need to look at how it is implemented.

This is how using custom type both increases the level of abstraction and simultaneously makes the architecture more explicit.

Also, the second version with the custom type is arguably less complex. Why? Precisely for the reason outlined above. ZipCode being a custom type allows it to hide implementation details that are irrelevant to a reader trying to understand what UserData is. Using custom types allows the code to be separated into components that can be understood individually with a reasonable level of confidence.

The complexity of a piece of code is the difficulty of the reader to understand it. And that difficulty increases exponentially with the size of each tightly-coupled isolated unit of logic. If the UserData has a string as a zipCode then UserData is more complex than UserData containing a custom ZipCode type, because in the first case the logic governing the fact that the field is a zip code will be mixed with the rest of the UserData logic. To decrease complexity, the overall code should be split into reasonably sized, loosely coupled and most importantly well-named abstractions. An abstraction can be said to truly work well when a reader that doesn't know about it reads the name of the abstraction and goes "ok that makes sense" and doesn't look at its implementation.

In theory (I know if you want it to work in absolutely every real world case this is not quite true), a ZipCode is a great abstraction because the meaning should be obvious to almost every developer and few will ever feel the need to look into its implementation.

0

Just to add some discussion from a not so much a fan of primitive wraping...

The actual answer depends heavily on the language/framework you use. Mine is for c#...

"In other words, he wants the primitives encapsulated, to protect the model from someone putting their favorite kind of cake into the zip code field." -> and then a dev adds implicit conversion op on ZipCode class :) At some point the conversion is imminent anyway. So its not fun at all.

In my opinion given c# tooling, if you have a genuinely primitive value (iban, money, email, etc.), use a common validation rule (in my case attribute on prop), not a dedicated class/struct. Business intent is clear enough by a prop name. Rule is in one place/class. And you avoid overdesign and unneccessary complexity. In a way its also a way of primitive encapsulation but way less intrusive/complex.

Zip is a bad example as it almost never goes alone. Its inherently part of an address concept/struct. Iban is a best example as far as im concerned.

As of the Money class/struct, i dont find it helpfull at all. If its only a decimal value, see above. If you try add currency/round order to the mix, you end up in a nightmare when the currency and round order is distributed in the domain model. E.g. unit price in an invoice item shall have the same currency as the invoice itself; while rounding order is arbitrary defined by the type of invoice item (one round order for certain goods, another for others...). Plus money (class) is extensively used in formulas, without implicit op its nightmare, with its pointless...

-1

Going back from propaganda to the concrete question of what exactly does "make the domain model more explicit" mean.

The interface below is implicit - you have to guess the formats of input and output.


   // implicit types - just semantics over primitives:
   interface EstimatedAreaService {
      float[][] calculatePolygon(String zipCode);
   }

A model with explicit types, instead of implicit, allows us to describe inputs and outputs in an explicit way:


   // explicit types
   interface EstimatedAreaService {
      Polygon calculatePolygon(ZipCode in);
   }

In many cases the client code never needs to unpack the explicit domain value, because it's passed around and only unpacked in the last necessary moment, by lower level services:


Point point = sensorReading.getPoint();
Radius radius = radius.of(500, Unit.Meter)
List<ZipCode> possibleZipCodes = zipService.findZips(point, radius);
List<Address> allAddresses = addressService.findAddresses(possibleZipCodes);

allAddesses.forEach( address -> postalApi.sendMail(address, "An anaconda was sighted near your house, be sure to take precautions and have a nice day") );

Not the answer you're looking for? Browse other questions tagged or ask your own question.