So you finally managed to do it, you started a company after all those water cooler criticisms of your previous employers. You can finally do all those “if it were my company” things. So, after meeting your brilliant co-founder and raising a pile of cash from venture capitalists, you begin to hastily code the product of your dreams with the programming language of your dreams.
Fast forward a couple months, and you’ve got a customer and some new angels in the round. Time to hire your first engineer, whom you will pay very little salary and plenty of employee stock pool. After a couple weeks, your star developer hasn’t shipped any code! Circle-back-around to discuss. Turns out your star engineer can’t understand a thing you wrote. Impossible, you used Clojure!
Luckily you have some money left in the coffers, and summon the best Clojure consultant you can find, Rich Hickey himself. He’s really expensive though, so you just go with the independent Clojure Consultant from Toronto, some girl named Janet. Not really sure what to make of this consultant, you ask for a deep dive of the codebase and her humblest opinions on how to make this Clojure project last for eons. Expecting to read nothing but praise for your code, you peer into the report.
I thought it was pretty common knowledge to the average Clojure developer that
fn forms can have names. As in, its own symbol in the symbol table. Sure, It might not be useful in every case, but naming your
fn forms makes debugging awesome. The name will show up in stack traces, and debuggers can add breakpoints to them. Plus, with a descriptive enough name, you can probably get away with avoiding comments since the
fn form lacks a docstring argument.
Describe the shape of data in docstrings
Docstrings are pretty handy, as if documentation was built right into the language. For
defn, I usually use docstrings, and I always describe the function’s arguments and what it returns. But, I try to take it a bit further than that and describe the shape of the data. For example, if it takes a vector of maps as an argument, I try to describe the key value pairs in the map. Same with the return value. Sometimes Clojure functions can be ambiguous, and that ambiguity can bite when the codebase has enough complexity, or we’re just trying to get the last of the sprint in.
Sprinkle clojure.spec, lightly.
No need to worry about describing function input and output if using Clojure.spec, right? Here’s where I disagree with other Clojurians. If you’re going to go through the hassle of 'specing' all your functions, you may as well just use a statically typed language. Before you say it, I also don’t think it’s worth all the specing to have spec generators generate your test cases either. It requires enormous up-front effort, more code more problems, so think about it before reaching for them. However, I do use Clojure.spec (or competitors) for my system boundary / user input validation. If you do use Clojure.spec in your codebase, please don’t put them all in one namespace.
Organize code by technical and business domain
This might sound obvious to some, but I’ve seen a lot of Clojure codebases that put all their functions in a single namespace for a single domain. This might sound pretty damn close to the god class anti-pattern if you’re coming from object oriented programming, and it is. Here’s what happens: if you have a single namespace devoted to all your
honeysql database queries, over time the namespace will grow to the point where any new person will not search for a query they need, instead opting to write it again because of the information overload. I organize my Clojure codebases by technical and business domain via the namespace’s symbol and file structure, leading to a natural way of discovering the code for the next developer's needs.
Dispatch on protocols
One way I’ve segmented my codebase in the past was to have the core business logic use protocols to call domains, and have input/REST handlers use protocols to hook into the business logic. Sound familiar? I just described hexagonal architecture (basically). Protocols aren’t just great for abstraction either, you can extend existing types with protocols, including Java types, and probably the best option for coercing Java land types to Clojure types. For example, If you have a
java.util.HashMap with nested Java types, you can define a protocol for each Java type you expect to see in the
HashMap, walk the
HashMap with Clojure.walk, or reduce it with
reduce-kv, dispatching the protocol for the current key-value pair. Easy. And, since you’re smart, you probably just figured out you can do the same thing with Clojure types, and not just types, Clojure literals (primitives) as well. Obviously, you don't have to coerce anything; you can just make them polymorphic, write even less code, and avoid performance hits (unlike multimethods).