In early 2018, Google announced a change in the pricing of their maps services, and finally implemented it half a year later. The new prices would have been prohibitively expensive for many tech companies (think thousands of U.S. dollars a month), and many scrambled to find alternatives. Along with everyone else who was affected, we had to:
- Find alternative service providers for each Google Maps service we used
- Compare pricing, quality, and licensing (Many providers, while good, forbade the combination of their service with others. Even more maddening, few actually support the Philippines.)
- Decide, and then implement across all affected platforms.
The problem is, of course, that step 3 is a lot of engineering effort, and steps 1 and 2 take time. When customer satisfaction and operation costs are on the line, one does not simply say:
“Nah, fam. I already implemented the thing. We’re stuck with this suboptimal service because it would take too much effort to switch.”
One can’t wait 5 months for a final decision either.
The solution then, is to start early and build out infrastructure to reasonably support every alternative.
In our use case, we use two major features: the map and the geocoding service. For this article, we’ll be focusing on the map because it’s “read only”. Information only flows from the app state into the map, and not the other way around. As for forward and reverse geocoding, that’s for the next post in the series.
Protocols and Classes
Modern mapping libraries have roughly the same features. Beyond displaying the map itself, one can expect to place markers or pins, draw lines and shapes, and control what part of the map is shown. While these features are relatively consistent, their APIs vary greatly across alternatives.
The following is a sample class hierarchy involving pigeons, penguins, bats, and whales.
- Flying Birds
- Aquatic Birds
- Flying Birds
- Flying Mammals
- Aquatic Mammals
- Flying Mammals
Because a class hierarchy is a tree, the sharing of attributes and methods outside of the parent-child relationship is, depending on the language, anywhere between complicated and impossible.
In contrast, the same set of animals can be described in this way:
- Pigeons (Flying, Avian)
- Penguins (Aquatic, Avian)
- Bats (Flying, Mammalian)
- Whales (Aquatic, Mammalian)
While the Flying protocol shared by pigeons and bats may declare a set of methods like
maneuver, implementation will be specific to pigeons and bats. (Of course! They work differently.)
Protocols lend themselves very well to composition, but sometimes it’s harder to see structure. As the saying goes, use the right tool for the right job.
The first step is in declaring the protocols. There are many ways of organizing protocols and namespaces, but I like separating protocol declaration from implementation. This is how I’ve structured the project below.
Please read the inline documentation and comments–they’re part of this article!
And now we can implement these protocols as appropriate. Note that in the following section, I will be using
extend-type, which modifies an existing type (like
google.maps.Marker) with additional methods that conform to the attached protocols.
extend-type is not the only way to use protocols: there’s
reify, and while they’re safer to use than
Let’s declare a separate namespace
And then extend the marker type. A marker is a generic map entity, but it’s also a single map entity representing a single point on the map.
We are returning nil in
destroy! because it should no longer exist.
In contrast, the polyline does not represent a single point, so
set-position! makes no sense. So, we only want to extend polyline with just the
Probably the most straightforward implementation here is
get-dom-node, effectively an alias for
getDiv. We go through this trouble because we want a consistent API for all maps we could possibly want to use.
Finally, we explicitly define a
new-google-map function to instantiate a… new Google map. We could have named this as simply
new-map to allow for consistency of this “constructor” function across the other services, but in this case it’s important to be very clear about what it is you’re constructing.
Now let’s implement the same protocol for Mapbox. Notice that in our namespace declaration,
mapbox-gl is in the require statement. Unlike Google Maps, Mapbox exists as an npm library. Unfortunately, it’s huge–it takes up about 23% the size of final sakay webapp artifact, (in contrast, ClojureScript takes up 17%, and the sakay-specific code is another 17%) but there’s not much we can do about that.
Mapbox markers are interesting. They’re actually dom elements that are positioned relative to the map. So, to set the marker’s opacity, we can just apply css styling. I’ll leave the implementation of this method as an exercise for the reader.
Mapbox doesn’t have polylines as a first-class entity, so we have to make a record that implements the appropriate protocols. You can think of records as clojure maps that have methods associated with them.
And since polylines aren’t a first-class entity, “constructing” it is a little different. Also notice the trivially simple implementation for
Now that our implementation is complete for both providers, we can use them.
At the time of writing, there is a
#wontfix memory leak in Google Maps making it difficult to properly destroy a map instance. Having said this, I need to point out a few important things regarding this leak, and the exercise we just did.
- The Google map we’re instantiating here is very bare, and the memory leak is unlikely to affect this demo app much. For an actually useful webapp though, this leak will be non-trivial.
- You probably shouldn’t even be switching between different map providers during runtime. Just comment things out, and refresh the browser. Our runtime switch is for illustrative purposes only.
- If things are really that bad that you need to support service switching right up to deployment, you can come up with compiler flags via
goog-definethat are basically variables that can be initialized based on build flags.
Protocols are a powerful tool for creating and managing abstractions or interfaces to external services. They are not limited to our example above. Protocols can be used for database connections, display rendering, and more.
In the next part of this series, I’ll show you how we dealt with different geocoding providers using a webapp framework called fulcro.