🏋️ The Might of Microtypes

🏋️ The Might of Microtypes

Ints, Doubles, Strings. We work with primitives all the time, but did you know they can hurt your Software Design? Microtypes can help!

🚩 Primitives Turn Problematic

Primitives are the basic building blocks of the languages we work with, and the programs we write. The truth is, we tend to rely on them too much.

Overusing primitives can lead to problems. Thankfully, with a little elbow grease we can leverage primitives in a much better way, by creating so-called Microtypes.

First, let’s see which problems might arise from fixating on primitives.

🦨 Overusing Primitives a Code Smell?

Overusing primitives is a code smell, called Primitive Obsession. A tell for this code smell is operating upon primitives extensively, when the primitive represents a significant concept.

Consequences of this code smell can be bugs, duplication, low cohesion and an implicit model. I’ll show examples of these problems later in this post.

One easily overlooked way to solve this smell is by defining a Microtype.

🔬 Introducing the Microtype

Microtypes are custom classes which wrap a single primitive, and are therefore very small (hence the name). They are also commonly referred to as Value Classes or Value Objects.

Being Value Objects, the identity of a Microtype is defined entirely by their attribute. Furthermore, Microtypes should be immutable and its equality should be based on it’s attribute, not its reference.

Let’s set the scene before diving into some examples.

☕ Example: Coffee Roasters

Imagine that your writing a piece of software for a coffee roaster, and the model includes Coffee. It represents a bag of coffee beans that will be sold to a customer.

It needs to contain information about the type of bean used, the intensity (1-5) and the weight of the bag (in grams). A first take might look like this:

Listing 1. First iteration for Coffee

This does get the job done, but reeks of Primitive Obsession. Next, we’ll look at how Microtypes help out.

🪲 Bug Prevention

Using primitives all over the place can become a way for subtle, semantic bugs to creep into the code. For instance if we look at Coffee, both intensity and weight are represented using an Int.

It’s all too easy for clients to mix up intensity and weight. The compiler won’t catch this issue because it cannot distinguish them by semantics alone.

Listing 2. Switching intensity and weight causes a bug

Running the code above results in:

1
Coffee(type=ARABICA, intensity=1000, weight=2)

Ouch, that is some very strong coffee. Not quite what we intended!

By defining a Microtype for weight, the compiler will yell at us if we mix up the concepts. How’s that for early feedback!

Listing 3. Preventing semantic bugs with a Microtype

📍 Combat Duplication

There are business rules concerning the weight of coffee. For instance, it cannot be a negative number, and we need to convert the weight to kilograms in order to determine the price for the bag of coffee beans.

Without a dedicated type, this logic might pop up anywhere in the code where the it is needed:

Listing 4. Duplication because weight logic doesn’t have a “home”

When we define a Microtype for weight, it is the logical place to define this validation. The duplication is gone, and any instances of Weight we use are validated. This makes the code more maintainable!

Listing 5. The Microtype contains the logic, removing duplication

📦 Improving Cohesion

The duplication problem also closely relates to a cohesion problem. Without a specific type logic regarding weight can pop up all over the place.

The Coffee class might validate that weight is not a negative number. Some service orchestrating a use case might do some conversion of weight from grams to kilograms. Logic becomes disparate, lowering cohesion in other classes… Bad.

As we just saw, a dedicated Microtype is a natural “home” to place these business rules (and future ones, too). It itself has high cohesion, and improves cohesion of other classes by relieving them of containing weight logic.

Listing 6. Microtype containing weight related logic

🗣️ Expressing the Domain Model

Expressing the concepts of weight and intensity as primitives deprives our model of depth.

One of the tenets of Domain-Driven Design is that your code should capture and express the Ubiquitous Language. If we express these concepts as mere primitives, it can end up making the model implicit.

We want the opposite: an expressive model. Just compare the primitive (listing 7) to the Microtype versions (listing 8).

Listing 7. Primitives might result in an implicit model

Listing 8. Microtypes make the model expressive and explicit

In the primitive version you’re left making assumptions on what represents weight. Is it grams? Kilograms? Pounds? With the Microtype, you instantly see that weight is measured in grams. It captures knowledge about the domain in code!

⚖️ Microtypes: a Silver Bullet?

So, should you go and wrap every single primitive you find?

That’s probably a bad idea.

Primitives are heavily optimized, and using objects everywhere might cause a serious performance hit. This is especially true with high volume use cases.

Some languages (like Kotlin) offer solutions to this problem in the form of a Value Class. This allows you to use a custom type at compile-time, and make use of the primitive at runtime.

Another downside is that Microtypes can be clunky to work with. They have to be constructed, and using them requires accessing their attribute, or defining custom methods (like plus, minus, etc).

Though the extra code problem is hard to solve, some languages help make it read nicely by allowing you to define custom operators. For Weight that might look like this:

Listing 9. Operator overloading

Strategically using extension functions can work miracles too. For instance you could make Weight construction easier with an extension on Int:

Listing 10. Extension functions to construct Weights

My advice is to use Microtypes diligently. Use them where they make your code more expressive and maintainable. You get most of your money’s worth if they capture a domain concept.

📝 Wrapping Up

Primitives are essential in creating software. Can you imagine writing programs without Ints, Strings or Booleans? They are generic programming constructs however, and that comes with problems.

A solution to those problems are Microtypes: small classes that wrap a primitive and give it meaning and purpose in a certain context. Though they have their own pro’s and cons, they can improve your software when used correctly.

Think twice next time you express a concept with a primitive, a Microtype might serve you better!

What’s your take on Microtypes? Do you see other pro’s or cons? Share your thoughts in the comments below!