From 698a2537ed5ee0631268cdbee38b11fc6a83fbb9 Mon Sep 17 00:00:00 2001 From: andray shotkin Date: Tue, 9 Jul 2024 21:12:59 +0300 Subject: [PATCH] clojurification --- src/clj_ddd_example.clj | 34 ++--- src/clj_ddd_example/domain_model.clj | 180 ++++-------------------- src/clj_ddd_example/domain_services.clj | 84 ++++++++--- src/clj_ddd_example/repository.clj | 90 +++++------- 4 files changed, 144 insertions(+), 244 deletions(-) diff --git a/src/clj_ddd_example.clj b/src/clj_ddd_example.clj index f56db28..f760195 100644 --- a/src/clj_ddd_example.clj +++ b/src/clj_ddd_example.clj @@ -32,10 +32,11 @@ must perform the necessary side-effects the use case demand, such as in our case updating our persistent state using the repository to update our datastore about the transfer of money that occurred." - (:require [clj-ddd-example.repository :as repository] - [clj-ddd-example.domain-model :as dm] - [clj-ddd-example.domain-services :as ds])) + (:require [clj-ddd-example.domain-services :as ds] + [clj-ddd-example.repository :as repository] + [clojure.spec.alpha :as s])) +(s/check-asserts true) (defn transfer-money "Our first use case, transfer-money, can be used to transfer money from one @@ -54,17 +55,12 @@ (try (let [from-account (repository/get-account from) to-account (repository/get-account to) - domain-amount (dm/make-amount amount currency) - transfered-money (ds/transfer-money transfer-number from-account to-account domain-amount)] - (repository/commit-transfered-money-event transfered-money) + transfer-money (ds/transfer-money transfer-number from-account to-account {:amount/currency currency + :amount/value amount})] + (repository/commit-transfer-money transfer-money) {:status :done - :transfered [(-> domain-amount :value) (-> domain-amount :currency)] - :debited-account (-> transfered-money :debited-account :number) - :debited-account-amount [(-> transfered-money :posted-transfer :transfer :debit :amount :value) - (-> transfered-money :posted-transfer :transfer :debit :amount :currency)] - :credited-account (-> transfered-money :credited-account :number) - :credited-account-amount [(-> transfered-money :posted-transfer :transfer :credit :amount :value) - (-> transfered-money :posted-transfer :transfer :credit :amount :currency)]}) + :transfered [amount currency] + :transfered-result transfer-money}) (catch Exception e {:status :error :transfer-number transfer-number @@ -86,12 +82,12 @@ ;; Evaluate this to transfer some money -#_(transfer-money - :transfer-number "ABC12345678" - :from "125746398235" - :to "234512768893" - :amount 200 - :currency :usd) +(transfer-money + :transfer-number "ABC12345678" + :from "125746398235" + :to "234512768893" + :amount 200 + :currency :usd) ;; Evaluate this to run two thousand 1$ transfers in parallel to test the ;; eventual consistency of our implementation. diff --git a/src/clj_ddd_example/domain_model.clj b/src/clj_ddd_example/domain_model.clj index 109feec..c23de34 100644 --- a/src/clj_ddd_example/domain_model.clj +++ b/src/clj_ddd_example/domain_model.clj @@ -163,21 +163,14 @@ ;;;;;;;;;;;;;; ;;; Amount ;;; -(s/def :amount/currency - currency?) +(s/def :amount/currency currency?) (s/def :amount/value (s/and number? pos?)) (s/def :amount/amount - (s/keys :req-un [:amount/currency - :amount/value])) - -(defn make-amount - [value currency] - (s/assert :amount/amount - {:currency currency - :value value})) + (s/keys :req [:amount/currency + :amount/value])) ;;; Amount ;;; ;;;;;;;;;;;;;; @@ -186,22 +179,12 @@ ;;;;;;;;;;;;;;; ;;; Balance ;;; -(s/def :balance/value +(s/def :account/balance number?) -(s/def :balance/currency +(s/def :account/currency currency?) -(s/def :balance/balance - (s/keys :req-un [:balance/currency - :balance/value])) - -(defn make-balance - [value currency] - (s/assert :balance/balance - {:currency currency - :value value})) - ;;; Balance ;;; ;;;;;;;;;;;;;;; @@ -215,76 +198,19 @@ #(s/gen #{"273648898836" "111234871234" "998877324561"}))) (s/def :account/account - (s/keys :req-un [:account/number - :balance/balance])) - -(defn make-account - [account-number balance] - (s/assert :account/account - {:number account-number - :balance balance})) - -(s/def :debited-account/event #{:debited-account}) - -(s/def :debited-account/amount-value :amount/value) - -(s/def :account/debited-account - (s/keys :req-un [:debited-account/event - :account/number - :debited-account/amount-value])) - -(defn- make-debited-account-event - [account-number amount-value] - (s/assert :account/debited-account - {:event :debited-account - :number account-number - :amount-value amount-value})) - -(defn debit-account - "Returns a debited-account domain event describing the valid debit state - change that has happened to the Account, so that it can be applied to our app - state eventually." - [account debit] - (if - (and - (= (-> account :balance :currency) (-> debit :amount :currency)) - (= (-> account :number) (-> debit :number)) - (>= (- (-> account :balance :value) (-> debit :amount :value)) 0)) - (make-debited-account-event (-> account :number) (-> debit :amount :value)) - (throw (ex-info "Can't debit account" {:type :illegal-operation - :action :debit-account - :account account - :debit debit})))) - -(s/def :credited-account/event #{:credited-account}) - -(s/def :credited-account/amount-value :amount/value) - -(s/def :account/credited-account - (s/keys :req-un [:credited-account/event - :account/number - :credited-account/amount-value])) - -(defn- make-credited-account-event - [account-number amount-value] - (s/assert :account/credited-account - {:event :credited-account - :number account-number - :amount-value amount-value})) - -(defn credit-account - "Returns a credited-account domain event describing the valid credit state - change that has happened to the Account, so that it can be applied to our app - state eventually." - [account credit] - (if - (and - (= (-> account :balance :currency) (-> credit :amount :currency)) - (= (-> account :number) (-> credit :number))) - (make-credited-account-event (-> account :number) (-> credit :amount :value)) - (throw (ex-info "Can't credit account" {:type :illegal-operation - :account account - :credit credit})))) + (s/keys :req [:account/number + :account/currency + :account/balance])) + +(s/def :account/debit + (s/keys :req [:account/number + :amount/value + :amount/currency])) + +(s/def :account/credit + (s/keys :req [:account/number + :amount/value + :amount/currency])) ;;; Account ;;; ;;;;;;;;;;;;;;; @@ -293,15 +219,6 @@ ;;;;;;;;;;;;; ;;; Debit ;;; -(s/def :debit/debit - (s/keys :req-un [:account/number - :amount/amount])) - -(defn make-debit - [account-number amount] - (s/assert :debit/debit - {:number account-number - :amount amount})) ;;; Debit ;;; ;;;;;;;;;;;;; @@ -310,16 +227,6 @@ ;;;;;;;;;;;;;; ;;; Credit ;;; -(s/def :credit/credit - (s/keys :req-un [:account/number - :amount/amount])) - -(defn make-credit - [account-number amount] - (s/assert :credit/credit - {:number account-number - :amount amount})) - ;;; Credit ;;; ;;;;;;;;;;;;;; @@ -338,44 +245,17 @@ (s/def :transfer/creation-date inst?) -(s/def :transfer/transfer - (s/and - (s/keys :req-un [:transfer/id - :transfer/number - :debit/debit - :credit/credit - :transfer/creation-date]) - (fn[{:keys [debit credit]}] - (and (= (:amount debit) (:amount credit)) - (not= (:number debit) (:number credit)))))) - -(defn make-transfer - [transfer-number debit credit] - (s/assert :transfer/transfer - {:id (random-uuid) - :number transfer-number - :debit debit - :credit credit - :creation-date (java.util.Date.)})) - -(s/def :posted-transfer/event #{:posted-transfer}) - -(s/def :transfer/posted-transfer - (s/keys :req-un [:posted-transfer/event - :transfer/transfer])) - -(defn- make-posted-transfer-event - [transfer-number debit credit] - (s/assert :transfer/posted-transfer - {:event :posted-transfer - :transfer (make-transfer transfer-number debit credit)})) - -(defn post-transfer - "Returns a posted-transfer domain event describing the valid posted state - change that has happened to the Transfer, so that it can be applied to our - app state eventually." - [transfer-number debit credit] - (make-posted-transfer-event transfer-number debit credit)) - ;;; Transfer ;;; ;;;;;;;;;;;;;;;; + +(s/def :transfer/transfer-money + (s/and + (s/keys :req [:account/debit + :account/credit + :transfer/id + :transfer/number + :transfer/creation-date]) + (fn [{debit :account/debit + credit :account/credit}] + (and (= (select-keys debit [:amount/currency :amount/value]) (select-keys credit [:amount/currency :amount/value])) + (not= (:account/number debit) (:account/number credit)))))) \ No newline at end of file diff --git a/src/clj_ddd_example/domain_services.clj b/src/clj_ddd_example/domain_services.clj index b571b75..eb879f6 100644 --- a/src/clj_ddd_example/domain_services.clj +++ b/src/clj_ddd_example/domain_services.clj @@ -50,21 +50,40 @@ Domain services can make use of other domain services as well as the domain model." - (:require [clojure.spec.alpha :as s] - [clj-ddd-example.domain-model :as dm])) + (:require [clojure.spec.alpha :as s])) +(defn- debit-account + "Returns a debit-account domain event describing the valid debit state + change that has happened to the Account, so that it can be applied to our app + state eventually." + [account amount] + (s/assert :account/account account) + (s/assert :amount/amount amount) + (if (and + (= (:account/currency account) (:amount/currency amount)) + (>= (- (:account/balance account) (:amount/value amount)) 0)) + {:account/number (:account/number account) + :amount/value (:amount/value amount) + :amount/currency (:amount/currency amount)} + (throw (ex-info "Can't debit account" {:type :illegal-operation + :action :debit-account + :account account + :amount amount})))) -(s/def :transfer-money/transfered-money - (s/keys :req-un [:account/debited-account - :account/credited-account - :transfer/posted-transfer])) - -(defn- make-transfered-money-event - [debited-account credited-account posted-transfer] - (s/assert :transfer-money/transfered-money - {:debited-account debited-account - :credited-account credited-account - :posted-transfer posted-transfer})) +(defn- credit-account + "Returns a credit-account domain event describing the valid credit state + change that has happened to the Account, so that it can be applied to our app + state eventually." + [account amount] + (s/assert :account/account account) + (s/assert :amount/amount amount) + (if (= (:account/currency account) (:amount/currency amount)) + {:account/number (:account/number account) + :amount/value (:amount/value amount) + :amount/currency (:amount/currency amount)} + (throw (ex-info "Can't credit account" {:type :illegal-operation + :account account + :amount amount})))) (defn transfer-money "Returns if money can be transferred from one account to another for some @@ -73,17 +92,20 @@ transferring money. Otherwise throws an exception about the transfer not being possible." [transfer-number from-account to-account amount] + (s/assert :account/account from-account) + (s/assert :account/account to-account) + (s/assert :amount/amount amount) (try - (let [debit (dm/make-debit (:number from-account) amount) - credit (dm/make-credit (:number to-account) amount) - debited-account (dm/debit-account from-account debit) - credited-account (dm/credit-account to-account credit) - posted-transfer (dm/post-transfer transfer-number - debit - credit)] - (make-transfered-money-event debited-account - credited-account - posted-transfer)) + (let [debit-account (debit-account from-account amount) + credit-account (credit-account to-account amount)] + ;; Returns a posted-transfer domain event describing the valid posted state + ;; change that has happened to the Transfer, so that it can be applied to our + ;; app state eventually. + {:transfer/id (random-uuid) + :transfer/number transfer-number + :account/debit debit-account + :account/credit credit-account + :transfer/creation-date (java.util.Date.)}) (catch Exception e (throw (ex-info "Money cannot be transferred." {:type :illegal-operation @@ -92,3 +114,19 @@ :to-account to-account :amount amount} e))))) + +(defn apply-debit-account + "Returns an updated account of the given account with the debit described + by debit-account-event applied to it." + [account debit-account] + (s/assert :account/account account) + (s/assert :account/debit debit-account) + (update account :account/balance - (:amount/value debit-account))) + +(defn apply-credit-account + "Returns an updated account of the given account with the credit described + by credit-account-event applied to it." + [account credit-account] + (s/assert :account/account account) + (s/assert :account/credit credit-account) + (update account :account/balance + (:amount/value credit-account))) \ No newline at end of file diff --git a/src/clj_ddd_example/repository.clj b/src/clj_ddd_example/repository.clj index 3889c20..e60f540 100644 --- a/src/clj_ddd_example/repository.clj +++ b/src/clj_ddd_example/repository.clj @@ -40,9 +40,9 @@ changes, and use the domain model/services to perform the change that the application service would commit back using the repository. In that sense, Finder is read only, while Repository is write only with reads done as - necessary in order to write, but switching from one to the other is possible." - (:require [clj-ddd-example.domain-model :as dm])) - + necessary in order to write, but switching from one to the other is possible." + (:require [clj-ddd-example.domain-services :as ds] + [clojure.spec.alpha :as s])) (def ^:private datastore (atom {:account-table #{["125746398235" 1000 :usd] @@ -51,26 +51,28 @@ (defn- account->account-table-row [account] - [(-> account :number) - (-> account :balance :value) - (-> account :balance :currency)]) + (s/assert :account/account account) + [(:account/number account) + (:account/balance account) + (:account/currency account)]) (defn- account-table-row->account [account-table-row] - (dm/make-account - (nth account-table-row 0) - (dm/make-balance (nth account-table-row 1) - (nth account-table-row 2)))) + (let [[account-number value currency] account-table-row] + {:account/number account-number + :account/currency currency + :account/balance value})) (defn- transfer->transfer-table-row [transfer] - [(-> transfer :id) - (-> transfer :number) - (-> transfer :debit :number) - (-> transfer :credit :number) - (-> transfer :debit :amount :value) - (-> transfer :debit :amount :currency) - (-> transfer :creation-date)]) + (s/assert :transfer/transfer-money transfer) + [(:transfer/id transfer) + (:transfer/number transfer) + (get-in transfer [:account/debit :account/number]) + (get-in transfer [:account/credit :account/number]) + (get-in transfer [:account/debit :amount/value]) + (get-in transfer [:account/debit :amount/currency]) + (:transfer/creation-date transfer)]) (defn- get-account-row "Returns the DB specific account structure, not one from @@ -80,20 +82,6 @@ (fn[row] (when (= (first row) account-number) row)) account-table)) -(defn- apply-debited-account-event - "Returns an updated account of the given account with the debit described - by debited-account-event applied to it." - [debited-account-event account] - (update-in account [:balance :value] - - (:amount-value debited-account-event))) - -(defn- apply-credited-account-event - "Returns an updated account of the given account with the credit described - by credited-account-event applied to it." - [credited-account-event account] - (update-in account [:balance :value] - + (:amount-value credited-account-event))) - (defn get-account "Returns Account entity for the account identified by account-number, nil if there isn't one." @@ -101,30 +89,28 @@ (when-let [account-row (get-account-row (:account-table @datastore) account-number)] (account-table-row->account account-row))) -(defn commit-transfered-money-event +(defn commit-transfer-money "Commits to our app state a transfered-money domain event, this implies adding a transfer entry for the posted-transfer event created as part of the transfer, as well as updating the debited account and the credited account - with their new balance as described by the debited-account and - credited-account domain events." - [{:keys [posted-transfer debited-account credited-account] :as _transfered-money}] + with their new balance as described by the debit-account and + credit-account domain events." + [{credit-account :account/credit + debit-account :account/debit + :as transfer-money}] + (s/assert :transfer/transfer-money transfer-money) (swap! datastore - (fn[currentstore] + (fn [currentstore] (let [account-table (:account-table currentstore) - debit-account-row (get-account-row account-table (-> debited-account :number)) - debit-account (account-table-row->account debit-account-row) - credit-account-row (get-account-row account-table (-> credited-account :number)) - credit-account (account-table-row->account credit-account-row) - new-debit-account-row (-> debited-account - (apply-debited-account-event debit-account) - (account->account-table-row)) - new-credit-account-row (-> credited-account - (apply-credited-account-event credit-account) - (account->account-table-row)) - transfer-row (transfer->transfer-table-row (:transfer posted-transfer))] + from-account-row (get-account-row account-table (:account/number debit-account)) + to-account-row (get-account-row account-table (:account/number credit-account)) + + from-account (account-table-row->account from-account-row) + to-account (account-table-row->account to-account-row) + + from-account-updated (ds/apply-debit-account from-account debit-account) + to-account-updated (ds/apply-credit-account to-account credit-account)] (-> currentstore - (update :account-table disj debit-account-row) - (update :account-table conj new-debit-account-row) - (update :account-table disj credit-account-row) - (update :account-table conj new-credit-account-row) - (update :transfer-table conj transfer-row)))))) + (update :account-table disj from-account-row to-account-row) + (update :account-table conj (account->account-table-row from-account-updated) (account->account-table-row to-account-updated)) + (update :transfer-table conj (transfer->transfer-table-row transfer-money)))))))