SOLID Principles in Clojure
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 use
s 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.
->
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".