Functional Composition with Dependency Injection (IN/Clojure 2016)

Functional Composition with Dependency Injection (IN/Clojure 2016)

Premise for first-class initialization stage in apps and a notion of dependency-passing approach via dependency injection for app initialization while adhering to the time tested Higher-order Functions philosophy.


Shantanu Kumar

November 26, 2016


  1. Functional Composition with Dependency Injection Shantanu Kumar (@kumarshantanu)

  2. Who am I? • Principal Engineer at Concur • Author

    of “Clojure High Performance Programming” • Open Source contributor: kumarshantanu • Using Clojure since early 2009 • @kumarshantanu on Twitter
  3. Layered design web- cart-items service- cart-items cache-fetch- cart-items db-fetch- cart-items

    cache-store- connection db-conn- pool
  4. (defn service-cart-items [user-id] (->> (cart-item-ids user-id) (mapv cache-cart-item))) Layered design:

    Code (defn web-cart-items [user-session] (service-cart-items (:user-id user-session))) (defn cache-cart-item [cache item-id] (or (cache-get cache item-id) (let [item (db-cart-item item-id)] (cache-put! cache item) item))) (defn db-cart-item [conn-pool item-id] (with-conn [conn conn-pool] (sql-fetch conn item-id)))
  5. Pause & Think • Should a layer know about the

    underlying layer’s implementation detail (cache, connection pool)? • What exactly is the “contract” between the caller and the underlying layer? Is the contract explicit? • How do you test the service layer for failures? Can failures be simulated? Can tests be run in parallel? • How easy is it to instrument or wrap an underlying layer implementation? (resilience, asynchrony etc.)
  6. Inter-layer contract • Functions (different arities for contract vs impl)

    • Protocols (usually backed by defrecord) • Multi-methods
  7. (defn service-cart-items [item-finder user-id] (->> (cart-item-ids user-id) (mapv item-finder))) Layered

    design: HoF (defn web-cart-items [service user-session] (service (:user-id user-session))) (defn cache-cart-item [db-item-finder cache item-id] (or (cache-get cache item-id) (let [item (db-item-finder item-id)] (cache-put! cache item) item))) (defn db-cart-item [conn-pool item-id] (with-conn [conn conn-pool] (sql-fetch conn item-id)))
  8. Dependency Injection • Dependency Inversion principle: “Depend upon Abstractions. Do

    not depend upon concretions.” • Creating partially applied functions… • Or, creating record (defrecord) instances • Injections cascade across layers • Boilerplate, Code maintenance, and Automation • Trade offs
  9. Friction in DI approaches • Dynamic binding • Passing the

    world • Protocol impedance mismatch • Var dissociation (clojure.core/partial) • Boilerplate and code maintenance • Incidental bootstrapping • Global mutable dependencies (clojure.core/alter-var-root)
  10. App initialization phase • First class application initialization • Decomplected

    from definition and state • Runtime split into initialization and run • Opportunity for composition, even performance optimization • Dependency Injection is well suited for initialization phase
  11. Dependency Injection Made Easy

  12. Why a library for DI/HoF? • Automate boilerplate (fns that

    make partials) • Visually distinguish dependencies vs runtime args • Pet (fns making partials) vs Cattle (using library) • Focus on business logic, not on incidental code
  13. (defn ^{:expose :item-finder} cache-cart-item [^:inject db-item-finder ^:inject cache item-id] (or

    (cache-get cache item-id) (let [item (db-item-finder item-id)] (cache-put! cache item) item))) (defn ^{:expose :db-item-finder} db-cart-item [^:inject conn-pool item-id] (with-conn [conn conn-pool] (sql-fetch conn item-id))) Layered design: DIME (defn web-cart-items [^:inject service user-session] (service (:user-id user-session))) (defn ^{:expose :service} service-cart-items [^:inject item-finder user-id] (->> user-id cart-item-ids (mapv item-finder)))
  14. DIME Injection Example (ns example-app.init (:require [dime.core :as di] [dime.var

    :as dv])) (defn resolve-deps [seed] (let [{:keys [web-cart-items]} (di/inject-all graph seed)] ;; now can call `web-cart-items` with 1 arg ...)) (def graph (dv/ns-vars->graph ['example-app.db 'example-app.cache ' 'example-app.web]))
  15. Visualization with Lein-viz

  16. Using REPL with DIME • Define vars — (def injected

    nil) (def de-init nil) • Update #’injected with all injected objects • Update #’de-init with fn to de-initialize the injected • ((get injected :expose-key) runtime-args…) • Reload support via clojure/tools.namespace
  17. DIME Advantages • Automatic generation of cascading partials • Automatic

    injection of dependencies • Declarative, hence visualization is possible • Easy to simulate dependencies for testing • Amenable to parallel testing • Can be invoked late, at initialization time
  18. DIME Challenges • Opinionated in favor of Higher-order Functions •

    Emacs M-. may not work (true for any HoF) • Exposed names should be namespaced for clarity • Automated cascading injection may be hard to grasp • CLJS support (though CLJS has reified vars/ns now) • Lack of tooling for navigating exposed names
  19. Thank You! @kumarshantanu (Twitter, Github)