フレームワークを使わずにClojureScriptでアプリケーションを書いてみる

# 概要
entity, use case等のビジネスロジックを前面に押し出すように、アプリケーションを書くことに挑戦してみます。
re-frame等のフレームワークを使わず、clojure.core.asyncに頑張ってもらいます。
Closure LibraryやReagentは普通に使います。

Clean Architectureの影響を受けています。

今回書くのは、下のようなタグを編集する機能です。

![tag-edit.png](https://qiita-image-store.s3.amazonaws.com/0/48106/5a73bc20-a0a4-59a9-7e2b-b9d576981fc4.png)


# タグ編集

できることはざっくりいうと、次の通りです
- タグ編集の開始、保存、キャンセル
- タグのattach、detach
- 新しいタグの追加、既存のタグの削除

一つ一つ実装してみます。

## Entities

概念の定義を置く場所をentitiesとしています。

### タグ

タグやタグの編集等、今回のアプリケーションで取り扱う概念を定義します。

```cljs
(ns entities.tag)

;; タグを表現するクラス
;; タグのIDと名前で表現します。
(defrecord Tag [tag-id name])

;; タグの編集状態を表すクラス
;; 全部のタグと、付与されているタグにより、編集状態を表します。
(defrecord AttachedTagEdit [tags attached-tags])

(defn attach [attached-tag-edit tag]
  (assoc attached-tag-edit :attached-tags conj tag))

(defn detach [attached-tag-edit tag]
  (let [tag-id (:tag-id tag)]
    (update attached-tag-edit :attached-tags
            (fn [tags] (remove #(= tag-id (:tag-id %)) tags)))))

;; 新しく追加するタグの編集状態を表すクラス
;; 追加するタグの名前で編集状態を表します。
(defrecord NewTagEdit [name])
```

### Transaction

現在の状態を取得、現在の状態を更新するプロトコルを、Transactionと呼ぶことにします。
アプリケーションが状態を取得したり状態を更新したりするときは、このプロトコルを経由します。

```cljs
(ns entities.transaction)

(defprotocol Transaction
  ;; 現在の状態を取得する
  (state [this])
  ;; 状態を取って状態を返す関数 (f: State -> State) を受けとって、状態を更新する
  (update-state [this f]))
```

例えば、以下のようなStateを考えます。

```cljs
(defrecord State [counts])
```

Stateのcountsをインクリメントするコードは下ようになります。
これにより、非同期でStateが更新されます。

```cljs
(defn increment-by [transaction diff]
  (update-state transaction (fn [state] (update state :counts inc diff))))
```

短く書くと下のようになります。

```cljs
(defn increment-by [transaction diff]
  (update-state transaction #(update % :counts inc diff)))
```

なお、一貫性をもったStateをどうやって保持し続けるかは、Transaction実装時の関心です。したがって、Transactionを利用する側はこれを考えないようにします。

## API

サーバへHTTP Requestを投げる場所です。
以下のAPIがあると仮定して以降の話を続けていきます (実装はしません)
ここはライブラリが必要になるでしょう。

```cljs
(ns api
  (:require [entities.tag]))

;; すべてのTagをmapとして取得します。
;; keyはtag-idで、valueはTagです。
;; 取得した後はそれを引数にcallbackを実行します。
(defn list-tags [callback])

;; nameを名前としてTagを追加します。追加後は無引数でcallbackを実行します。
(defn add-tag [name callback])

;; Tagを削除します
(defn delete-tag [tag-id callback])

;; 付与されたTagをリストとして取得します。
(defn list-attached-tags [callback])

;; 付与されたTagのリストを保存します。
(defn save-attached-tags [tags callback])
```

## Use Cases

アプリケーションのコードを書く場所はuse_casesとしています。
entities, api等を使って、アプリケーションのアルゴリズムを書く場所です。

アプリケーションのアルゴリズムは、基本的に、状態を引数にとって新しい状態を生成する関数を生成します。

```cljs
(ns use_cases
  (:require [entities.tag]
            [entities.transaction :refer [state update-state]]
            [api]))

;; アプリケーションにより遷移する状態を表します
(derecord State [attached-tag-edit new-tag-edit])
```

```cljs
;;; タグ編集の開始
(defn start-edit [transaction]
  (api/list-tags (fn [tags]
    (api/list-attached-tags (fn [attached-tags]
      (update-state transaction
        ;; 現在のStateの、new-tag-editとattached-tag-editを変更する。
        #(-> %
             (assoc :new-tag-edit (entities.tag/NewTagEdit. ""))
             (assoc :attached-tag-edit (entities.tag/AttachedTagEdit. tags attached-tags)))))))`

;;; タグ編集のキャンセル
(defn cancel-edit [transaction]
  (update-state transaction #(State. nil nil)))

;;; タグ編集の保存
(defn save-edit [transaction]
  (let [attached-tags (-> transaction state :attached-tags)]
    (api/save-attached-tags attached-tags (fn []
      (update-state transaction #(State. nil nil)))))

;;; タグのattach
(defn attach-tag [transaction tag-id]
  (update-state transaction (fn [state]
    (if-let [tag (get tag-id (-> state :attached-tag-edit :tags))]
      (update state :attached-tag-edit entities.tag/attach tag)
      state)))

;;; タグのdetach
(defn detach-tag [transaction tag-id]
  (update-state transaction (fn [state]
    (if-let [tag (get tag-id (-> state :attached-tag-edit :tags))]
      (update state :attached-tag-edit entities.tag/detach tag)
      state)))

;;; 新しいタグ名の編集
(defn change-new-tag-name [transaction name]
  (update-state transaction #(assoc-in % [:new-tag-edit :name] name)))

;;; 新しいタグの追加
(defn add-new-tag [transaction]
  (let [tag-name (-> transaction state :new-tag-edit :name)]
    (api/add-tag tag-name (fn []
      (api/list-tags (fn [tags]
        (update-state transaction
          #(-> %
               (assoc-in [:new-tag-edit] (NewTagEdit. ""))
               (assoc-in [:attached-tag-edit :tags] tags)))))))

;;; 既存のタグの削除
(defn delete-tag [transaction tag-id]
  (detach-tag transaction tag-id)
  (api/delete-tag tag-id (fn []
    (api/list-tags (fn [tags]
      (update-state transaction
        #(assoc-in % [:attached-tag-edit :tags] tags)))))
```

## Components

componentを置きます。Reagentをフルに使いますが、詳細は省略。

```cljs
(ns components
  (:require [reagent.core :as r]))
```

Transactionやuse casesを見れるコンテナコンポーネントや、純粋にどう見せるかだけに関心を持つプレゼンテーショナルコンポーネントに分けるとよさそうです。

## main

mainはアプリケーションの起点となるところです。
Transactionの生成、ページとTransactionの対応付け等を行います。

今回の場合、ページが関わるStateはuse_cases/Stateの一つだけです。
しかし一般にページは複数のStateからなりえます。
すると、Stateに対応するTransactionも複数必要です。
ここで、ページで扱うTransactionをまとめた概念をStoreと呼ぶことにします。

```cljs
(ns main
  (:require [clojure.core.async :refer [go <! chan]]
            [goog.dom :as gdom]
            [reagent.core :as r]
            [entities.transaction]
            [use_cases]
            [components]))

;; stateの更新処理をupdate-fnに委譲するTransactionの実装です
(defrecord DelegateTransaction [state update-fn]
  entities.transaction/Transaction
  (entities.transaction/state [this]
    (-> this :state))

  (entities.transaction/update-state [this f]
    ((-> this :update-fn) f)))

;; storeはTransactionの集まりです。
;; 今回のstoreはただ一つのTransactionを持てば十分です。
;; DelegateTransactionのコンストラクタに渡すupdate-fnは、
;; きちんとStateが更新されるように設定します。
(defun create-store [update-store]
  {:transaction
   (DelegateTransaction. (use_cases/State. nil nil) 
                         #(fn [s] (update-in s [:transaction :state] %))})


(defn main [create-store render]
  (let [update-store-fn-chan (chan)]
    (go (loop [store (create-store #(put! update-store-fn-chan %))]
          ;; storeを元に、画面を描画します
          (render store)
          ;; storeの更新が起きる (= storeの更新関数がput!される) のを待ちます
          (let [update-store-fn (<! update-store-fn-chan)]
            ;; storeを更新してループします
            (recur (update-store-fn store)))))))

(main create-store
      (let [elem (gdom/getElement "app")]
        (fn [store]
          (r/render [components/tag-edit
                     :transaction (-> store :transaction)]
                    elem))))
```
# まとめ
- 主要なビジネスロジックをできるだけClojureScriptでだけ実装する挑戦
  - API以外は、だいたいそんな感じになった？
- clojure.core.asyncを使った副作用として、代入をしなくてよくなりました