Apply AOP in stuartsierra/component

December 15, 2014
  1. introduction
  2. what is it included?
  3. basic howto
  4. Minimal example
  5. Let's match with component perspective
  6. Dependency Component Query Oriented



milesian/aop lets you wrap your stuartsierra/components in the same way as AOP does.

... for those who aren't familiar with AOP, it is a programming paradigme that aims to increase modularity by allowing the separation of cross-cutting concerns. Examples of cross-cutting concerns can be: applying security, logging and throwing events, and as wikipedia explains:

Logging exemplifies a crosscutting concern because a logging strategy necessarily affects every logged part of the system. Logging thereby crosscuts all logged classes and methods....

what is it included?

It includes a wrap function that works as a customization system function and specific component-matchers to calculate the-component-place where we'll apply middleware.

basic howto

To simplify AOP meanings, let's try refactoring for a while two AOP concepts to quickly understand the functionality provided.
  • the thing-to-happen = aspect/cross-cutting concern
  • the place-where-will-happen = target

So, basically to include a new thing-to-happen in your component system, you need to define the thing-to-happen and the place-where-will-happen


It's a function milddleware, very similar to common ring middleware
(defn your-fn-middleware
  [*fn* this & args]
  (let [fn-result (apply *fn* (conj args this))]


It's calculated with a defrecord-wrapper.aop/Matcher protocol implementation
(defprotocol defrecord-wrapper.aop/Matcher
  (match [this protocol function-name function-args]))

Minimal example

As you can see the options available to decide if the thing has to happen in current place are component protocol, function-name and function-argsLet's try to use this AOP stuff in a minimal example:

1. define protocols and component

(defprotocol Database
  (save-user [_ user])
  (remove-user [_ user]))

(defprotocol WebSocket
  (send [_ data]))

(defrecord YourComponent []
  (save-user [this user]
    (format "saving user: %" user ))
  (remove-user [this user]
    (format "removing user: %" user ))
  (send [this data]
    (format "sending data: %" data)))

2. define your middleware to apply (thing-to-happen)

(defn logging-middleware
  [*fn* this & args]
  (let [fn-result (apply *fn* (conj args this))]
   (println "aop-logging/ function-name:" (:function-name (meta *fn*)))

3. define your matcher (place-where-will-happen)

;; maybe you want match all your component fns protocols

(defrecord YourComponentMatcher [middleware]
  (match [this protocol function-name function-args]
    (when (contains? #{Database WebSocket} protocol))

;; or maybe you're only are interested in Database/remove-user function

(defrecord YourRefinedComponentMatcher [middleware]
  (match [this protocol function-name function-args]
    (when (and (= Database protocol) (= function-name "remove-user")))

4. wrap your system (apply conditional middleware to your components)

;;  construct your instance of SystemMap as usual
(def system-map (component/system-map :your-component (YourComponent.)))

;; Using stuartsierra customization way
(def started-system (-> system-map
                         (comp component/start 
                               #(milesian.aop/wrap % (YourRefinedComponentMatcher. logging-middleware))))))
  ;; or, if you prefer a better way to express the same
  ;; you can use milesian/BigBang
(def started-system (milesian.bigbang/expand
                     {:before-start []
                      :after-start  [[milesian.aop/wrap (YourRefinedComponentMatcher. logging-middleware)]]}))

5. try your wrapped-started-system

;;  construct your instance of SystemMap as usual
(-> started-system :your-component (send "data"))
=> repl output: aop-logging/ function-name: send

Let's match with component perspective

milesian/aop includes a Matcher implementation that uses a stuartsierra/component perspective in contrast to function and protocol perspective of matchers included on more generic tangrammer/defrecord-wrapper lib Also offers a simple "Dependency Component Query Oriented" that I found very useful to think/query the system in a component way :- in our component case is the same as straighforward way


This implementation uses the system component-id to match using its component protocols and the middleware fn to apply.

Example using previous example will match both protocols: Database and Websocket, and therefore all their related fns. Previous matchers examples used protocols and fn-names to do their works, now we are at a high level, a component level.

(milesian.aop.matchers/new-component-matcher :system system-map 
                                             :components [:your-component] 
                                             :fn logging-middleware)]                                          

Dependency Component Query Oriented

This project also contains two ComponentMatcher function constructors that let you match using a dependency component query point of view.

Let's extend our data example adding a couple of components more:

(defprotocol Greetings
  (morning [_]))

(defrecord GreetingsComponent [your-component]
  (morning [this]
    (send your-component "Morning, it's a great day here!"))

(defprotocol Connector
  (connect [_]))

(defrecord ConnectorComponent [greetings-component]
  (connect [this]
    (morning greetings-component))
And also we'll need extend our system definition
(def system-map (component/system-map   
                 :your-component (YourComponent.)
                 :greetings-components (->(GreetingsComponent.)
                                          (component/using [:your-component]))
                 :connector-component (->(ConnectorComponent.)
                                         (component/using [:connector-component]))))

ComponentTransitiveDependenciesMatcher fn constructor

new-component-transitive-dependencies-matcher uses stuartsierra/dependency transitive-dependencies to get all component dependencies for each component specified in :components [...] argument.
 :system system-map 
 :components [:your-component] 
 :fn logging-middleware)
;; it's the same as                                           

 :system system-map 
 :components [:your-component :greetings-component :connector-component] 
 :fn logging-middleware)

ComponentTransitiveDependentsMatcher fn constructor

new-component-transitive-dependents-matcher uses stuartsierra/dependency transitive-dependents to get the all dependents components for each component specified in :components [...] argument.
 :system system-map 
 :components [:connector-component] 
 :fn logging-middleware)
;; it's the same as                                           

 :system system-map 
 :components [:your-component :greetings-component :connector-component] 
 :fn logging-middleware)