Introducing structure

Introducing structure

As seen at :clojureD 2018, Munich Clojurians and (clojure 'vienna) in June 2017, and Leipzig Clojure Meetup in July 2017.

Once you’ve got a couple of Clojure katas under your belt you start to wonder how to structure an entire application. Where does the logic go? Where do you respond to HTTP requests? Where to connect to the database and how to pass the connection to code which needs it? Let’s see what the best practices are and which Clojure libraries can help us.

https://www.meetup.com/Munich-Clojurians/events/238978671/

https://www.meetup.com/clojure-vienna/events/240060558/

https://www.meetup.com/Leipzig-Clojure-Meetup/events/241271037/

Ae7a42fb716793697b1d222f3cc753b8?s=128

Jan Stępień

June 06, 2017
Tweet

Transcript

  1. 2.
  2. 3.
  3. 16.

    src └── table ├── entities │ └── booking.clj ├── use_cases

    │ └── book_table.clj └── adapters └── ring.clj Domain entities Use cases Adapters
  4. 17.

    (ns table.entities.booking (:require [clojure.spec.alpha :as spec])) (spec/def ::booking (spec/keys :req-un

    [::name ::seats ::time])) (spec/def ::name string?) (spec/def ::seats (spec/and integer? pos?)) (spec/def ::time inst?)
  5. 18.

    (spec/explain ::booking {:name "რუსთაველი" :seats 0 :time #inst "2017-06-06T21:00Z"})) (spec/valid?

    ::booking {:name "Stępień" :seats 3 :time #inst "2017-06-06T21:00Z" :phone "+49876543210"}))
  6. 19.

    (gen/sample (spec/gen ::booking)) #_=> ({:name "ldeDMP9" :seats 3122 :time #inst

    "1969-12-31T23:59:59.999Z"} {:name "C5bLbvKzGeRt94" :seats 1771071 :time #inst “1970-01-01T00:04:28.571Z"} ...)
  7. 20.

    src └── table ├── entities │ └── booking.clj ├── use_cases

    │ └── book_table.clj └── adapters └── ring.clj Domain entities Use cases Adapters
  8. 21.

    (ns table.use-cases.book-table (:require [table.entities.booking :as booking] [clojure.spec.alpha :as spec])) (defn

    book-table [context booking] {:pre [(spec/valid? ::booking/booking booking)]} (save-booking (:save-booking context) booking)) (defprotocol SaveBooking (save-booking [this booking]))
  9. 22.

    (let [booking {:name "Foo Bar" :seats 2 :time (java.util.Date.)} context

    {:save-booking (reify SaveBooking (save-booking [_ booking] (prn :saved booking)))}] (book-table context booking))
  10. 23.

    src └── table ├── entities │ └── booking.clj ├── use_cases

    │ └── book_table.clj └── adapters └── ring.clj Domain entities Use cases Adapters
  11. 24.

    (ns table.adapters.ring (:require [table.use-cases.book-table :as book-table])) (defn post-booking [req] (let

    [context (:table/context req) booking (coerce-params (:params req))] (book-table/book-table context booking) {:body "Success!", :status 201)) (defn list-bookings [req] ...)
  12. 25.

    (ns table.adapters.ring (:require [compojure.core :as comp :refer [GET POST]])) (defn

    routes [] (comp/routes (POST "/bookings" [] post—bookings) (GET "/bookings" [] list—bookings))) (defn wrap-context [handler context] (fn [req] (handler (assoc req :table/context context)))) (defn handler [context] (-> (routes) (wrap-context context) ...))
  13. 26.

    src └── table ├── entities │ └── booking.clj ├── use_cases

    │ └── book_table.clj ├── adapters │ └── ring.clj ├── system │ ├── postgres.clj │ └── http_server.clj └── system.clj @janstepien
  14. 27.

    (ns table.system.postgres (:require [com.stuartsierra.component :as component])) (defrecord Postgres [config connection]

    component/Lifecycle (start [this] (assoc this :connection (connect config))) (stop [this] (close connection) (assoc this :connection nil)))
  15. 28.

    (ns table.system.postgres (:require [table.use-cases.book-table :as book-table])) (extend-protocol book-table/SaveBooking Postgres (save-booking

    [postgres booking] (execute-sql postgres "insert into bookings..." booking))) (defn postgres [config] (map->Postgres {:config config}))
  16. 29.

    (ns table.system.http-server (:require [com.stuartsierra.component :as component])) (defrecord HttpServer [handler context

    server] component/Lifecycle (start [this] (assoc this :server (start-server (handler context)))) (stop [this] (stop-server server) (assoc this :server nil))) (defn http-server [handler] (map->HttpServer {:handler handler}))
  17. 30.

    (ns table.system (:require [com.stuartsierra.component :as component] [table.system [http-server :as http]

    [postgres :as postgres]] [table.adapters.ring :as ring])) (defn system [config] (component/system-map :postgres (postgres/postgres config) :context (component/using {} {:save-booking :postgres}) :http (component/using (http/http-server ring/handler) {:context :context})))
  18. 31.

    src └── table ├── entities │ └── booking.clj ├── use_cases

    │ └── book_table.clj ├── adapters │ └── ring.clj ├── system │ ├── postgres.clj │ └── http_server.clj ├── system.clj └── main.clj @janstepien
  19. 32.

    (ns table.main (:require [com.stuartsierra.component :as component] [table.system :as system])) (defn

    -main [] (let [config {:postgres {:host "localhost" :user "foo" :password "bar"} :http {:port 8080}}] (component/start (system/system config))))
  20. 33.

    src └── table ├── entities │ └── booking.clj ├── use_cases

    │ └── book_table.clj ├── adapters │ └── ring.clj ├── system │ ├── postgres.clj │ └── http_server.clj ├── system.clj └── main.clj
  21. 34.

    clojure.spec for contracts Ring and Compojure for HTTP Component and

    System for lifecycle Luminus or Duct for a framework