5 Comments

Four Reasons to Use Maps Instead of Classes

I started my career back when Object-Oriented Programming was first becoming a dominant methodology, and boy, was OOP going to change the world. Encapsulation, inheritance, polymorphism—these tools promised to save the righteous from complexity, guarantee reusability, and decouple all the things!

So when I recently starting developing in Clojure, the suggestion to “just use maps to represent data” gave me pause—how could maps be better than objects? Heresy! I’ve since fallen in love with the approach, and would like to share four reasons why.

1. Maps travel well.

Most of the data I work with today needs to travel, and when my data does move from one machine to another, it’s quite likely that I’m going to be using different languages and solving different problems. So I generally want to serialize the data, but not the logic I use to operate on that data. Maps are an extremely good model here–they’re trivial to serialize, and they separate the data that moves between platforms from logic that does not. As a bonus, they poof up nicely into Javascript objects.

2. Maps come with wonderful tools.

Well, maybe not in every language, but in Clojure, they’re amazing. Let’s say I have the following players on my fantasy team:

(def players [
  {:name {:first "Jose" :last "Peraza"}
   :stats {:stolen-bases 35 :home-runs 6}
   :position :ss}
  {:name {:first "Mike" :last "Trout"}
   :stats {:stolen-bases 15 :home-runs 36}
   :position :of}
  {:name {:first "Manny" :last "Machado"}
   :stats {:stolen-bases 20 :home-runs 30}
   :position :ss}])

I can very easily do things like compute total stolen bases:

user=> (reduce + (map #(get-in % [:stats :stolen-bases]) players))
70

or get a list of my shortstops:

user=> (:ss (group-by :position players))
 
[{:name {:first "Jose", :last "Peraza"}, :position :ss, :stats {:stolen-bases 35, :home-runs 6}} {:name {:first "Manny", :last "Machado"}, :position :ss, :stats {:stolen-bases 20, :home-runs 30}}]

Clojure’s core library includes a ton of great functions for querying, transforming, and reducing maps, and you get all of that for free when you start out with a map for your data.

3. Maps reduce boilerplate.

I suppose it should be noted that maps, of course, are objects, and they’re wonderful objects for holding data of all shapes and sizes. It’s been very liberating to use a great structure specifically designed for holding data instead of constantly writing boilerplate to do the same job. For example, if I want to add a person to some storage structure, I can just write:

(add-person {:first-name "Jose" :last-name "Peraza"})

and happily be on my way. If I want an object, I have five minutes of Person class boilerplate to hammer out. And all that boilerplate generally gets me much less functionality.

4. It’s easy to modify the structure of a map.

Objects, sadly, have fixed signatures. While this can be wonderful for some use cases, it’s the opposite of what you want for others. I’m finding more and more cases where it’s really nice to be able to add or remove bits of data from records. Let’s say, for example, that Mike Trout wins another MVP this year, and we really want to update his entry to show this important information. If I’m using maps, that data can easily be added to what I know about him:

user=> (->
  #_=> (get players 1)
  #_=> (assoc :awards ["MVP 2015", "MVP 2016"]))
{:name {:first "Mike", :last "Trout"}, :position :of, :stats {:stolen-bases 15, :home-runs 36}, :awards ["MVP 2015", "MVP 2016"]}

And now my Mike Trout object is clearly marked with his MVP award.

I hope you have a chance to try using maps for your data structures more often. Yes, there will be times when a custom class is more appropriate, and it does take some getting used to, but I’ve found that most of my data, most of the time, lives quite happily in maps. This frees me to spend more time on what’s unique about my application and less time banging out uninteresting code.