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. Introducing structure with Jan Stępień @janstepien :clojureD, Berlin 2018

  2. None
  3. @cljmuc

  4. Let’s talk about structure

  5. Let’s talk about structure of web apps

  6. Architecture off the shelf

  7. There are no frameworks* * well, actually

  8. This is just one recipe

  9. Ports and adapters Hexagonal Clean

  10. @janstepien Layered structure

  11. Domain entities @janstepien

  12. Use cases and pure logic @janstepien

  13. Adapters and the outer world @janstepien

  14. @janstepien Dependencies point only inwards

  15. Booking tables

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

    │ └── book_table.clj └── adapters └── ring.clj Domain entities Use cases Adapters
  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?)
  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"}))
  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"} ...)
  20. src └── table ├── entities │ └── booking.clj ├── use_cases

    │ └── book_table.clj └── adapters └── ring.clj Domain entities Use cases Adapters
  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]))
  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))
  23. src └── table ├── entities │ └── booking.clj ├── use_cases

    │ └── book_table.clj └── adapters └── ring.clj Domain entities Use cases Adapters
  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] ...)
  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) ...))
  26. src └── table ├── entities │ └── booking.clj ├── use_cases

    │ └── book_table.clj ├── adapters │ └── ring.clj ├── system │ ├── postgres.clj │ └── http_server.clj └── system.clj @janstepien
  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)))
  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}))
  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}))
  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})))
  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
  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))))
  33. src └── table ├── entities │ └── booking.clj ├── use_cases

    │ └── book_table.clj ├── adapters │ └── ring.clj ├── system │ ├── postgres.clj │ └── http_server.clj ├── system.clj └── main.clj
  34. clojure.spec for contracts Ring and Compojure for HTTP Component and

    System for lifecycle Luminus or Duct for a framework
  35. Introducing structure with Jan Stępień @janstepien :clojureD, Berlin 2018