16

What are general guidelines or rules of thumb for when to use a domain-speciifc object vs a plain String or number?

Examples:

  • Age class vs Integer?
  • FirstName class vs String?
  • UniqueID vs String
  • PhoneNumber class vs String vs Long?
  • DomainName class vs String?

I think most OOP practitioners would definitely say specific classes for PhoneNumber and DomainName. The more rules around what makes them valid and how to compare them make simple classes easier and safer to deal with. But for the first three there is more debate.

I have never come across an "Age" class but one could argue it makes sense given it must be non-negative (okay I know you can argue for negative ages but it's a good example that it's almost equivalent to a primitive integer).

String is common to represent "First Name" but it's not perfect because an empty String is a valid String but not a valid name. Comparison would usually be done ignoring case. Sure there are methods to check for empty, do case-insensitive compare, etc but it requires the consumer to do this.

Does the answer depend on the environment? I am primarily concerned with enterprise/high-value software that will live and be maintained for possibly more than a decade.

Perhaps I'm overthinking this but I would really like to know if anyone has rules on when to choose class vs primitive.

9
  • 3
    Age class is not a good idea if it can be replaced by an integer. If it takes a date of birth in the constructor and has getCurrentAge () and getAgeAt (Date date) methods then the class has meaning. But it cannot be replaced by an integer.
    – kiwiron
    Oct 30, 2017 at 7:12
  • 5
    "A string is sometimes not a string. Model it accordingly.". There is a good post by Mark Seemann about 'primitive obsession': blog.ploeh.dk/2015/01/19/… Oct 30, 2017 at 7:26
  • 5
    Actually an Age class does makes some sense in a weird way. The common Western definition of Age is zero at birth and add 1 at your next birthday. The East Asian methodology is that you are 1 at birth and 1 is added at the next New Years day. Thus depending on your locale, your age can be calculated differently. EG from en.wikipedia.org/wiki/East_Asian_age_reckoning people may be one or two years older in Asian reckoning than in the western age system
    – Peter M
    Oct 30, 2017 at 11:26
  • 1
    "an empty String is a valid String but not a valid name" People absolutely do not need to have first names, or last names. Mononyms are an uncommon, but real, thing.
    – Deacon
    Oct 30, 2017 at 13:12
  • 6
    I see a lot of writing below this question, but the answer is not really that complicated. You use an Age class instead of an integer when you need the additional behavior that such a class would provide. Oct 30, 2017 at 14:38

7 Answers 7

21

What are general guidelines or rules of thumb for when to use a domain-speciifc object vs a plain String or number?

The general guideline is that you want to be modeling your domain in a domain specific language.

Consider: why do we use integer? We can represent all of the integers with strings just as easily. Or with bytes.

If we were programming in a domain agnostic language that included primitive types for integer and age, which would you choose?

What it really comes down to is the "primitive" nature of certain types is an accident of the choice of language for our implementation.

Numbers, in particular, usually require additional context. Age isn't just a number but it also has dimension (time), units (years?), rounding rules! Adding ages together makes sense in a way that adding an age to a money does not.

Making the types distinct allows us to model the differences between an unverified email address and a verified email address.

The accident of how these values are represented in memory is one of the least interesting parts. The domain model doesn't care of a CustomerId is an int, or a String, or a UUID/GUID, or a JSON node. It just wants the affordances.

Do we really care whether integers are big endian or little endian? Do we care if the List we have been passed is an abstraction over an array, or a graph? When we discover that double precision arithmetic is inefficient, and that we need to change to a floating point representation, should the domain model care?

Parnas, in 1972, wrote

We propose instead that one begins with a list of difficult design decisions or design decisions which are likely to change. Each module is then designed to hide such a decision from the others.

In a sense, the domain specific value types we introduce are modules that isolate our decision of what underlying representation of the data should be used.

So the upside is modularity - we get a design where it is easier to manage the scope of a change. The downside is cost - it's more work to create the bespoke types that you need, choosing the correct types requires acquiring a deeper understanding of the domain. The amount of work required to create the value module will depend on your local dialect of Blub.

Other terms in the equation might include expected lifetime of the solution (careful modeling for script ware that will be run once has lousy return on investment), how close the domain is to the core competency of the business.

One special case that we might consider is that of the communication across a boundary. We don't want to be in a situation where changes to one deployable unit require coordinated changes with other deployable units. So messages tend to be focused more on representations, without consideration of invariants or domain specific behaviors. We're not going to try to communicate "this value must be strictly positive" in the message format, but rather communicate its representation on the wire, and apply validation to that representation at the domain boundary.

1
  • Excellent point about hiding (or abstracting) those things that are difficult and likely to change.
    – user949300
    Oct 30, 2017 at 15:00
4

You're thinking about abstraction, and that's great. However, the things your listing seem to me to be mostly individual attributes of something larger (not all of the same thing, of course).

What I feel is more important is to provide abstraction that groups attributes together and gives them a proper home, such as a Person class, so that the client programmer doesn't have to deal with multiple attributes, but rather one higher level abstraction.

Once you have this abstraction you don't necessarily need additional abstractions for attributes that are just values. The Person class can hold a BirthDate as a (primitive) Date, along with PhoneNumbers as list of (primitive) strings, and a Name as a (primitive) string.

If you have a phone number with a formatting code, now that's two attributes, and would merit a class for phone number (as it would likely have some behavior as well, such as get numbers only vs. get formatted phone number).

