poly
Search
K

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
...
...
Now we are free to edit the interface.clj file for both user and admin, which means they can get out of sync if we are not careful enough. Luckily, the Polylith tool will help us keep them consistent, and complain if they differ when we run the check, info or test commands!
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.
It can then be used from e.g. the handler namespace in rest-api:
(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!

Interface definitions

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 a vector to the implementing function.
  • Testing is simplified by allowing access to implementing namespaces from the test directory. Only the code under the src directory is restricted to only access the interface namespace. The check is performed when running the check, info or test 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 be function. The same goes for macros. 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. Because interface 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.