Customizing stuartsierra/component

December 12, 2014
  1. what is a component?
  2. update-system: for everything and everywhere
  3. How can I customize my component?
  4. When can I customize my component?
  5. BigBang: all component updates are just-before and just-after component/start

My interest in customization systems started when I tryed to achieve real time system visualisations, and reverse dependency injection (also called 'co-dependency'). To solve both requirements I started with forking original stuartsierra/component library until I realised that I could use the "customization way" of this library

what is a component?

Extracted from the component/README

For the purposes of this framework, a component is a collection of functions or procedures which share some runtime state.

But, I really think that we can improve the clarity of this definition with my following one:

started-component (own definition proposal)

a started component is a collection of functions or procedures wich share some runtime state produced in component/start (possibly using other started components, also called dependencies)

One example of started-component can be a webserver component listening on a specific port. As you can realise, once the server is started the port is assigned, then if you try to start again your server component using same port value you'll get a "java.net.BindException: Address already in use:"

update-system: for everything and everywhere

IMHO this update-system is the key function to understand the internals and the endless posibilities of stuartsierra/component library, used to update, start or customize your component system.

(defn update-system
  "Invokes (apply f component args) on each of the components at
  component-keys in the system, in dependency order. Before invoking
  f, assoc's updated dependencies of the component."
  [system component-keys f & args]
  (let [graph (dependency-graph system component-keys)]
    (reduce (fn [system key]
              (assoc system key
                     (-> (get-component system key)
                         (assoc-dependencies system)
                         (try-action system key f args))))
            system
            (sort (dep/topo-comparator graph) component-keys))))
...so update-system it's a common reduction on the system that applies any fn to each component after injecting fresh dependencies. And... logically, if we pass a system and a fn we get a new-updated-system.


update-system: to start your system (all your components)

As you can see: component/start-system also uses component/update-system to call component/start to get the system started.


update-system: to customize your system (all your components)

Extracted from component/README#customization :

Both update-system and update-system-reverse take a function as an argument
and call it on each component in the system.
Along the way, they assoc in the updated dependencies of each component.
The update-system function iterates over the components in dependency order
(a component will be called after its dependencies)....


How can I customize my component?

In the component world, customizing your component is to apply updates to the component before injecting it into its dependants (or before leaving this component in the system to others components to use it).

;; so in other words ... the update-system internal code :-
		(assoc system key
	                 (-> (get-component system key)
	                     (assoc-dependencies system)
	                     (try-action system key f args)))

You can update your system (all your components) with update-system function like that:


(defn component-customizer[component & args]
	... apply any transformation to your component and return your component updated
  	component)
  	

(component/update-system your-system component-customizer) 	
  	

When can I customize my component?

Try to rethink my previous started-component example, and...

One example of started-component can be a webserver component listening on a specific port. As you can realise, once the server is started the port is assigned, then if you try to start again your server component using same port value you'll get a "java.net.BindException: Address already in use:"

let's find the differences of two following sequence calls:

(-> system                                              ;;  {components}
    (component/update-system your-update-fn your-args)  ;;  {updated-components}
    (component/update-system component/start)           ;;  {started-updated-components}
    
(-> system                                              ;;  {components}
    (component/update-system component/start)           ;;  {started-components} 
    (component/update-system your-update-fn your-args)  ;;  {updated-started-components}

Yes, we get different results {started-updated-components} vs {updated-started-components}, and If we simplify the problem...

;; case 1 {started-updated-components}

user> (def system {:a 1})
user> (def system-updated (assoc system :c 3))
user> (def system-started (assoc system-updated :b 2))
user>  (system-started :c)
=> :3

;; case 2 {updated-started-components}

user> (def system {:a 1})
user> (def system-started (assoc system :b 2))
user> (def system-updated (assoc system-started :c 3))
user> (nil? (system-started :c))
=> true

When we update our system with component/start we get the running system state and further updates over this running system state are not available to this started-system-value

BigBang: all component updates are just-before and just-after component/start

milesian/BigBang generalize the "how and when can you customize your system?" letting you compose all your component updates in the same component/start invocation time, but distinguishing those updates that have to happen just-before from those that have to happen just-after component/start.

So you write this code


(bigbang/expand system-map
                        {:before-start [[fn1 arg1]
                                        [fn2 arg1 arg2]]
                         :after-start  [[fn3 arg1 arg2]
                                        [fn4 arg1]]})

and you get this

(update-system system-map #(comp (apply fn4 [arg1]) (apply fn3 [arg1 arg2]) component/start (apply fn2 [arg1 arg2]) (apply fn1 [arg1]))