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.
