Having a protocol for your data layer and an in memory implementation of that for testing can make sense, especially when your real data store is expensive to bring up, but I would try to minimize the number of seams like this in my system. Each one introduces cognitive overhead due to indirection, so you should use them judiciously.
Pervasive use of protocols and custom, user-defined higher-order functions I would argue is unidiomatic Clojure and creates long-term pain as you end up with opaque functions being passed around that can't be inspected at the REPL and a lot of tricky "fitting functions together" that is made difficult without a static type system. You sometimes need them, but you should reach for them very judiciously.
There's a reason Clojure emphasizes data over functions (and both over macros).
It's of course not easy to see that next step from the article, since it doesn't eliminate any code by creating pure functions. But even in a toy example, there is value of creating pure functional abstractions. In some codebases, you might even see the team lead segregate pure functions by namespace: "Pure stuff over here, tainted stuff over there." In those situations, teams try to reduce impure surface area -- in this case, anything that touches the `db` namespace.
Clojure is such a practical language.
I actually feel like I understand everything that happens in the system now, and when problems arise I can REPL in and hunt them down with confidence.
Though I don’t use Clojure professionally, I’ve definitely become a better developer just from playing with it, a contrast to JavaScript where I feel like learning it actually lowered my iq…
That's not true that the approach is more OO.
People get confused because a lot of OO languages like Java eventually added support for something protocol-like, in Java it was interfaces for example, but for a long time Java, an OO language, didn't have interfaces, you only had Classes and Objects and inheritance.
Protocols and polymorphism is not an OO concept, and doesn't even need to involve Objects at all.
In Clojure for example, you can dispatch the protocol over a map, given two maps based on their metadata the protocol will pick a different implementation for the called protocol function. There are no Objects and Classes involved.
All you need for polymorphism is a sort of metadata over a datatype, it could be type information or it could be something else, like attached metadata like in Clojure.
In an OO language, and in Clojure by virtue of running on the JVM, user datatypes are defined using Classes and Objects, but in a language like Haskell they're not.
So basically you can implement protocol-like polymorphism, basically the idea that you dispatch based on meta-information about the arguments passed to the function with or without object constructs.
I'm pointing this out because it is an argument against OO. The fact that even in most OO language people have over time preferred to use such polymorphism over object inheritance hierarchies is a sign that Objects aren't as useful as ounce thought.
What is very useful though is to be able to define alternate function implementations based on some metainfo about a given argument, such as their type. So much so that all languages, OO and Functional will tend to have such feature.
smalltalk on the other hand, did not have explicit interfaces: http://www.jot.fm/issues/issue_2002_05/article1/
That seems like a huge loss. I feel like that approach could be great for business logic maybe? There's the "business logic as a library" technique that works well for this, and allows for easy testing.
I got a good laugh out of that one. Honestly that final paragraph should just be deleted. It's just a giant, indefensible claim.
Here's my suggestion instead, break out your pure and impure behavior. Don't design it so that you inject the behavior, instead seperate them into independent units and compose them at the handler.
(defn get-article-id
[request]
(get-in request [:path-params :id]))
(defn make-response
[status body]
{:status status
:body body})
(defn get-article!
[data-source request]
(->> request
(get-article-id)
(db/get-article-by-id! data-source)
(make-response 200)))
This is often known as the Functional Core, Imperative Shell pattern. The functional core cannot call out to the imperative shell.* it handles http requests * it uses a jdbc data source * the result of the parsing function and the arguments of the database fetch function are coupled, * the result of the database fetch and the formation of the http response are coupled
This has implications both in testing and in application maintenance. What needs to change if you decide you want to add caching in front of the database? What if you want to pick up requests off of a Kafka stream instead of from a web server?
Like I said, there are tradeoffs. The entire premise of the article is about depending on abstractions rather than implementations. Depending on your particular context, where you want to end up on the continuum should reflect your particular context.
InfoQ list the Key Takeaways as:
- We should aim for simplicity because simplicity is a prerequisite for reliability.
- Simple is often erroneously mistaken for easy. "Easy" means "to be at hand", "to be approachable". "Simple" is the opposite of "complex" which means "being intertwined", "being tied together". Simple != easy.
- What matters in software is: does the software do what is supposed to do? Is it of high quality? Can we rely on it? Can problems be fixed along the way? Can requirements change over time? The answers to these questions is what matters in writing software not the look and feel of the experience writing the code or the cultural implications of it.
- The benefits of simplicity are: ease of understanding, ease of change, ease of debugging, flexibility.
- Complex constructs: State, Object, Methods, Syntax, Inheritance, Switch/matching, Vars, Imperative loops, Actors, ORM, Conditionals.
- Simple constructs: Values, Functions, Namespaces, Data, Polymorphism, Managed refs, Set functions, Queues, Declarative data manipulation, Rules, Consistency.
- Build simple systems by: Abstracting (design by answering questions related to what, who, when, where, why, and how); Choosing constructs that generate simple artifacts; Simplifying by encapsulation.
So Clojure is a language that embodies these principles in its design. It's a Lisp, which means that all code is constructed from a very regular expression syntax that has an inherent simplicity and can be quickly understood. It's a functional programming language that provides exceptional tools for minimising mutating state, and it favours working with a small set of data structures and provides a core api with many useful functions that operate on them.
I'd say the result is getting a lot done with a small amount of code, minimal ceremony, true reuse, and the ability to maintain simplicity even as your system's capabilities grow.
It is hard to go back to other languages once you appreciate its simplicity.
Many options exists to not having to touch Java when you use Clojure, but I guess it's hard to kill old memes?