I was working on some JDBC-related Clojure code this week and had a nice refactoring/abstraction experience in a case where I would have punted (or not even considered it) in Java.
The code in question is reading a bunch of metadata from DatabaseMetaData and producing output. The interesting methods in DBMD all takes some filtering parameters and return a ResultSet. For example (using the clojure.sql lib):
(defn process-tables [connection] (reduce conj '() (map #(processTableRow %) (resultset-seq (.getTables (.getMetaData (connection)) nil nil "%" nil)))))
Given a connection, we (read from the inside-out):
- Get the DatabaseMetaData
- Call getTables() on it
- Convert the ResultSet into a sequence of rows (in the form of a map keyed by column name
- For each row map, call processTableRow which will return a sequence of output values
- Combine all of those output values into a single sequence with reduce
All very well, that works great. But then if you need to process catalogs you might write something like this:
(defn process-catalogs [connection] (reduce conj '() (map #(processCatalogRow %) (resultset-seq (.getCatalogs (.getMetaData (connection)))))))
As soon as I wrote the second one of these (wasn’t quite as clean as this) I could see that there was a pattern here where the two variable bits in the pattern are a) what method and parameters to call on DatabaseMetaData and b) how to process a row of the ResultSet produced by the call. The latter one is duh a function and would be easy to abstract in Java. The former would have been more annoying, requiring either a reflective method call or maybe an anonymous inner class. Honestly, I would have probably just copied the code in Java and been done with it.
However, in Clojure it’s trivial to abstract out that pattern:
(defn import-jdbc-metadata [connection databaseMetadataCall rowFunction] (reduce concat '() (map #(rowFunction %) (resultset-seq (databaseMetadataCall (.getMetaData (connection)))))))
Here the abstraction points are the databaseMetadataCall and rowFunction functions, both passed into the function. We can then rewrite the prior examples like this:
(defn import-tables [connection] (import-jdbc-metadata connection (fn [dbmd] (.getTables dbmd nil nil "%" nil)) processTableRow)) (defn import-catalogs [connection] (import-jdbc-metadata connection (fn [dbmd] (.getCatalogs dbmd)) processCatalogRow))
(process-metadata connection :tables [nil nil “%” nil] processTableRow)
And of course you don’t have to stop there. You could further say that it would be handy to create a macro that simplified those import method patterns as well but I’ll leave that for another day.