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