web-dev-qa-db-ja.com

ClojureでStuartSierraのコンポーネントライブラリを使用する方法

Clojureアプリ内で Stuart Sierraのコンポーネントライブラリ を使用する方法について頭を悩ませています。彼の Youtubeビデオ を見て、彼がライブラリを作成するに至った問題を十分に理解できたと思います。しかし、私はそれを新しい、かなり複雑なプロジェクトで実際に使用する方法を見つけるのに苦労しています。

これは非常に曖昧に聞こえますが、いくつかの重要な概念が欠けているように感じます。それを理解すれば、コンポーネントの使用方法をよく理解できます。別の言い方をすれば、Stuartのドキュメントとビデオでは、コンポーネントの内容と理由についてかなり詳しく説明していますが、方法がわかりません。

以下に入る詳細なチュートリアル/ウォークスルーはありますか?

  • 重要なClojureアプリにコンポーネントを使用する理由
  • コンポーネントを適度に最適な方法で実装できるように、重要なClojureアプリの機能をどのように分解するかについての方法論。あなたが持っているすべてが例えばであるとき、それはかなり簡単です。データベース、アプリサーバー、ウェブサーバーの層ですが、すべてがコヒーレントに連携する必要のある多くの異なるレイヤーを持つシステムでどのように使用するかを理解するのに苦労しています。
  • 開発/テスト/フェイルオーバーなどにアプローチする方法。コンポーネントを使用して構築された重要なClojureアプリで

前もって感謝します

27
monch1962

つまり、Componentは特殊なDIフレームワークです。システムマップと依存関係マップの2つのマップを指定して、注入されたシステムをセットアップできます。

作成されたWebアプリを見てみましょう(免責事項、実際に実行せずにフォームに入力しました):

(ns myapp.system
  (:require [com.stuartsierra.component :as component]
            ;; we'll talk about myapp.components later
            [myapp.components :as app-components]))

(defn system-map [config] ;; it's conventional to have a config map, but it's optional
  (component/system-map
    ;; construct all components + static config
    {:db (app-components/map->Db (:db config))
     :handler (app-components/map->AppHandler (:handler config))
     :server (app-components/map->Server (:web-server config))}))

(defn dependency-map
  ;; list inter-dependencies in either:
  ;;    {:key [:dependency1 :dependency2]} form or
  ;;    {:key {:name-arg1 :dependency1
  ;;           :name-arg2 :dependency2}} form
  {:handler [:db]
   :server {:app :handler})

;; calling this creates our system
(def create-system [& [config]]
  (component/system-using
    (system-map (or config {})
    (dependency-map)))

これにより、(create-system)を呼び出して、必要なときにアプリケーション全体の新しいインスタンスを作成できます。

(component/start created-system)を使用して、システムが提供するサービスを実行できます。この場合、ポートと開いているデータベース接続をリッスンしているのはWebサーバーです。

最後に、(component/stop created-system)で停止して、システムの実行を停止できます(たとえば、Webサーバーを停止し、dbから切断します)。

それでは、アプリのcomponents.cljを見てみましょう。

(ns myapp.components
  (:require [com.stuartsierra.component :as component]
            ;; lots of app requires would go here
            ;; I'm generalizing app-specific code to
            ;; this namespace
            [myapp.stuff :as app]))

(defrecord Db [Host port]
   component/Lifecycle
   (start [c]
      (let [conn (app/db-connect Host port)]
        (app/db-migrate conn)
        (assoc c :connection conn)))
   (stop [c]
      (when-let [conn (:connection c)]
        (app/db-disconnect conn))
      (dissoc c :connection)))

(defrecord AppHandler [db cookie-config]
   component/Lifecycle
   (start [c]
      (assoc c :handler (app/create-handler cookie-config db)))
   (stop [c] c))

;; you should probably use the jetty-component instead
;; https://github.com/weavejester/ring-jetty-component
(defrecord Server [app Host port]
   component/Lifecycle
   (start [c]
      (assoc c :server (app/create-and-start-jetty-server
                        {:app (:handler app)
                         :Host host 
                         :port port})))
   (stop [c]
      (when-let [server (:server c)]
         (app/stop-jetty-server server)
      (dissoc c :server)))

では、私たちは何をしたのでしょうか?再読み込み可能なシステムを手に入れました。 figwheel を使用している一部のclojurescript開発者は類似点を見始めていると思います。

これは、コードをリロードした後、システムを簡単に再起動できることを意味します。 user.cljへ!

(ns user
    (:require [myapp.system :as system]
              [com.stuartsierra.component :as component]
              [clojure.tools.namespace.repl :refer (refresh refresh-all)]
              ;; dev-system.clj only contains: (def the-system)
              [dev-system :refer [the-system]])

(def system-config
  {:web-server {:port 3000
                :Host "localhost"}
   :db {:Host 3456
        :Host "localhost"}
   :handler {cookie-config {}}}

(def the-system nil)

(defn init []
  (alter-var-root #'the-system
                  (constantly system/create-system system-config)))

(defn start []
  (alter-var-root #'the-system component/start))

(defn stop []
  (alter-var-root #'the-system
                  #(when % (component/stop %))))

(defn go []
  (init)
  (start))

(defn reset []
  (stop)
  (refresh :after 'user/go))

システムを実行するには、次のようにreplに入力します。

(user)> (reset)

コードをリロードし、システム全体を再起動します。稼働中の既存のシステムが起動するとシャットダウンします。

その他のメリットもあります。

  • エンドツーエンドのテストは簡単です。構成を編集するか、コンポーネントを置き換えてインプロセスサービスを指すようにします(テスト用のインプロセスkafkaサーバー)を指すために使用しました)。
  • 理論的には、同じJVMに対してアプリケーションを複数回生成できます(最初のポイントほど実用的ではありません)。
  • コードを変更してサーバーを再起動する必要がある場合は、REPL)を再起動する必要はありません。
  • リングリロードとは異なり、目的に関係なく、アプリケーションを再起動するための統一された方法が得られます。バックグラウンドワーカー、マイクロサービス、または機械学習システムはすべて同じ方法で設計できます。

すべてが進行中であるため、Componentはフェイルオーバー、分散システム、または障害のあるコードに関連するものを処理しないことに注意してください;)

コンポーネントがサーバー内での管理に役立つ「リソース」(ステートフルオブジェクト)はたくさんあります。

  • サービス(キュー、データベースなど)への接続
  • 時間の経過(スケジューラー、cronなど)
  • ロギング(アプリロギング、例外ロギング、メトリックなど)
  • ファイルIO(blobストア、ローカルファイルシステムなど)
  • 着信クライアント接続(Web、ソケットなど)
  • OSリソース(デバイス、スレッドプールなど)

Webサーバーとデータベースしかない場合、コンポーネントはやり過ぎのように見えることがあります。しかし、最近ではWebアプリはほとんどありません。

サイドノート:the-systemを別の名前空間に移動すると、開発時にthe-system変数が更新される可能性が低くなります(例:代わりにrefreshを呼び出す) resetの)。

38
Jeff