[230312] clojure port-adapter architecture
Table of Contents
1 Motivation
I've read a blog post abstract clojure
. and I am really impressed.
If I want to my clojure service more maintainable, I should use this pattern.
in the blog post, the code is contained but it is snippet. I want to see the whole code. so I make my own version of the code.
2 Code
first of all we need deps.edn
.
{:path ["src"] :deps {com.github.seancorfield/next.jdbc {:mvn/version "1.3.847"} org.clojure/java.jdbc {:mvn/version "0.7.12"} io.pedestal/pedestal.service {:mvn/version "0.5.7"} io.pedestal/pedestal.route {:mvn/version "0.5.7"} io.pedestal/pedestal.jetty {:mvn/version "0.5.7"} org.clojure/data.json {:mvn/version "0.2.6"} org.slf4j/slf4j-simple {:mvn/version "1.7.28"} org.xerial/sqlite-jdbc {:mvn/version "3.41.0.0"} integrant/integrant {:mvn/version "0.8.0"}}}
and we have a db
, web
, system
2.1 db
database have two part. interface
, implementation
(ns demo1.review-repository) (defprotocol ReviewRepository (get-review-by-id [_ id]))
and demo1.db
is an implementation of ReviewRepository
.
(ns demo1.db (:require [next.jdbc :as jdbc] [demo1.review-repository :refer [ReviewRepository]])) (defrecord SqliteReviewRepository [data-source] ReviewRepository (get-review-by-id [_ id] (let [result (jdbc/execute-one! data-source ["select * from reviews where rowid=?" id])] result)))
2.2 web
web
has also two part. server
, router
.
server
namespace depends on repository
(ns demo1.server (:require [demo1.review-repository :as review-repository] [io.pedestal.http :as http])) (defn greet [req] {:status 200 :body "Hello, World!"}) (defn get-review [review-repository req] (let [id (-> req :path-params :id)] {:status 200 :body (str (review-repository/get-review-by-id review-repository id))}))
router
only focus on routing. interesting thing is router doesn't have dependency on server
(ns demo1.router (:require [io.pedestal.http.route :as route])) (defn router [route->handler] (route/expand-routes #{["/greet" :get (route->handler :greet) :route-name :greet] ["/reviews/:id" :get (route->handler :get-review) :route-name :reviews]})) (comment (route/try-routing-for routes :prefix-tree "/greet" :get) (route/try-routing-for routes :prefix-tree "/greet" :post) ;; )
2.3 system
system deals with dependencies.
and to test the api I have a initial database setup code in there (but it is not relevent about system
).
this code is only for the proof of the concept
(ns demo1.system (:require [demo1.db :as db :refer [->SqliteReviewRepository]] [demo1.server :as server] [demo1.router :as router] [integrant.core :as ig] [io.pedestal.http :as http] [next.jdbc :as jdbc])) ;;;; DB 초기 세팅 BEGIN (def db {:connection-uri "jdbc:sqlite:db/review.db"}) (defn create-db "create db and table" [] (jdbc/execute! db ["create table reviews (content text)"])) (defn init-test-data [] (jdbc/execute-one! db ["insert into reviews(content) values ('number 1'), ('number 2'), ('number 3')"])) (defn print-all-reviews [] (jdbc/execute! db ["select rowid, * from reviews"])) (comment (init-test-data)) ;;;; DB 초기 세팅 END ;;; integrant (def config {::data-source db ::get-greet-handler {} ::get-review-handler {:review-repository (ig/ref ::review-repository)} ::review-repository {:data-source (ig/ref ::data-source)} ::router {:route->handler {:get-review (ig/ref ::get-review-handler) :greet (ig/ref ::get-greet-handler)}} ::http-server {:router (ig/ref ::router) :port 8080}}) (defmethod ig/init-key ::get-greet-handler [_ system] server/greet) (defmethod ig/init-key ::data-source [_ system] system) (defmethod ig/init-key ::review-repository [_ {:keys [data-source]}] (->SqliteReviewRepository data-source)) (defmethod ig/init-key ::get-review-handler [_ {:keys [review-repository]}] #(server/get-review review-repository %)) (defmethod ig/init-key ::router [_ {:keys [route->handler]}] (router/router route->handler)) (defmethod ig/init-key ::http-server [_ {:keys [router port]}] (println ":http-server init " router) (let [server (http/create-server {::http/routes router ::http/type :jetty ::http/port port ::http/join? false})] (http/start server) server)) (defmethod ig/halt-key! ::http-server [_ server] (println "halt :http-server" server) (http/stop server)) (comment (def running-system (ig/init config)) (ig/halt! running-system) ;; )
you can run in running-system
and halt by using (ig/halt! running-system)
3 conclusion
That is the end of my code. I have an about 20-month experience with clojure recently. I was a java developer.
For me, this is a familiar pattern in java. using Repository
interface is identical. It looks like hexagonal architecture.
I know using hexagonal architecture is kind of verbose but it has better maintainability. but if we have a domain or problem to solve. i guess you want to concentrate on them. (unless it is not a technical issue) hexagonal architecture will help.
I am curious that Nubank uses what kind of architecture.