[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.