This is how two simple changes can improve your code’s maintainability

Denis Kranjcec

Ever opened code that feels like ancient hieroglyphics? You’ve likely encountered primitive obsession or premature generalization. But don’t worry - there’s a way out!

I often see code with readability and maintainability issues caused by primitive obsessions and premature generalizations. Both problems have been well-known for a long time, well-described, easy to understand, and easy to avoid.

The key is to use domain-specific types and generalizations thoughtfully, applying them only when they genuinely simplify the current requirements. Doing so makes your code not just cleaner, but also much easier to read and maintain.

How do we avoid primitive obsession?

Primitive obsession occurs when a software engineer uses built-in primitives and classes instead of custom, domain-specific types. This problem is described in many presentations, blogs, or books, such as in Chapter 65, “Prefer Domain-Specific Types to Primitive Types, by Einar Landrein thebook 97 Things Every Programmer Should Know.

The most commonly used types in Java applications, from my experience, are String, int, and List, where String creates the most problems. These types, along with other primitives and built-in classes, are certainly needed, but they should primarily be used as implementation details, not as domain-specific types.

For example, when an int is used to represent a package weight, it’s not clear which unit of measurement is being used – grams, kilograms, tons, pounds, or what the accepted values are. 

Similarly, when a String is used for a color, it’s not clear what values are supported – an RGB hex value, CMYK, “red”, “yellow”, “black”, some locale-specific values, etc. Joshua Bloch describes the problems with String overuse in Item 62: Avoid strings where other types are more appropriate in the Effective Java, Third Edition.

Lists are often used as a simple collection of Objects, and all related business rules are scattered throughout the application. A common problem is that a List can be updated anywhere without any business rules. And that’s just a sample of the readability and maintainability issues when using primitives.

When domain-specific classes are used:

  • Semantics become clearer because we are thinking in domain-related terms, not Strings or ints.
  • All business rules have an obvious place for implementation – the domain-specific classes. Code and idea duplication can be avoided.
  • Only allowed functionality and methods are exposed to users, making the API easier to understand and use.
  • Implementation details can be hidden from users, making the code less coupled and easier to change.
  • Methods are easier to understand, and the compiler will help with using correct arguments – e.g., there are no more methods with many Strings and ints as arguments where it’s easy to replace the order – someMethod(String a, String b, String c, int x, int y, int z)

Don’t create a general solution for a specific problem

Premature generalization happens when we implement generalizations in a code for possible and unclear features that are not required for current and known tasks. Dan North explained that in an excellent blog, “Best Simple System for Now“.

Most of us are working on applications that need to solve some specific problem. So, we should implement only those particular features that we understand well enough to implement. Too often, I see code that has (half-)support for enabling future and unknown features, which makes implementation much more complex than needed for the initial and specific requirements. Those future features are always different from what is expected or are never required and won’t be implemented. 

However, some additional features will be required to implement, and their implementation will be more complex due to unnecessary generalization.

Therefore, the next step should be the removal of unnecessary generalization before introducing the new feature; however, often, a new feature is added to the code with unnecessary generalization. The tech debt is growing, and future development is becoming increasingly complex.

I’ll share my “secret” with you

In my experience, the best way to implement features is to use Test-Driven Development (TDD) and Behavior-Driven Development (BDD) to stay focused on the essential requirements.

When working on applications, if generalization is needed to support new features, we can typically achieve it using the refactoring tools provided by modern IDEs. Since we’re guided by explicit requirements, any generalizations we introduce are purposeful and aligned with actual user needs.

To sum up: primitive obsession and premature generalization are common pitfalls, but they are easily avoided. Using clear, domain-specific types and delaying generalization until it’s truly needed keeps your code readable and maintainable. By focusing on actual requirements and leveraging TDD and BDD, you build simpler, cleaner solutions – no more ancient hieroglyphics, just clear, understandable code.

> subscribe shift-mag --latest

Sarcastic headline, but funny enough for engineers to sign up

Get curated content twice a month

* indicates required

Written by people, not robots - at least not yet. May or may not contain traces of sarcasm, but never spam. We value your privacy and if you subscribe, we will use your e-mail address just to send you our marketing newsletter. Check all the details in ShiftMag’s Privacy Notice