Interface
Component interfaces give a number of benefits:
- Single point of access. Components can only be accessed through their interface, which makes them easy to find, use and reason about.
- Encapsulation. All the implementing namespaces for a component can be changed without breaking the interface contract.
- Composability. All components have access to all other components via interfaces, and can be replaced as long as they use the same interface.
When we created the
user
component, the user
interface was also created.So what is an
interface
and what is it good for?An interface in the Polylith world is a namespace named
interface
that often lives in one but sometimes several namespaces within a component. It defines a number of def
, defn
or defmacro
statements which forms the contract that it exposes to other components and bases.If more than one component uses the same interface, then all these components must define the exact same set of
def
, defn
and defmacro
definitions, which is something the tool helps us with.To give an example, let's pretend we have the interface
user
containing the functions fun1
and fun2
and that two components "implement" this interface, e.g:▾ myworkspace
...
▾ components
▾ user
▾ src
▾ com
▾ mycompany
▾ user
interface.clj
fun1
fun2
...
▾ admin
▾ src
▾ com
▾ mycompany
▾ user
interface.clj
fun1
fun2
...
...
We often choose to have just a single
interface
namespace in a component, but it's also possible to divide the interface into several sub namespaces. To do so we first create an interface
package (directory) with the name interface
at the root and then we put the sub namespaces in there.We can find an example where the
util
component in the Polylith repository does that, by dividing its util interface into several sub namespaces:util
└── interface
├── color.clj
├── exception.clj
├── os.clj
├── str.clj
└── time.clj
This can be handy if we want to group the functions and not put everyone into one place. A common usage is to place clojure specs in its own
spec
sub namespace, which we have an example of in the RealWorld example app, where the article
component also has an interface.spec sub interface.(ns clojure.realworld.rest-api.handler
(:require ...
[clojure.realworld.user.interface.spec :as user-spec]
...))
(defn login [req]
(let [user (-> req :params :user)]
(if (s/valid? user-spec/login user)
(let [[ok? res] (user/login! user)]
(handle (if ok? 200 404) res))
(handle 422 {:errors {:body ["Invalid request body."]}}))))
Every time you think of splitting up the interface, keep in mind that it may be an indicator that it's instead time to split up the component into smaller components!
So far, we have only used functions in the interface. Polylith also supports having
def
and defmacro
statements in the interface. There is no magic here, just include the definitions you want, like this:(def one-two-three 123)
Now it can be used as a normal definition from any other component or base.
A
defmacro
definition can look like this:(ns se.example.logger.interface
(:require [se.example.logger.core :as core]))
(defmacro info [& args]
`(core/info ~args))
...which delegates to:
(ns se.example.logger.core
(:require [taoensso.timbre :as timbre]))
(defmacro info [args]
`(timbre/log! :info :p ~args))
This list of tips makes more sense when you have used Polylith for a while, so take note of this section for later:
- Functions can be sorted in alphabetical order in the interface, while we can freely arrange them in the implementation namespace(s).
- The interface can expose the name of the entity, e.g.
sell [car]
, while the implementing function can do the destructuring, e.g.sell [{:keys [model type color]}]
which sometimes can improve the readability. - If we have a multi-arity function in the interface, a simplification can sometimes be to have a single arity function in the implementing namespace that allows some parameters to be passed in as
nil
. - If using variadic functions in the interface, a simplification is to pass in what comes after
&
as avector
to the implementing function. - Testing is simplified by allowing access to implementing namespaces from the
test
directory. Only the code under thesrc
directory is restricted to only access theinterface
namespace. The check is performed when running thecheck
,info
ortest
command. - All functions can be declared public while still being protected. This improves testability and the debugging experience. When stopping at a breakpoint to evaluate a function, we don't need to use any special syntax to access it, that we otherwise would have to if it was private.
- If using a
function
in two components that implement the same interface, all definitions must befunction
. The same goes formacros
. The reason for this restriction is that functions are composable, but macros are not, which could otherwise cause problems.
Finally, the
interface
namespace name can be changed in :interface-ns
in ./workspace.edn
. Here are a few reasons why we would like to do that:- We want to share code between Clojure and ClojureScript via
.cljc
files. Becauseinterface
is a reserved word in ClojureScript, this may cause problems. - We want to consume clojure code from another language on the JVM, e.g. Kotlin, where
interface
is a reserved word.
A good reason to keep the default
interface
name is that it communicates what it does.Last modified 1yr ago