01: Your First System
Systems, components, and signal handlers
donut.system supports you in thinking about your application as a system of interacting components, where:
- components are well-boundaried collections of processes and state that produce behavior
- systems are collections of components
How does the library do this? By giving you a way to express the conceptual, architectural organization of your application in code. Let’s look at a very simple example:
|
|
Going from the bottom up: on line 10 we’re calling ds/signal
with two
arguments, a system map and the signal ::ds/start
. On line 8 you can see
that there’s a map that has the key :printer
with a map as its value, and that
inner map includes the key ::ds/start
– the same keyword that we passed to
ds/signal
. Its function gets called, and the result is that a true statement
about donuts got printed.
How does this happen? What is the relationship between all these pieces – the
ds/signal
function, the structure of the system
map, and the ::ds/signal
keyword – that produces the behavior we saw?
Conceptually, donut.system models system behavior in terms of sending and responding to signals. (This is metaphorical: there’s no network or sockets or anything like that involved.) The system of components define signal handlers, and when you “send” a signal to a system it applies the signal by traversing each component and calling its signal handler for that signal. These signal handlers are what perform useful work in your application.
This idea manifests in your code in the way components are defined. You define
components as maps that associate signal names (like ::ds/start
) with signal
handlers (functions like (fn [_] (print "donuts are yummy!"))
). This is a
component definition:
{::ds/start (fn [_] (print "donuts are yummy!"))}
So when you call (ds/signal system some-signal-name)
, it looks for all
components definition maps that have a key of some-signal-name
, and calls the
corresponding function.
When I say “it looks for all component definition maps”, I mean that component definition maps must be organized in a particular way within the system map for them to be found and used. The general structure is:
{::ds/defs
{:group-1
{:component-1 component-def-map
:component-2 component-def-map}
:group-2
{:component-3 component-def-map}}}
The value of ::ds/defs
is a map whose keys are group names, and and whose
values are component groups. A component group is a map whose keys are component
names, and whose values are component maps.
For a map to be treated as a component definition, it must be located at the
correct place within these nested maps. It must be accessible via get-into using
a 3-element path like [::ds/defs :group-1 :component-1]
; this won’t work:
{::ds/defs
{:group-1
{:sub-group-1 component-def-map}}}
There’s more to donut.system, but this is the foundation: it gives you the tools to structure your application as a system of components that produce behavior by handling signals.
How might you modify this system definition so that your component can handle other signals? Let’s look at that next.
Summary
- Components are well-boundaried collections of processes and state that produce behavior
- Systems are collections of components
- You define components as maps of signal handlers
- Signal handlers produce behavior in response to signals
- You place components in a system maps under component groups, where component
groups live under the
::ds/defs
key