component入門
背景
アプリケーションの中で一度だけ行いたい初期化・終了処理を書くことはよくあります. 例えば,データベースへの接続や外部リソースの初期化などです.
Clojureプログラムにおいて,このような初期化・終了の処理というのは意外と面倒です.
普通に走るプログラムであれば -main
関数などの開始時・終了時に処理を書けばよいのですが,ClojureプログラムはREPL環境で開発される場合が多いので,そうもいきません.
例えば,データベースへのハンドラを管理する場合を考えてみましょう.アプリケーションの開始時にコネクションを作成し,終了時に開放するとします.一番簡単なのは,varに束縛してしまうことです.
(def conn (create-db-conn)) (defn foo [] ;; connを使う )
しかし,これだとconn
の解放のタイミングがよくわかりません.REPLのセッション中ではずっとコネクションを維持したいですから,foo関数の中で開放してしまうわけに行きません.また,コードを更新後にリソース(上の例で言うconn
)をREPL上で作り直したいこともあります.これは,手動で conn
の解放・再作成をすればOKですが面倒です.
さらに,このような初期化・解放処理が必要なリソースがプログラム中にいくつも存在する場合はどうでしょう?さらに,それらのリソース間に依存性があることも考えられます.例えば,データベースコネクションに依存したリソースはよくありそうですね.それらも含めてリフレッシュしたい場合は?あちこちの名前空間に多数のvarが存在するようになると,アプリケーションの開発効率とメンテナンス性は急速に悪化します.
結局のところ問題は,「アプリケーションの名前空間内にmutableな状態が存在する」というところにつきます.Clojureはimmutableなデータ構造を使ってプログラムを書けるのがよいところですが,時には状態を持ったオブジェクトを管理する必要もあるわけです.このような構造をアプリケーションを支援してくれるのが stuartsierra/component です.
概要
componentを使ったプログラミングは,大きくわけて2つの要素からなります.
コンポーネントは,オブジェクト指向でいうオブジェクトに近いものですが,通常のClojureアプリケーションではかなり粗粒度のオブジェクトになるでしょう.典型的には各ネームスペースに1つのコンポーネントを定義することが多いようです.またコンポーネント同士は依存関係を持つことができます.
システムはコンポーネントの集合体として定義され,コンポーネント間の依存関係を考慮した初期化・終了処理を実行してくれます.
使い方/各コンポーネントの構築
まずは,stuartsierra/component
をプロジェクトで利用できるようにしましょう.
プロジェクトの依存関係に [com.stuartsierra/component “0.3.0”]
を追加しておきます.
ここでは,架空のプロジェクトとして,DB接続を管理するDB component, 初期化・終了処理が必要な外部APIを管理するAPI coponent, そしてそれらを利用するcomponent Aがあるとします.
それぞれのcomponentは,component/Lifecycle
プロトコルとdefrecord
を用いて定義します
;; DB接続を管理するComponent (defrecord DBComponent [conn host port] component/Lifecycle (start [this] (println "Initializing DB component.") ;; DB接続の開始処理 ;; ここでは仮に,"DB connection"という文字列を代入しておく (assoc this :conn (format "<DB connection: %s:%d" host port))) (stop [this] (println "Stopping DB component.") ;; DB接続の終了処理 ;; 注意: this自身からフィールドをnilに設定して返すが,dissocをしてしまうと ;; 戻り値が普通のmapになってしまう (assoc this :conn nil)))
メインとなるのはstart
とstop
関数で,これらを用いて初期化を行います.
引数として渡されるthis
は,コンポーネントのベースとなるrecordが渡されます.アプリケーションの各コンポーネントは,this
に必要なデータを格納していくことでコンポーネントを構築していきます.
使い方/システムの構築
(defn app-system [config-options] (let [{db-host :db-host db-port :db-port api-host :api-host} config-options] (component/system-map ;; (B) :db (map->DBComponent {:host db-host :port db-port}) :api (map->APIComponent {:host api-host}) :A (component/using (map->ComponentA config-options) {:db :db :api :api}))))
component/system-map
関数を用いてシステムを構築します.system-map
関数にはmapを渡しますが,これがシステム内でのキーとコンポーネントの関係を示します.各コンポーネントは,defrecord
時に定義されるmap->xxx
関数を使って構築されているのがわかると思います. component/using
は依存関係を定義する関数で,ここではComponentA
がDBComponent
とAPIComponent
に依存していることを示しています.
使い方/REPLでの活用法
REPLでの実行では,user
名前空間にシステムをvarとして定義して使っていく方法がメインとなります.
;; systemを定義 (def system (app-system {:db-host "dbhost" :db-port 9999 :api-host "apihost"})) (alter-var-root #'system component/start) ; systemを開始 ;;Initializing DB component. ;;Starting API component ;;Initializing A: db-conn= <DB connection: dbhost:9999 api= <External API session host=apihost> (alter-var-root #'system component/stop) ; systemを終了 ;; Finalizing A ;; Stopping API component ;; Stopping DB component.
これによって,user/system
にシステムが束縛されますので,systemを使ったプログラミングを行うことができます.それぞれのコンポーネントは,定義した当該のキーで取り出すことができます
user> (:db system) #example-ns.DBComponent{:conn "<DB connection: dbhost:9999", :host "dbhost", :port 9999}
tools.namespace
との連携によってより便利に使うことができますが,また次の機会に紹介したいと思います.