In my book “Clojure Polymorphism” I use a service as an example for creating a polymorphic abstraction. The service is imagined to be a key-value blob store, like S3, Cloudfiles, or Azure.
I try as much as possible to focus the book on features that are compatible with both Clojure and ClojureScript, but here is a tip that is only useful in Clojure.
The main function in my example changes slightly for each implementation approach, but the core of it remains the same: we let the initialized service then enter a
try block which closes the service in a
finally block. Using a
finally block means that the service will get closed whether the
try block exits normally or via exception, and this is the safest way to ensure resources are cleaned up.
For this pattern Clojure has a built-in macro called
with-open. However, you can only use
with-open with an object that implements the
java.io.Closeable interface. An advantage with defining your abstraction using a protocol (as opposed to plain functions or multimethods) is you can also implement
java.io.Closeable for your service object:
Then the main function can be rewritten to:
The downside is there is no way to enforce this in the abstraction. Each implementation of the abstraction must individually implement
java.io.Closeable. It would be possible if you were using the “Protocol + Client Namespace + Multimethod” approach to wrap the object as it gets returned from connect in a
Closeable object, but that is the closest you could get:
I find this ugly, because not implementing
java.io.Closeable is a programmer mistake. If you were to use
with-open with an object that does not implement
java.io.Closeable, you would get an exception, you would go add that interface to the appropriate service implementation, you would double check all the other service implementations, and from that point on forevermore you would not get the exception anymore. Over the lifetime of your application maybe you would write 5-10 implementations of this service abstraction? Do you need to add a bunch of lines of code and an extra layer of run-time construct just because a programmer accidentally wrote the wrong code? The tradeoff doesn’t make sense to me.
What does make sense to me is taking advantage of platform interfaces and conventions to save myself time and lines of code. So if you’re implementing a service abstraction on the JVM throw a
java.io.Closeable in there.