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

4 레퍼런스

Date: 2022-03-12 Sat 00:00

Author: Younghwan Nam

Created: 2024-01-04 Thu 09:13

Emacs 27.2 (Org mode 9.4.4)

Validate