I've been trying to build a discipline of separating my protocol definitions into their own namespace, primarily as a stylistic choice. One thing I don't like about this approach is that since anything a namespace requires is effectively "private" to that namespace, users wanting to call protocol functions from another namespace would have to add a require statement to their code for the protocols.
For example:
Protocol definition namespace:
(ns project.protocols)
(defprotocol Greet
(greet [this greeting]))
Implementation namespace:
(ns project.entities
(:require [project.protocols :as protocols]))
(defrecord TheDude
[name drink]
protocols/Greet
(greet [this greeting]
(println "The Dude sips a" drink)
(println greeting)))
Core namespace:
(ns project.core
(:require [project.protocols :as protocols]
[project.entities :refer [TheDude]]))
(let [dude (TheDude. "Jeff" "white russian")]
(protocols/greet dude "not on the rug, man..."))
This works just fine, but I don't particularly like that users need to be aware of the need to require project.protocols
which is really an implementation detail internal to project.entities
. In other languages I would just refer to project.entities/greet
within project.core
but namespaces don't "export" their required vars in Clojure, they are internal to the requiring namespace only. I see two obvious alternatives, a third could be using something like Potemkin:
project.entities
here).As an example of number 2:
(ns project.entities
(:require [project.protocols :as protocols]))
(defrecord TheDude
[name drink]
protocols/Greet
(greet [this greeting]
(println "not on the rug, man...")
(println "guess i'll have another " drink)))
(def greet protocols/greet) ; ¯\_(ツ)_/¯
My question, which I suppose is primarily one of preference, is what is the "best practices" (if any) way to handle this sort of separation of concerns? I realize that adding the require
in project.core
is only one more line, but my concern is less about line count and more about minimizing what a user would need to be aware of.
EDIT: I think the obvious way to accomplish this is to not expect users to require both namespaces, but to create a core namespace which does that for them:
(ns project.core
(:require [project.protocols :as protocols]
[project.entities :refer [TheDude]]))
;; create wrapper 'constructor' functions like this for each record in `project.entities`
(defn new-dude
[{:keys [name drink] :as dude}]
(map->TheDude dude))
;; similarly, wrap each protocol method
(defn greet [person phrase]
(protocols/greet person phrase))
Now any user can just require core, and if they want to extend the protocol to their own record in a different namespace, they can do so and calls to core/greet
will pick up the new implementations. Additionally, if there is any pre/post processing to be done, that can be handled in the "higher level" API function core/greet
.
In a program that needs protocols, usually objects are instantiated in some places and consumed (by protocol) in other places. Perhaps in some cases where that's not so, you didn't really need a protocol anyway. In effect, is not too common to "require" the namespaces of both a protocol and an implementation of it. If it happens a lot, it is a code smell.
pete23's answer mentions using the dot syntax to call a record's method without involving the protocol's namespace. But using the protocol function has some modest advantages.
Absent implementation inheritance, a protocol contains essential ("primitive") functions only. Such functions are convenient to implement, but not necessarily super-friendly to callers. The protocol namespace is a great place to add non-primitive accessors, of the sort that you might object-orientedly have declared as a default method on an interface or as an inherited non-abstract method on an abstract base class. Consumers that use the protocol's namespace can call the primitives and non-primitives alike.
Sometimes a primitive turns out to need pre- or post-processing common to all implementations. No need to repeat the common stuff in every implementation! Just lightly refactor: rename the protocol function from f
to -f
, update the implementations, and add a function f
in the protocol's namespace that wraps -f
with the necessary pre and post. The callers do not need any change.