SOLID Principles in Clojure

Sign up for weekly Clojure tips, software design, and a Clojure coding challenge.

Summary: The SOLID principles are guidelines for writing good Object-Oriented code. It turns out that these principles are followed and embodied in Clojure.

Robert C. Martin (Uncle Bob) has named five basic principles of software design called SOLID. It's an acronym that helps people remember them. I really like these kinds of mnemonics because, well, we all need help remembering things. The easier to remember, the more we can learn.

Through lots of experience designing software, these principles were developed to help make software that is maintainable in the long term. It's a boon to the OO world that these and similar principles have been talked about so much. They've been identified, digested, named, and codified. And now you can speak about them and people know what you mean. This kind of thing is strangely lacking in the world of functional programming.

Why is this so? Perhaps it's that there hasn't been much use of functional programming in the software engineering industry in the last few decades. Some might say that these kinds of principles are not needed in FP. Regardless of why, it is a common frustration among people who switch from OOP to FP. I am often asked "How do I structure my code?" and "Where are all of the design guidelines?"

Well, I want to say that functional programmers do design their code. And they do follow principles. There just hasn't been enough people churning through them all to come up with catchy acronyms and names. Many of the same principles apply.

Today I'm going to go through the SOLID principles and show how they are manifest in Clojure. Let's do this one letter at a time.

Single Responsbility Principle

How much should one class do? The Single Responsibility Principle (SRP) says one thing. And the way you count the things it can do is by counting the reasons it may change. For instance, if you have a class that is responsible for reading in records from a database and displaying them to the user, that's actually 2 reasons to change. One reason is if the database schema changes. The other is if the design of the display changes. That's a violation of the principle and you should consider splitting up that class along those two lines.

Believe it or not, this comes up a lot in Clojure. You don't program much with classes, but you do program with functions. It's very common to see a function that reads in from a database then formats a string for display, maybe even printing it out!

(defn display-records []
  (let [records (sql/query "SELECT * FROM ...")
        record-string (str/join "\n" (for [r records]
                                       (str (:first-name r) (:last-name r) (:id r))))]
    (println record-string)))

That's doing three things and it's pretty obviously violating the SRP. The fix in Clojure is to refactor this into separate functions.^1

(defn fetch-records []
  (sql/query "SELECT * FROM ..."))

(defn record->string [record]
  (str (:first-name record) (:last-name record) (:id record)))

(defn records->string [records]
  (str/join "\n" (map record->string records)))

Then display-records just ties them together. You still need one that does everything. How many reasons does it have to change now? You don't need to change it if the schema changes. You don't need to change it if the format changes. I'll leave that as an exercise for you.

(defn display-records []
  (-> (fetch-records)
      records->string
      println))

Open/Closed Principle

What happens if you're using a library and you like what it does but you need to do it slightly differently? It would be terrible if you just changed the source code for that library. What else was depending on it? What might break? The Open/Closed Principle (OCP) states that we should be able to extend the functionality without changing the module.

The OCP is something Clojure does extremely well. In Clojure, we can extend existing protocols and extend existing classes without breaking existing code. For instance, let's say I have written this nice protocol called ToDate that has one method that converts something to a java.util.Date.

(defprotocol ToDate
  (to-date [x]))

Obviously, to make it useful I'll have to implement it somewhere. I can take this protocol and implement it with existing classes without modifying the classes themselves.

(extend-protocol ToDate
  String ;; strings get parsed
  (to-date [s]
    (.parse (java.text.SimpleDateFormat. "ddMMyyyy") s))
  Long   ;; longs are unix timestamps
  (to-date [l]
    (java.util.Date. l))
  java.util.Date ;; Dates are just returned
  (to-date [d] d))

Look at that! Now I can run this:

(to-date "08082015")

;;=> #inst "2015-08-08T05:00:00.000-00:00"

Or this:

(to-date 0)

;;=> #inst "1970-01-01T00:00:00.000-00:00"

Liskov Substitution Principle

Are queues and stacks subclasses of each other? They both have the same interface (push and pop), but semantically, they're quite different. Stacks are Last-In-First-Out and queues are First-In-First-Out. The Liskov Substitution Principle (LSP) states that a subclass should be able to be substituted for its superclass. You can't replace a stack with a queue (or vice versa), so they're not really subclasses of each other.

LSP is mostly about subclass hierarchies, which are rare in Clojure. But Clojure is built on the class hierarchies of Java. And the core types, which are written in Java, are well-designed with respect to this principle.

A simple example is the variety of clojure.lang.APersistentMap implementations. They each have different performance characteristics but they maintain the relevant semantics of maps. There are :

  • PersistentArrayMap
  • PersistentHashMap
  • PersistentStructMap
  • PersistentTreeMap

Because they all have compatible semantics according to LSP, the runtime can choose between them freely without you ever having to know or care.

Interface Segregation Principle

If I use some API and one of the methods I use changes, I can accept that I'll have to change my code. But if one of the methods I didn't use changes, it would be aggravating if I had to change something on my end. I shouldn't even have to know about those methods even existing. One way to prevent this nuissance is by applying the Interface Segregation Principle (ISP). It states that you should split up your interfaces into smaller interfaces, typically so that they have one reason to change. Now clients are only affected by changes that are relevant to them.

ISP is prevalent in Clojure. Much more so than in typical Java systems. Just look at the size of the interfaces in clojure.lang. So small! Here's a typical one:

class: clojure.lang.Associative

methods: containsKey, entryAt, assoc

These methods correspond to the typical map operations of containsKey, get, and put respectively. These three methods are highly cohesive. Contrast this with java.util.Map, which has 14. Now, all of the functionality of Java maps are in Clojure maps, they're just segregated to different, reusable interfaces.

For instance, the size method java.util.Map is a separate, 1-method interface called clojure.lang.Counted. Clojure applies the ISP very thoroughly, and ClojureScript slightly more so.

Dependency Inversion Principle

A module often depends on a lower-level module for the implementation details. This tightly couples the higher-level module to the decisions of the implementation module. For instance, if I have a reporting module that depends on the SQL query module to get its data, the reporting module is indirectly tied to the SQL database. The Dependency Inversion Principle (DIP) inserts an interface between layers. In our example, the reporting module will depend on a Data Source interface. And the SQL module will implement the Data Source interface. You could switch out the SQL module for a file-storage module and the reporting module wouldn't have to know.

Clojure uses the DIP everywhere. For instance, the core function map does not operate on any fixed data type—only abstractions. It operates on the clojure.lang.IFn abstraction, which is the interface functions implement. It also operates on the seq abstraction, which defines orders for collections, iterables, and other types. This makes map decoupled from any particular type and thus more generally useful. The same principle holds for many of the core library functions. By applying DIP universally, Clojure is made more powerful because functions can be reused more often.

Conclusions

The SOLID principles are important guidelines for designing software that will last. They guide us to make more useful, reusable components. However, they must be repeated a lot in language communities like Java because Java does not make them very easy to apply. In Clojure, the principles are present everywhere. One of the things I like about Clojure is how it seems to embody a lot of the lessons of the last 20 years of software engineering. And that's one of the things I like to bring up in the PurelyFunctional.tv Online Mentoring course. One of the reasons Clojure is making such big waves is that it has integrated good engineering principles, like SOLID, immutable values, and concurrency right into the core.


  1. [-> is an idiom in Clojure (not syntax, just a naming scheme). It means "transform to" and is pronounced "to". For instance, record->string is read "record to string".]{#fn1}