Map as Class Anti-Pattern

Today, I will write about a Map anti-pattern that I have sometimes observed.

I have come across code as follows from time to time:

I suggested to the developer who wrote similar code to use a class instead of a Map.  I explained that a Map should be used for mapping problems, such as associating data with key value pairs.

The developer was upset, and argued back that using a class instead of a map is over-engineering. I interpreted this as some sort of logical fallacy “Ad Hominem” attack against classes. I exclaimed “Using a map is under-engineering! At the very least, we can introduce an interface that functions a marker class, and let Jackson perform the transformations?” This developer would not budge, and stated that she did not want to write a class for everything that will become serialized as JSON. I stepped back, gave her the benefit of the doubt, looked further at the code, noted the pervasiveness of this anti-pattern, and now I am writing this.

Deeper analysis of the code affirmed my conviction that classes should not be substituted with maps. Now I will attempt to objectively make my case.

My most compelling reason is that it is not cohesive.  When maps are created, fields are assigned at different places within the code, and ultimately are consumed at different sections of the code. This production and consumption of the data loses its proximity from the location of its original declaration. It would often happen in a different method, a different class, or even a different library. This forces the consumer to search through multiple modules, methods, or classes in order to discover which data resides within the map. This a time-consuming and error prone process that takes much more time than just creating a plain ordinary Java object. Then after time is spent on this lookup, the following problems still exist:

  • A map promotes magic “Strings” for the keys unless a class is written just to contain the keys. If there is no class written for the keys, then either a) somebody must read the source code to determine which key value pairs and types are in the map b) the application must be debugged to determine which key value pairs and types are in the map or c) information can pass through the organization through tribal knowledge.
  • There can be a typo in the name of the field, which is not a compilation, but instead a runtime error or bug.
  • Refactoring has to change String names and types, and there is less tooling available for that.
  • Maps cannot have any business methods.
  • Maps cannot support primitives.
  • Composition with other classes requires more and more hash maps.
  • Maps do not work well with other frameworks that use annotations such as the Validations framework (JSR 349), JPA, Spring, Juice, etc.
  • Maps performs more slowly than a class, and also requires more memory.
  • Extracting values from a map requires a cast, which pollutes the readability of the code.
  • The lack of strong typing of a value in the Map makes it error prone.

Conclusion

If you need serialize data then introduce a class that represents that data and then use a serialization framework that can perform the serialization for you.

If you need to read JSON into a general purpose data-structure (situations where you don’t know what class you are parsing, then use Jackson’s JsonNode class.

If you need to pass data to other classes or methods, then again, introduce a class. You’ll find the code much more cohesive, readable and safe.

If you are tired of writing complete classes then use Lombok. With Lombok you can write POJOs as simply as this:

Or use the new JDK 14 record type.

It is not over-engineering to introduce a class.

Leave a Reply

Your email address will not be published. Required fields are marked *