|
| 1 | +(ns examples |
| 2 | + (:require [com.stuartsierra.component :as component])) |
| 3 | + |
| 4 | +;;; Dummy functions to use in the examples |
| 5 | + |
| 6 | +(defn connect-to-database [host port] |
| 7 | + (prn "Opening database connection") |
| 8 | + (reify java.io.Closeable |
| 9 | + (close [_] (prn "Closing database connection")))) |
| 10 | + |
| 11 | +(defn execute-query [& _] |
| 12 | + (prn 'execute-query)) |
| 13 | + |
| 14 | +(defn execute-insert [& _] |
| 15 | + (prn 'execute-insert)) |
| 16 | + |
| 17 | +(defn new-scheduler [] |
| 18 | + (prn 'new-scheduler) |
| 19 | + {}) |
| 20 | + |
| 21 | + |
| 22 | +;;; Example database component |
| 23 | + |
| 24 | +;; To define a component, define a Clojure record that implements the |
| 25 | +;; `Lifecycle` protocol. |
| 26 | + |
| 27 | +(defrecord Database [host port connection] |
| 28 | + ;; Implement the Lifecycle protocol |
| 29 | + component/Lifecycle |
| 30 | + |
| 31 | + (start [component] |
| 32 | + ;; In the 'start' method, initialize this component |
| 33 | + ;; and start it running. For example, connect to a |
| 34 | + ;; database, create thread pools, or initialize shared |
| 35 | + ;; state. |
| 36 | + (let [conn (connect-to-database host port)] |
| 37 | + ;; Return an updated version of the component with |
| 38 | + ;; the run-time state assoc'd in. |
| 39 | + (assoc component :connection conn))) |
| 40 | + |
| 41 | + (stop [component] |
| 42 | + ;; In the 'stop' method, shut down the running |
| 43 | + ;; component and release any external resources it has |
| 44 | + ;; acquired. |
| 45 | + (.close connection) |
| 46 | + ;; Return the component, optionally modified. |
| 47 | + component)) |
| 48 | + |
| 49 | +;; Optionally, provide a constructor function that takes in |
| 50 | +;; the essential configuration parameters of the component, |
| 51 | +;; leaving the runtime state blank. |
| 52 | + |
| 53 | +(defn new-database [host port] |
| 54 | + (map->Database {:host host :port port})) |
| 55 | + |
| 56 | +;; Define the functions implementing the behavior of the |
| 57 | +;; component to take the component itself as an argument. |
| 58 | + |
| 59 | +(defn get-user [database username] |
| 60 | + (execute-query (:connection database) |
| 61 | + "SELECT * FROM users WHERE username = ?" |
| 62 | + username)) |
| 63 | + |
| 64 | +(defn add-user [database username favorite-color] |
| 65 | + (execute-insert (:connection database) |
| 66 | + "INSERT INTO users (username, favorite_color)" |
| 67 | + username favorite-color)) |
| 68 | + |
| 69 | + |
| 70 | +;;; Second Example Component |
| 71 | + |
| 72 | +;; Define other components in terms of the components on which they |
| 73 | +;; depend. |
| 74 | + |
| 75 | +(defrecord ExampleComponent [options cache database scheduler] |
| 76 | + component/Lifecycle |
| 77 | + |
| 78 | + (start [this] |
| 79 | + ;; In the 'start' method, a component may assume that its |
| 80 | + ;; dependencies are available and have already been started. |
| 81 | + (assoc this :admin (get-user database "admin"))) |
| 82 | + |
| 83 | + (stop [this] |
| 84 | + ;; Likewise, in the 'stop' method, a component may assume that its |
| 85 | + ;; dependencies will not be stopped until AFTER it is stopped. |
| 86 | + this)) |
| 87 | + |
| 88 | +;; Not all the dependencies need to be supplied at construction time. |
| 89 | +;; In general, the constructor should not depend on other components |
| 90 | +;; being available or started. |
| 91 | + |
| 92 | +(defn example-component [config-options] |
| 93 | + (map->ExampleComponent {:options config-options |
| 94 | + :cache (atom {})})) |
| 95 | + |
| 96 | + |
| 97 | +;;; Example System |
| 98 | + |
| 99 | +;; Components are composed into systems. A system is a component which |
| 100 | +;; knows how to start and stop other components. |
| 101 | + |
| 102 | +;; A system can use the helper functions `start-system` and `stop-system`, |
| 103 | +;; which take a set of keys naming components in the system to be |
| 104 | +;; started/stopped. Order of the keys doesn't matter here. |
| 105 | + |
| 106 | +(defrecord ExampleSystem [config-options db scheduler app] |
| 107 | + component/Lifecycle |
| 108 | + (start [this] |
| 109 | + (component/start-system this [:scheduler :app :db])) |
| 110 | + (stop [this] |
| 111 | + (component/stop-system this [:scheduler :app :db]))) |
| 112 | + |
| 113 | +;; When constructing the system, specify the dependency relationships |
| 114 | +;; among components using the `using` function. |
| 115 | + |
| 116 | +(defn example-system [config-options] |
| 117 | + (let [{:keys [host port]} config-options] |
| 118 | + (map->ExampleSystem |
| 119 | + {:config-options config-options |
| 120 | + :db (new-database host port) |
| 121 | + :scheduler (new-scheduler) |
| 122 | + :app (component/using |
| 123 | + (example-component config-options) |
| 124 | + {:database :db |
| 125 | + :scheduler :scheduler})}))) |
| 126 | +;; ^ ^ |
| 127 | +;; | | |
| 128 | +;; | \- Keys in the ExampleSystem record |
| 129 | +;; | |
| 130 | +;; \- Keys in the ExampleComponent record |
| 131 | + |
| 132 | +;; `using` takes a component and a map telling the system where to |
| 133 | +;; find that component's dependencies. Keys in the map are the keys in |
| 134 | +;; the component record itself, values are the map are the |
| 135 | +;; corresponding keys in the system record. |
| 136 | + |
| 137 | +;; Based on this information (stored as metadata on the component |
| 138 | +;; records) the `start-system` function will construct a dependency |
| 139 | +;; graph of the components, assoc in their dependencies, and start |
| 140 | +;; them all in the correct order. |
| 141 | + |
| 142 | +;; Optionally, if the keys in the system map are the same as in the |
| 143 | +;; component map, they may be passed as a vector to `using`. If you |
| 144 | +;; know all the dependencies in advance, you may even add the metadata |
| 145 | +;; in the component's constructor: |
| 146 | + |
| 147 | +(defrecord AnotherComponent [component-a component-b]) |
| 148 | + |
| 149 | +(defn another-component [] |
| 150 | + (component/using |
| 151 | + (->AnotherComponent nil nil) |
| 152 | + [:component-a :component-b])) |
| 153 | + |
| 154 | +(defrecord AnotherSystem [component-a component-b component-c]) |
| 155 | + |
| 156 | + |
| 157 | + |
| 158 | + |
| 159 | +;; Local Variables: |
| 160 | +;; clojure-defun-style-default-indent: t |
| 161 | +;; End: |
0 commit comments