SlideShare a Scribd company logo
1 of 24
Dynamic Polymorphism
                         in Clojure
                                -or-
                       How I learned to stop
                     configuring and start loving
                           multimethods

                     Scott Shaw <scottwshaw@gmail.com>


Friday, 11 May 12
Outline
                    • Intro - a bit about me
                    • The problem
                    • Approaches
                     • Late namespace binding
                     • Protocols
                     • Multimethods
Friday, 11 May 12
About Me
                    • Reformed academic
                     • wrote 1000’s of lines of bad Lisp code
                        back in the day
                    • Now, IT consultant
                     • developer, architect, manager,
                        technologist, consultant for
                        ThoughtWorks in AsiaPac


Friday, 11 May 12
The Problem




Friday, 11 May 12
The Problem
                                                                                           discogs



            (POST "/search"                                               (search query)
               [query]
                              Controller
                                           (releases-for query)
                                                                  model

                                                                          (search query)
                                                                                                     ?
                                                                                             stubs



                                 Switchable at runtime
                                    Nonintrusive
                                       Testable
                                       Generic

Friday, 11 May 12
Controller
             (ns record-nav.core
               (:use [compojure.core :only (defroutes GET POST)])
               (:require [compojure.route :as route]
                         [compojure.handler :as handler]
                         [clojure.contrib.json :as json]
                         [record-nav.model :as model]
                         [record-nav.views.layout :as layout]
                         [record-nav.views.search :as search-view]))

             (defroutes record-nav-routes
               (GET "/" [] (layout/common "Record Nav" (search-view/form "")))
               (POST "/search" [query] (let [res (model/get-releases-for query)]
                                          (search-view/results res query)))
               (route/resources "/")
               (route/not-found (layout/four-oh-four)))

             (def app (handler/site record-nav-routes))




Friday, 11 May 12
model.clj
                    (defn get-releases-for [query]
                      (do-some-munging
                        (discogs/search query)))

                              - or -
                    (defn get-releases-for [query]
                      (do-some-munging
                        (stub-data/search query)))




Friday, 11 May 12
So why not keep the
                     same function name
                    and dynamically switch
                        the namespace
                      depending on the
                         environment?
Friday, 11 May 12
with namespace binding
       config.clj
             {:online {:datasource 'record-nav.discogs}
              :offline {:datasource 'record-nav.stub-data}}

       model.clj
             (defn get-releases-for [query]
               (nsb/with-namespace-bindings [:datasource search]
                 (search query)))
     discogs.clj
               (defn search [query]
                 (let [response (execute-query query)]
                   (-> (json/read-json response)
                       :resp :search :searchresults :results)))
     stub-data.clj
              (defn search [query] sample-data)
Friday, 11 May 12
the macro
     (defmacro with-namespace-binding
       "Takes a single [namespace-keyword function] pair"
       [[namespace-id function] & body]
       (let [the-namespace (namespace-id (namespace-bindings))]
         `(do
            (when-not (find-ns '~the-namespace) (require '~the-namespace))
            (let [~function (ns-resolve '~the-namespace '~function)]
              ~@body))))




Friday, 11 May 12
Summary
                    •   Not very intrusive

                        •   Just wrap the function call in a
                             (let [:namespace function] ...) form

                    •   Don’t have to anticipate the potential
                        namespaces in the calling function (no explicit
                        require)

                    •   Testing isn’t great

                        •   tests are run after macro expansion so
                            difficult to inject behaviour without
                            configuration.
Friday, 11 May 12
Maybe we should be
                    using protocols instead?


Friday, 11 May 12
Protocols
                    • Abstraction and contracts without the OO
                      concept of inheritance
                    • Define protocols for each participant,
                      Model and DataSource.
                    • In a configuration file, define a wiring
                      pattern of datatypes for each
                      environmental option
                    • You end up with a typical OO dependency
                      injection framework (with an implicit
                      constructor function)
Friday, 11 May 12
config.clj (trees of datatypes)
       {:online ['record-nav.model ['record-nav.discogs]]
        :stubs ['record-nav.model ['record-nav.stub-data]]}
     data_source.clj
      (defprotocol DataSource
        (search [this query])
        (get-release [this release-id]))

      discogs.clj
  (defn create []
    (reify data-source/DataSource
      (search [this query]
        (let [response (client/get (str "http://api.discogs.com/search?q="
                                        (codec/url-encode query))
                                   {:as :json})]
          (-> response :body :resp :search :searchresults :results)))
      (get-release [this release-id]
        (let [response (client/get (str "http://api.discogs.com/releases"
                                        release-id)
                                   {:as :json})]
          (-> response :resp :release)))))


Friday, 11 May 12
model.clj
(defprotocol Model
  (releases-for [this query])
  (release-by-id [this release-id])
  (get-players-for [this release-id]))

(defn create
  "Creates a model object with implementations that use the data-connector
   argument"
  [data-connector]
  (reify Model
    (releases-for [this query] (data-source/search data-connector query))
    (release-by-id [this release-id] (data-source/get-release data-connector
                                                               release-id))
    (get-players-for [this release-id]
      (let [release (release-by-id this release-id)
             artist-uris (map :resource_url (:artists release))]
        (mapcat retrieve-artists artist-uris)))))


     stub_data.clj
 (defn create []
   (reify
     data-source/DataSource
     (search [this query] sample-response)
     (get-release [this release-id] release-data)))

Friday, 11 May 12
core.clj (the controller)
   (let [the-model (loom/get-wired nil)]
     (defroutes record-nav-routes
       (GET "/" [] (layout/common "Record Nav"
                                  (search-view/form "")))
       (POST "/search" [query]
         (let [res (model/releases-for the-model query)]
           (search-view/results res query)))
       (route/resources "/")
       (route/not-found (layout/four-oh-four))))


 loom.clj (the macro)
    (defmacro get-wired [pattern-key]
      (let [[ns-1 [ns-2]] (get-wiring-pattern pattern-key)]
        `(do
           (when-not (find-ns '~ns-1) (require '~ns-1))
           (when-not (find-ns '~ns-2) (require '~ns-2))
           (let [c1# (ns-resolve '~ns-1 '~'create)
                 c2# (ns-resolve '~ns-2 '~'create)]
             (c1# (c2#))))))


Friday, 11 May 12
Testing
(defrecord TestArtistData []
  data-source/DataSource
  (search [this query] nil)
  (get-release [this release-id]
               {:artists [{:resource_url "testurl-1"}
                          {:resource_url "testurl-2"}]}))

(let [model-under-test (model/create (TestArtistData.))]
  (fact "for a release with multiple artists,
         should concatenate all artist's info"
    (get-players-for model-under-test anything) =>
      (contains *ron-wood-guitar* *charlie-watts*
                :in-any-order :gaps-ok)
    (provided (model/retrieve-artists "testurl-1") =>
                [*ron-wood-guitar* *ron-wood-bass*]
              (retrieve-artists "testurl-2") =>
                [*charlie-watts*])))


Friday, 11 May 12
Summary
                    • Heading toward a typical OO interface-
                      driven dependency-injection framework
                      • Am I rewriting Spring in Clojure?
                    • Fairly intrusive in that it requires a protocol
                      to be defined for each participant
                    • Nice ability to inject behaviour in tests
                    • Still hard to test the framework itself
Friday, 11 May 12
OK, I suppose I should
                     try out multimethods
                    since it is the “official”
                      Clojure approach to
                    runtime polymorphism

Friday, 11 May 12
multimethods
                    • Use defmulti to declare a method name
                      and a function to be used for dispatch
                    • Use defmethod to define functions for each
                      possible dispatch value
                    • In our case...
                     • define a generic set of multimethods that
                        a data-source must implement
                     • create new data sources by defmethods
                     • the target environment is encoded in the
                        method implementation
Friday, 11 May 12
data_source.clj (the abstraction)
      (defn app-env [& args]
        (keyword (get (System/getenv) "APP_ENV")))

      (defmulti search #'app-env :default :online)
      (defmulti get-release #'app-env :default :online)

      stub_data.clj
      (defmethod data-source/search :stubs [query] sample-response)
      (defmethod data-source/get-release :stubs [release-id] release-data)

    discogs.clj
      (defmethod data-source/search :online [query]
        (let [response (client/get
                        (str "http://api.discogs.com/search?q="
                             (codec/url-encode query))
                        {:as :json})]
          (-> response :body :resp :search :searchresults :results)))

      (defmethod data-source/get-release :online [release-id]
        (let [response (client/get
                        (str "http://api.discogs.com/releases"
                             release-id)
                        {:as :json})]
          (-> response :resp :release)))
Friday, 11 May 12
Testing
  (defmethod data-source/search :test [query] sample-release-data)

  (fact
    (model/releases-for anything) => (has every?
                                          contains-title-and-uri-strings?)
    (provided
      (data-source/app-env anything) => :test))




Friday, 11 May 12
Summary
                • Least code of all the solutions by far
                • True runtime polymorphism
                 • No ugly macros
                • Easy to inject behaviour for tests
                • No configuration!
                • Model must be aware of implementations
                    to a certain extent
                    • (require ‘[all possible namespaces that
                      might be dispatched])

Friday, 11 May 12
The End



Friday, 11 May 12

More Related Content

What's hot

4java Basic Syntax
4java Basic Syntax4java Basic Syntax
4java Basic Syntax
Adil Jafri
 
Refactoring In Tdd The Missing Part
Refactoring In Tdd The Missing PartRefactoring In Tdd The Missing Part
Refactoring In Tdd The Missing Part
Gabriele Lana
 

What's hot (20)

Requery overview
Requery overviewRequery overview
Requery overview
 
Clojure Intro
Clojure IntroClojure Intro
Clojure Intro
 
Fun with Functional Programming in Clojure
Fun with Functional Programming in ClojureFun with Functional Programming in Clojure
Fun with Functional Programming in Clojure
 
Simplifying Persistence for Java and MongoDB with Morphia
Simplifying Persistence for Java and MongoDB with MorphiaSimplifying Persistence for Java and MongoDB with Morphia
Simplifying Persistence for Java and MongoDB with Morphia
 
4java Basic Syntax
4java Basic Syntax4java Basic Syntax
4java Basic Syntax
 
04 sorting
04 sorting04 sorting
04 sorting
 
Zend Framework 1 + Doctrine 2
Zend Framework 1 + Doctrine 2Zend Framework 1 + Doctrine 2
Zend Framework 1 + Doctrine 2
 
Hjelp, vi skal kode funksjonelt i Java!
Hjelp, vi skal kode funksjonelt i Java!Hjelp, vi skal kode funksjonelt i Java!
Hjelp, vi skal kode funksjonelt i Java!
 
Elementary Sort
Elementary SortElementary Sort
Elementary Sort
 
A brief tour of modern Java
A brief tour of modern JavaA brief tour of modern Java
A brief tour of modern Java
 
MongoDB: Easy Java Persistence with Morphia
MongoDB: Easy Java Persistence with MorphiaMongoDB: Easy Java Persistence with Morphia
MongoDB: Easy Java Persistence with Morphia
 
MongoDB + Java - Everything you need to know
MongoDB + Java - Everything you need to know MongoDB + Java - Everything you need to know
MongoDB + Java - Everything you need to know
 
Java 5 and 6 New Features
Java 5 and 6 New FeaturesJava 5 and 6 New Features
Java 5 and 6 New Features
 
JUnit5 and TestContainers
JUnit5 and TestContainersJUnit5 and TestContainers
JUnit5 and TestContainers
 
Cloudera Impala, updated for v1.0
Cloudera Impala, updated for v1.0Cloudera Impala, updated for v1.0
Cloudera Impala, updated for v1.0
 
Kotlin @ Coupang Backend 2017
Kotlin @ Coupang Backend 2017Kotlin @ Coupang Backend 2017
Kotlin @ Coupang Backend 2017
 
Kotlin @ Coupang Backed - JetBrains Day seoul 2018
Kotlin @ Coupang Backed - JetBrains Day seoul 2018Kotlin @ Coupang Backed - JetBrains Day seoul 2018
Kotlin @ Coupang Backed - JetBrains Day seoul 2018
 
Refactoring In Tdd The Missing Part
Refactoring In Tdd The Missing PartRefactoring In Tdd The Missing Part
Refactoring In Tdd The Missing Part
 
Out ofmemoryerror what is the cost of java objects
Out ofmemoryerror  what is the cost of java objectsOut ofmemoryerror  what is the cost of java objects
Out ofmemoryerror what is the cost of java objects
 
Postgresql search demystified
Postgresql search demystifiedPostgresql search demystified
Postgresql search demystified
 

Similar to Dynamic poly-preso

PHP Development With MongoDB
PHP Development With MongoDBPHP Development With MongoDB
PHP Development With MongoDB
Fitz Agard
 
PHP Development with MongoDB (Fitz Agard)
PHP Development with MongoDB (Fitz Agard)PHP Development with MongoDB (Fitz Agard)
PHP Development with MongoDB (Fitz Agard)
MongoSF
 
JRuby + Rails = Awesome Java Web Framework at Jfokus 2011
JRuby + Rails = Awesome Java Web Framework at Jfokus 2011JRuby + Rails = Awesome Java Web Framework at Jfokus 2011
JRuby + Rails = Awesome Java Web Framework at Jfokus 2011
Nick Sieger
 
Automated testing with OffScale and MongoDB
Automated testing with OffScale and MongoDBAutomated testing with OffScale and MongoDB
Automated testing with OffScale and MongoDB
Omer Gertel
 
The Solar Framework for PHP
The Solar Framework for PHPThe Solar Framework for PHP
The Solar Framework for PHP
ConFoo
 
Microsoft PowerPoint - &lt;b>jQuery&lt;/b>-1-Ajax.pptx
Microsoft PowerPoint - &lt;b>jQuery&lt;/b>-1-Ajax.pptxMicrosoft PowerPoint - &lt;b>jQuery&lt;/b>-1-Ajax.pptx
Microsoft PowerPoint - &lt;b>jQuery&lt;/b>-1-Ajax.pptx
tutorialsruby
 
&lt;img src="../i/r_14.png" />
&lt;img src="../i/r_14.png" />&lt;img src="../i/r_14.png" />
&lt;img src="../i/r_14.png" />
tutorialsruby
 

Similar to Dynamic poly-preso (20)

Clojure made-simple - John Stevenson
Clojure made-simple - John StevensonClojure made-simple - John Stevenson
Clojure made-simple - John Stevenson
 
PHP Development With MongoDB
PHP Development With MongoDBPHP Development With MongoDB
PHP Development With MongoDB
 
PHP Development with MongoDB (Fitz Agard)
PHP Development with MongoDB (Fitz Agard)PHP Development with MongoDB (Fitz Agard)
PHP Development with MongoDB (Fitz Agard)
 
From Java to Parellel Clojure - Clojure South 2019
From Java to Parellel Clojure - Clojure South 2019From Java to Parellel Clojure - Clojure South 2019
From Java to Parellel Clojure - Clojure South 2019
 
JRuby + Rails = Awesome Java Web Framework at Jfokus 2011
JRuby + Rails = Awesome Java Web Framework at Jfokus 2011JRuby + Rails = Awesome Java Web Framework at Jfokus 2011
JRuby + Rails = Awesome Java Web Framework at Jfokus 2011
 
APOC Pearls - Whirlwind Tour Through the Neo4j APOC Procedures Library
APOC Pearls - Whirlwind Tour Through the Neo4j APOC Procedures LibraryAPOC Pearls - Whirlwind Tour Through the Neo4j APOC Procedures Library
APOC Pearls - Whirlwind Tour Through the Neo4j APOC Procedures Library
 
Clojure/conj 2017
Clojure/conj 2017Clojure/conj 2017
Clojure/conj 2017
 
Exploring Clojurescript
Exploring ClojurescriptExploring Clojurescript
Exploring Clojurescript
 
Connecting the Worlds of Java and Ruby with JRuby
Connecting the Worlds of Java and Ruby with JRubyConnecting the Worlds of Java and Ruby with JRuby
Connecting the Worlds of Java and Ruby with JRuby
 
Automated testing with OffScale and MongoDB
Automated testing with OffScale and MongoDBAutomated testing with OffScale and MongoDB
Automated testing with OffScale and MongoDB
 
Groovy On Trading Desk (2010)
Groovy On Trading Desk (2010)Groovy On Trading Desk (2010)
Groovy On Trading Desk (2010)
 
Lobos Introduction
Lobos IntroductionLobos Introduction
Lobos Introduction
 
Building node.js applications with Database Jones
Building node.js applications with Database JonesBuilding node.js applications with Database Jones
Building node.js applications with Database Jones
 
The Solar Framework for PHP
The Solar Framework for PHPThe Solar Framework for PHP
The Solar Framework for PHP
 
Microsoft PowerPoint - &lt;b>jQuery&lt;/b>-1-Ajax.pptx
Microsoft PowerPoint - &lt;b>jQuery&lt;/b>-1-Ajax.pptxMicrosoft PowerPoint - &lt;b>jQuery&lt;/b>-1-Ajax.pptx
Microsoft PowerPoint - &lt;b>jQuery&lt;/b>-1-Ajax.pptx
 
jQuery-1-Ajax
jQuery-1-AjaxjQuery-1-Ajax
jQuery-1-Ajax
 
&lt;img src="../i/r_14.png" />
&lt;img src="../i/r_14.png" />&lt;img src="../i/r_14.png" />
&lt;img src="../i/r_14.png" />
 
jQuery-1-Ajax
jQuery-1-AjaxjQuery-1-Ajax
jQuery-1-Ajax
 
Python mongo db-training-europython-2011
Python mongo db-training-europython-2011Python mongo db-training-europython-2011
Python mongo db-training-europython-2011
 
Oscon Java Testing on the Fast Lane
Oscon Java Testing on the Fast LaneOscon Java Testing on the Fast Lane
Oscon Java Testing on the Fast Lane
 

Recently uploaded

Histor y of HAM Radio presentation slide
Histor y of HAM Radio presentation slideHistor y of HAM Radio presentation slide
Histor y of HAM Radio presentation slide
vu2urc
 
IAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI SolutionsIAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI Solutions
Enterprise Knowledge
 

Recently uploaded (20)

Boost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdfBoost Fertility New Invention Ups Success Rates.pdf
Boost Fertility New Invention Ups Success Rates.pdf
 
Scaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organizationScaling API-first – The story of a global engineering organization
Scaling API-first – The story of a global engineering organization
 
Understanding Discord NSFW Servers A Guide for Responsible Users.pdf
Understanding Discord NSFW Servers A Guide for Responsible Users.pdfUnderstanding Discord NSFW Servers A Guide for Responsible Users.pdf
Understanding Discord NSFW Servers A Guide for Responsible Users.pdf
 
2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...2024: Domino Containers - The Next Step. News from the Domino Container commu...
2024: Domino Containers - The Next Step. News from the Domino Container commu...
 
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
Apidays Singapore 2024 - Building Digital Trust in a Digital Economy by Veron...
 
Data Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt RobisonData Cloud, More than a CDP by Matt Robison
Data Cloud, More than a CDP by Matt Robison
 
The 7 Things I Know About Cyber Security After 25 Years | April 2024
The 7 Things I Know About Cyber Security After 25 Years | April 2024The 7 Things I Know About Cyber Security After 25 Years | April 2024
The 7 Things I Know About Cyber Security After 25 Years | April 2024
 
Histor y of HAM Radio presentation slide
Histor y of HAM Radio presentation slideHistor y of HAM Radio presentation slide
Histor y of HAM Radio presentation slide
 
IAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI SolutionsIAC 2024 - IA Fast Track to Search Focused AI Solutions
IAC 2024 - IA Fast Track to Search Focused AI Solutions
 
Handwritten Text Recognition for manuscripts and early printed texts
Handwritten Text Recognition for manuscripts and early printed textsHandwritten Text Recognition for manuscripts and early printed texts
Handwritten Text Recognition for manuscripts and early printed texts
 
Evaluating the top large language models.pdf
Evaluating the top large language models.pdfEvaluating the top large language models.pdf
Evaluating the top large language models.pdf
 
How to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected WorkerHow to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected Worker
 
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...
Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...Workshop - Best of Both Worlds_ Combine  KG and Vector search for  enhanced R...
Workshop - Best of Both Worlds_ Combine KG and Vector search for enhanced R...
 
presentation ICT roal in 21st century education
presentation ICT roal in 21st century educationpresentation ICT roal in 21st century education
presentation ICT roal in 21st century education
 
Exploring the Future Potential of AI-Enabled Smartphone Processors
Exploring the Future Potential of AI-Enabled Smartphone ProcessorsExploring the Future Potential of AI-Enabled Smartphone Processors
Exploring the Future Potential of AI-Enabled Smartphone Processors
 
08448380779 Call Girls In Civil Lines Women Seeking Men
08448380779 Call Girls In Civil Lines Women Seeking Men08448380779 Call Girls In Civil Lines Women Seeking Men
08448380779 Call Girls In Civil Lines Women Seeking Men
 
08448380779 Call Girls In Friends Colony Women Seeking Men
08448380779 Call Girls In Friends Colony Women Seeking Men08448380779 Call Girls In Friends Colony Women Seeking Men
08448380779 Call Girls In Friends Colony Women Seeking Men
 
How to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected WorkerHow to Troubleshoot Apps for the Modern Connected Worker
How to Troubleshoot Apps for the Modern Connected Worker
 
A Domino Admins Adventures (Engage 2024)
A Domino Admins Adventures (Engage 2024)A Domino Admins Adventures (Engage 2024)
A Domino Admins Adventures (Engage 2024)
 
04-2024-HHUG-Sales-and-Marketing-Alignment.pptx
04-2024-HHUG-Sales-and-Marketing-Alignment.pptx04-2024-HHUG-Sales-and-Marketing-Alignment.pptx
04-2024-HHUG-Sales-and-Marketing-Alignment.pptx
 

Dynamic poly-preso

  • 1. Dynamic Polymorphism in Clojure -or- How I learned to stop configuring and start loving multimethods Scott Shaw <scottwshaw@gmail.com> Friday, 11 May 12
  • 2. Outline • Intro - a bit about me • The problem • Approaches • Late namespace binding • Protocols • Multimethods Friday, 11 May 12
  • 3. About Me • Reformed academic • wrote 1000’s of lines of bad Lisp code back in the day • Now, IT consultant • developer, architect, manager, technologist, consultant for ThoughtWorks in AsiaPac Friday, 11 May 12
  • 5. The Problem discogs (POST "/search" (search query) [query] Controller (releases-for query) model (search query) ? stubs Switchable at runtime Nonintrusive Testable Generic Friday, 11 May 12
  • 6. Controller (ns record-nav.core (:use [compojure.core :only (defroutes GET POST)]) (:require [compojure.route :as route] [compojure.handler :as handler] [clojure.contrib.json :as json] [record-nav.model :as model] [record-nav.views.layout :as layout] [record-nav.views.search :as search-view])) (defroutes record-nav-routes (GET "/" [] (layout/common "Record Nav" (search-view/form ""))) (POST "/search" [query] (let [res (model/get-releases-for query)] (search-view/results res query))) (route/resources "/") (route/not-found (layout/four-oh-four))) (def app (handler/site record-nav-routes)) Friday, 11 May 12
  • 7. model.clj (defn get-releases-for [query] (do-some-munging (discogs/search query))) - or - (defn get-releases-for [query] (do-some-munging (stub-data/search query))) Friday, 11 May 12
  • 8. So why not keep the same function name and dynamically switch the namespace depending on the environment? Friday, 11 May 12
  • 9. with namespace binding config.clj {:online {:datasource 'record-nav.discogs} :offline {:datasource 'record-nav.stub-data}} model.clj (defn get-releases-for [query] (nsb/with-namespace-bindings [:datasource search] (search query))) discogs.clj (defn search [query] (let [response (execute-query query)] (-> (json/read-json response) :resp :search :searchresults :results))) stub-data.clj (defn search [query] sample-data) Friday, 11 May 12
  • 10. the macro (defmacro with-namespace-binding "Takes a single [namespace-keyword function] pair" [[namespace-id function] & body] (let [the-namespace (namespace-id (namespace-bindings))] `(do (when-not (find-ns '~the-namespace) (require '~the-namespace)) (let [~function (ns-resolve '~the-namespace '~function)] ~@body)))) Friday, 11 May 12
  • 11. Summary • Not very intrusive • Just wrap the function call in a (let [:namespace function] ...) form • Don’t have to anticipate the potential namespaces in the calling function (no explicit require) • Testing isn’t great • tests are run after macro expansion so difficult to inject behaviour without configuration. Friday, 11 May 12
  • 12. Maybe we should be using protocols instead? Friday, 11 May 12
  • 13. Protocols • Abstraction and contracts without the OO concept of inheritance • Define protocols for each participant, Model and DataSource. • In a configuration file, define a wiring pattern of datatypes for each environmental option • You end up with a typical OO dependency injection framework (with an implicit constructor function) Friday, 11 May 12
  • 14. config.clj (trees of datatypes) {:online ['record-nav.model ['record-nav.discogs]] :stubs ['record-nav.model ['record-nav.stub-data]]} data_source.clj (defprotocol DataSource (search [this query]) (get-release [this release-id])) discogs.clj (defn create [] (reify data-source/DataSource (search [this query] (let [response (client/get (str "http://api.discogs.com/search?q=" (codec/url-encode query)) {:as :json})] (-> response :body :resp :search :searchresults :results))) (get-release [this release-id] (let [response (client/get (str "http://api.discogs.com/releases" release-id) {:as :json})] (-> response :resp :release))))) Friday, 11 May 12
  • 15. model.clj (defprotocol Model (releases-for [this query]) (release-by-id [this release-id]) (get-players-for [this release-id])) (defn create "Creates a model object with implementations that use the data-connector argument" [data-connector] (reify Model (releases-for [this query] (data-source/search data-connector query)) (release-by-id [this release-id] (data-source/get-release data-connector release-id)) (get-players-for [this release-id] (let [release (release-by-id this release-id) artist-uris (map :resource_url (:artists release))] (mapcat retrieve-artists artist-uris))))) stub_data.clj (defn create [] (reify data-source/DataSource (search [this query] sample-response) (get-release [this release-id] release-data))) Friday, 11 May 12
  • 16. core.clj (the controller) (let [the-model (loom/get-wired nil)] (defroutes record-nav-routes (GET "/" [] (layout/common "Record Nav" (search-view/form ""))) (POST "/search" [query] (let [res (model/releases-for the-model query)] (search-view/results res query))) (route/resources "/") (route/not-found (layout/four-oh-four)))) loom.clj (the macro) (defmacro get-wired [pattern-key] (let [[ns-1 [ns-2]] (get-wiring-pattern pattern-key)] `(do (when-not (find-ns '~ns-1) (require '~ns-1)) (when-not (find-ns '~ns-2) (require '~ns-2)) (let [c1# (ns-resolve '~ns-1 '~'create) c2# (ns-resolve '~ns-2 '~'create)] (c1# (c2#)))))) Friday, 11 May 12
  • 17. Testing (defrecord TestArtistData [] data-source/DataSource (search [this query] nil) (get-release [this release-id] {:artists [{:resource_url "testurl-1"} {:resource_url "testurl-2"}]})) (let [model-under-test (model/create (TestArtistData.))] (fact "for a release with multiple artists, should concatenate all artist's info" (get-players-for model-under-test anything) => (contains *ron-wood-guitar* *charlie-watts* :in-any-order :gaps-ok) (provided (model/retrieve-artists "testurl-1") => [*ron-wood-guitar* *ron-wood-bass*] (retrieve-artists "testurl-2") => [*charlie-watts*]))) Friday, 11 May 12
  • 18. Summary • Heading toward a typical OO interface- driven dependency-injection framework • Am I rewriting Spring in Clojure? • Fairly intrusive in that it requires a protocol to be defined for each participant • Nice ability to inject behaviour in tests • Still hard to test the framework itself Friday, 11 May 12
  • 19. OK, I suppose I should try out multimethods since it is the “official” Clojure approach to runtime polymorphism Friday, 11 May 12
  • 20. multimethods • Use defmulti to declare a method name and a function to be used for dispatch • Use defmethod to define functions for each possible dispatch value • In our case... • define a generic set of multimethods that a data-source must implement • create new data sources by defmethods • the target environment is encoded in the method implementation Friday, 11 May 12
  • 21. data_source.clj (the abstraction) (defn app-env [& args] (keyword (get (System/getenv) "APP_ENV"))) (defmulti search #'app-env :default :online) (defmulti get-release #'app-env :default :online) stub_data.clj (defmethod data-source/search :stubs [query] sample-response) (defmethod data-source/get-release :stubs [release-id] release-data) discogs.clj (defmethod data-source/search :online [query] (let [response (client/get (str "http://api.discogs.com/search?q=" (codec/url-encode query)) {:as :json})] (-> response :body :resp :search :searchresults :results))) (defmethod data-source/get-release :online [release-id] (let [response (client/get (str "http://api.discogs.com/releases" release-id) {:as :json})] (-> response :resp :release))) Friday, 11 May 12
  • 22. Testing (defmethod data-source/search :test [query] sample-release-data) (fact (model/releases-for anything) => (has every? contains-title-and-uri-strings?) (provided (data-source/app-env anything) => :test)) Friday, 11 May 12
  • 23. Summary • Least code of all the solutions by far • True runtime polymorphism • No ugly macros • Easy to inject behaviour for tests • No configuration! • Model must be aware of implementations to a certain extent • (require ‘[all possible namespaces that might be dispatched]) Friday, 11 May 12