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.

(defn -main
  [& args]
  ;; ... initialize some things ...
  (let [access-key (System/getenv "AWS_ACCESS_KEY_ID")
        secret-key (System/getenv "AWS_SECRET_ACCESS_KEY")
        storage-conn (storage/connect access-key secret-key)]
    (try
      ;; ... initialize some more things, or maybe just do stuff ...
      (finally
        (storage/close storage-conn)))))

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:

(defrecord S3Storage
    [access-key secret-key]
  proto/IStorage
  (get [this bucket key]
    (s3/get-object this bucket key))
  (put [this bucket key value]
    (s3/put-object this bucket key value))
  (delete [this bucket key]
    (s3/delete-object this bucket key))
  java.io.Closeable
  (close [this]
    (proto/close this)))

Then the main function can be rewritten to:

(defn -main
  [& args]
  ;; ... initialize some things ...
  (let [access-key (System/getenv "AWS_ACCESS_KEY_ID")
        secret-key (System/getenv "AWS_SECRET_ACCESS_KEY")]
    (with-open [storage-conn (storage/connect access-key secret-key)]
      ;; ... initialize some more things, or maybe just do stuff ...
      )))

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:

(defn connect
  [options]
  (let [obj (proto/connect options)]
    (reify
      proto/IStorage
      (get [this bucket key]
        (proto/get obj bucket key))
      (put [this bucket key value]
        (proto/put obj bucket key value))
      (delete [this bucket key]
        (proto/delete obj bucket key))
      java.io.Closeable
      (close [this]
        (proto/close obj)))))

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.