If you have a Name as multiple attributes (e.g. prefixes/titles, first, last, suffix) that would merit a class (as now you have some behaviors such as getting the full name as a string vs. getting smaller pieces, title etc..).

1

You should model as you need, if you simply want some data you can use primitives types but if you want to make more operations on these data it should be modeled in specific classes, for example (I agree with @kiwiron in his comment) an Age class doesn't have sense, but it would be better to replace it with a Birthdate class and you put these methods there.

The benefit of modeling these concepts inside classes is that you enforcing the richness of your model, for example, instead you have a method which accepts any Date, a user can fill it with any date, WW II start date or Cold war end date, of these dates still a valid birthdate. you can replace it with Birthdate so in this case, you enforce your method to accepts a Birthdate not to accept any Date.

For Firstname I don't see much of benefit, UniqueID too, PhoneNumber a little, you can ask it to give you the country and region for example because in general PhoneNumber refers to countries and regions, some operators use predefined number Suffixes to classify Mobile, Office ... numbers, so you can add these methods to that class, same for DomainName.

For more details I recommend you to take a look at Value Objects in DDD (Domain Driven Design).

1

What it depends on is if you have behavior (that you need to maintain and/or change) associated with that data or not.

In short, if it's just a piece of data that gets tossed around without much behavior associated with it, use a primitive type or, for somewhat more complex pieces of data, a (largely behaviorless) data structure (e.g. a class with only getters and setters).

If, on the other hand, you find that, in order to make decisions, you are checking or manipulating this data at various places in your code, places often not close to each other - then turn it into a proper object by defining a class and putting the relevant behavior inside of it as methods. If for some reason you feel it doesn't make sense to define such a class, an alternative is to consolidate this behavior into some sort of a "service" class that operates on this data.

1

Think about what you get from using a class vs. a primative. If using a class can get you

  • validation
  • formatting
  • other useful methods
  • type safety (ie. with a PhoneNumber class, it should be impossible for me to assign it to a string or a random string (ie. "cucumber") where I expect a phone number)
  • any other benefits

and those benefits make your life easier, improve code quality, readability and outweigh the pain of using such things, do it. Otherwise, stick with the primatives. If it's close, make a judgement call.

Also realize that sometimes good naming can just handle it (ie. a string named firstName vs. making a whole class that just wraps a string property). Classes aren't the only way to solve things.

1

Your examples look a lot more like properties or attributes of something else. What that means is that it would be perfectly reasonable to start with just the primitive. At some point you will need to use a primitive, so I usually save complexity for when I actually need it.

For example, let's say I'm making a game and I don't have to worry about aging characters. Using an integer for that makes perfect sense. The age could be used in simple checks to see if the character is allowed to do something or not.

As others pointed out, age can be a complicated subject depending on your locale. If you have a need to reconcile ages in different locales, etc. then it makes perfect sense to introduce an Age class that has DateOfBirth and Locale as properties. That Age class can also calculate current age at any given timestamp as well.

So there are reasons to promote a primitive to a class, but they are very application specific. Here are some examples:

  • You have special validation logic that has to be applied everywhere the type of information is used (e.g. phone numbers, email addresses).
  • You have special formatting logic that is used to render for humans to read (e.g. IP4 and IP6 addresses)
  • You have a set of functions that all relate to that object type
  • You have special rules for comparing similar value types

Basically, if you have a bunch of rules for how a value is to be treated that you want to be used consistently throughout your project, create a class. If you can live with simple values because it's not really critical to functionality, use a primitive.

1

At the end of the day, an object is just a witness that some process actually got ran.

A primitive int means that some process zeroed out 4 bytes of memory, and then flipped the bits necessary to represent some integer in the range of -2,147,483,648 to 2,147,483,647; which is not a strong statement about the concept of age. Likewise, a (non-null) System.String means that some bytes were allocated and bits were flipped to represent a sequence of Unicode characters with respect to some encoding.

So, when deciding how to represent your domain object, you should think about the process for which it serves as a witness. If a FirstName really should be a non-empty string, then it should stand as a witness that the system ran some process that ensures that at least one non-whitespace character was created in a sequence of characters that were handed to you.

Similarly, an Age object should probably be a witness that some process computed the difference between two dates. Since ints are generally not the result of computing the difference between two dates, they are ipso facto insufficient for representing ages.

So, it's pretty obvious that you almost always want to create a class (which is just a program) for each domain object. So what's the problem? The problem is that C# (which I love) and Java demand too much ceremony in creating classes. But, we can imagine alternate syntaxes that would make defining domain objects much more straightforward:

//hypothetical syntax
class Age = |start:date - end:date|.Years

(* ML-like syntax *)
type Age(x, y) = datediff(year, x, y)

//C#-like syntax with primary constructors and expression-bodied classes
class Age(DateTime x, DateTime y) => implicit operator int (Age a) => Abs((y - x).Years);

F#, for instance, actually does have a nice syntax for this in the form of Active Patterns:

//Impossible to produce an `Age` without first computing the difference of two dates
//But, any pair (tuple) of dates is implicitly converted to an `Age` when needed.

let (|Age|) (x, y) =  (date_diff y x).Days / 365

The point is that just because your programming language makes it cumbersome to define domain objects doesn't mean that it actually is ever a bad idea to do so.


†We also call these things post conditions, but I prefer the term "witness" as it serves as a proof that some process can and did run.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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