Introducing structure with Jan Stępień @janstepien
 janstepien.com
@innoQ @cljmuc
Functional
 programming Operations, cont. delivery Generative testing Software architecture
Functional
 programming Operations, cont. delivery Generative testing Software architecture
Let’s talk about structure
Let’s talk about structure of web apps
There are no frameworks*
This is just one recipe
Ports and adapters or Hexagonal
 architecture or Clean architecture
Layered structure
Domain entities
Use cases
Adapters and the outer world
Dependencies point only inwards
Booking tables
src └── table ├── entities │   └── booking.clj ├── use_cases │ └── book_table.clj └── adapters    └── ring.clj
src └── table ├── entities │   └── booking.clj ├── use_cases │ └── book_table.clj └── adapters    └── ring.clj Domain entities Use cases Adapters
(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 #(instance? java.util.Date %))
(spec/explain ::booking {:name "" :seats 0 :time #inst “2017-06-06T21:00Z"})) (spec/valid? ::booking {:name "Stępień" :seats 3 :phone "+49876543210" :time #inst "2017-06-06T21:00Z"}))
src └── table ├── entities │   └── booking.clj ├── use_cases │ └── book_table.clj └── adapters    └── ring.clj Domain entities Use cases Adapters
(ns table.use-cases.book-table (:require [table.entities.booking :as booking] [clojure.spec.alpha :as spec])) (defn book-table [context booking] (save-booking (get context :save-booking) booking))
(ns table.use-cases.book-table (:require [table.entities.booking :as booking] [clojure.spec.alpha :as spec])) (defn book-table [context booking] (save-booking (get context :save-booking) booking)) (defprotocol SaveBooking (save-booking [this booking]))
(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 (get context :save-booking) booking)) (defprotocol SaveBooking (save-booking [this booking]))
(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))
src └── table ├── entities │   └── booking.clj ├── use_cases │ └── book_table.clj └── adapters    └── ring.clj Domain entities Use cases Adapters
(ns table.adapters.ring (:require [table.use-cases.book-table :as book-table])) (defn post-booking [context req] (let [booking (coerce-params (get req :params))] (book-table/book-table context booking) "Success!")) (defn list-bookings [context req] ...)
(ns table.adapters.ring (:require [compojure.core :as compojure :refer [GET POST]] [ring.middleware.params :as params])) (defn routes [context] (compojure/routes (GET "/bookings" [] (fn [req] (list-bookings context req))) (POST "/bookings" [] (fn [req] (post-booking context req))))) (defn handler [context] (params/wrap-params (routes context)))
src └── table ├── entities │   └── booking.clj ├── use_cases │ └── book_table.clj └── adapters    └── ring.clj Something is clearly missing
src └── table ├── entities │   └── booking.clj ├── use_cases │ └── book_table.clj ├── adapters │   └── ring.clj ├── system.clj └── system    ├── postgres.clj    └── http_server.clj
(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)))
(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 "insert into bookings..." booking))) (defn postgres [config] (map->Postgres {:config config}))
(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}))
(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})))
src └── table ├── entities │   └── booking.clj ├── use_cases │ └── book_table.clj ├── adapters │   └── ring.clj ├── system.clj └── system    ├── postgres.clj    └── http_server.clj
src └── table ├── entities │   └── booking.clj ├── use_cases │ └── book_table.clj ├── adapters │   └── ring.clj ├── system.clj ├── system │   ├── postgres.clj │   └── http_server.clj └── main.clj
(ns table.main (:require [com.stuartsierra.component :as component] [table.system :as system])) (defn -main [] (let [config {:postgres {:host "localhost" :user "foo" :password "bar"}}] (component/start (system/system config))))
src └── table ├── entities │   └── booking.clj ├── use_cases │ └── book_table.clj ├── adapters │   └── ring.clj ├── system.clj ├── system │   ├── postgres.clj │   └── http_server.clj └── main.clj
clojure.spec for contracts Compojure and ring-core for HTTP Component and System for lifecycle Luminus or Duct for a framework
Introducing structure with Jan Stępień @janstepien
 janstepien.com
© 2017 Jan Stępień, licensed under terms of CC BY-NC-SA 3.0

Jan Stepien - Introducing structure in Clojure - Codemotion Milan 2017