When Should You Use Clojure's Object-Oriented Features?

In the world of object-oriented programming, it is common to create classes to represent data elements from your domain. These classes run into all kinds of trouble. First, they tend to breed closely coupled classes like DTOs and XML type mappers. Second, they rarely contain any intelligence and sometimes don't contain any behavior at all. Third, proliferating concrete classes can make it hard to see common abstractions trying to escape.

In the past, I answered all of those objections by arguing that they were "merely" bad design. It is possible to do good OO design that doesn't suffer from those failings. I've reluctantly come to believe that the majority of OO code will never escape those failings. Common style, frameworks, and tools all push toward a design dominated by "god classes" and data containers.

Instead of fighting that, I've decided to embrace it. If your objects are just going to be data containers, then embrace the data! Don't hide it away inside objects, and don't fragment your namespace of functions over the data by splitting them across dozens of classes.

Now when I build applications in Clojure I tend not to use Clojure's OO constructs, protocols and types, for the domain data passing through the system. I've followed Rich and Stu down the path of values and data structures.

I do find Clojure's OO constructs valuable for the structure of the system itself. I like to use protocols to limn the boundaries between subsystems. They provide exactly the degree of information hiding I want. We've been evolving a common idiom for creating instances of protocols, where we have an implementation as a type together with a constructor function. The constructor function is considered public, while the type is meant to be private.

(defprotocol
IMarshaller
 (marshall [_ data] "Marshall data, return a
ByteBuffer ready to transmit.")
 (unmarshall [_ bbuf] "Unmarshall
data from the ByteBuffer"))

(deftype EdnMarshaller []

IMarshaller
 (marshall [_ data] (io/clj->bbuf data))

(unmarshall [_ bbuf] (io/bbuf->clj bbuf)))

(defn
edn-marshaller [] (EdnMarshaller.))

Most of what we pass across subsystem boundaries are primitives and data structures of primitives. This particular example uses Java ByteBuffer objects, but we treat them like immutable value objects so I'll call them "primitives" for the sake of argument.

That troika of protocol, type, and constructor appears fairly often, but there are some variants.

In this example, the functions on the type are small enough that you could simply reify the protocol inside the constructor function:

(defn edn-marshaller []

 (reify IMarshaller
 (marshall [_ data] (io/clj->bbuf
data))
 (unmarshall [_ bbuf] (io/bbuf->clj
bbuf))))

This works best when the constructor only takes one set of arguments. I would split this out when there are optional or variadic arguments to the constructor function.

Using objects to represent subsystems gives us information hiding and polymorphism. That's exactly what we need. Using data structures for the application data gives us leverage over those data structures. One more thing to consider is when and how to avoid coupling data structure shapes across subsystems, but that's a topic for a future post.

Get In Touch