/home/matstani/weblog

programming log.

[Clojure] componentフレームワークを利用した開発

componentフレームワークは動作中に状態を保持するソフトウェアに関して

  • 起動・停止のインターフェイスを定義する
  • ソフトウェアをコンポーネントに分割し、コンポーネント間の依存関係を宣言的に記述する
  • 動作中のソフトウェア全体の状態を一つのデータ構造(システム)にまとめる

といった機能を持つフレームワークです。componentフレームワークを利用することで、tools.namespaceを利用した開発が容易になります。

ring/compojureベースのWebアプリケーションに、componentフレームワークを適用する例を紹介します。

プロジェクト作成

compojure用テンプレートを利用してプロジェクトを作成します。

1
$ lein new compojure example-component-ring

依存ライブラリ追加

project.clj:dependenciesに以下を追記します。

componentフレームワーク

1
[com.stuartsierra/component "0.2.1"]

tools.namespace

1
[org.clojure/tools.namespace "0.2.5"]

ring-jetty-adapter(ringアプリケーションの直接起動、停止)

1
[ring/ring-jetty-adapter "1.3.0"]

HttpServerコンポーネント定義

example-component-ring.componentネームスペースに、HttpServerコンポーネントを記述した例です。
component.clj

コンポーネントはLifecycleプロトコルを実装したレコードとして定義します。
Lifecycleプロトコルはメソッドとしてstartstopを持っています。これをHTTPサーバの起動、停止を行うように実装しています。この時、サーバオブジェクト(このアプリケーションにおける状態)をレコードのフィールドに格納しています。

create-system関数は、コンポーネントを作成し、1つのシステムにまとめる関数です。内部でsystem-map関数を利用しています。上記では、HttpServerコンポーネントのみ作成していますが、複数のコンポーネントを組み合わせて1つのシステムにまとめることや、コンポーネント間の依存関係を記述することができます。

システムの起動・停止・リロード

システムの起動・停止・リロードを行うための関数を定義します。REPLから利用しやすいように、userネームスペースに定義する例を紹介します。

project.clj追記

起動・停止・リロード用の関数は、開発時のみ読み込まれるファイルに記述します。
project.clj:profiles :devに以下を追記します。

1
:source-paths ["dev"]

project.clj全体は以下のようになります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
(defproject example-component-ring "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [compojure "1.1.8"]
                 [com.stuartsierra/component "0.2.1"]
                 [ring/ring-jetty-adapter "1.3.0"]
                 [org.clojure/tools.namespace "0.2.5"]]
  :plugins [[lein-ring "0.8.11"]]
  :ring {:handler example-component-ring.handler/app}
  :profiles
  {:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
                        [ring-mock "0.1.5"]]
         :source-paths ["dev"]}})

user.clj作成

dev/user.cljファイルを作成し、システム起動・停止・リロードを行う関数を定義します。 user.clj

プロジェクト内でREPLを起動すると自動的にこれらの関数が読み込まれた状態になります。

システムを起動する

1
2
3
user> (go)
;; Starting HTTP server
#com.stuartsierra.component.SystemMap{:http-server #example_component_ring.component.HttpServer{:port 3000, :server #<Server org.eclipse.jetty.server.Server@120f5c9>}}

システムを停止する

1
2
3
user> (stop)
;; HTTP server stopped
#com.stuartsierra.component.SystemMap{:http-server #example_component_ring.component.HttpServer{:port 3000, :server nil}}

(ソースコード修正時)システムをリロードする

1
2
3
4
user> (reset)
:reloading (example-component-ring.handler example-component-ring.component example-component-ring.test.handler user)
;; Starting HTTP server
#com.stuartsierra.component.SystemMap{:http-server #example_component_ring.component.HttpServer{:port 3000, :server #<Server org.eclipse.jetty.server.Server@1fc6f6a>}}

複数のコンポーネントの連携

componentフレームワークでは、複数のコンポーネントの依存関係を記述し、コンポーネント同士を連携させることができます。
ここでは、Databaseコンポーネントを導入し、HttpServerコンポーネントが利用する(HttpServerコンポーネントがDatabaseコンポーネントに依存する)例を紹介します。

MongoDB導入

Databaseコンポーネントの中身として、MongoDBを利用することにします。ClojureからMongoDBを利用するためのクライアントライブラリとしてMongerを利用します。project.cljに以下を追記します。

1
[com.novemberain/monger "2.0.0"]

Databaseコンポーネント定義

component.cljにDatabaseコンポーネントを定義します。
component.clj

HttpServerと同様に、Lifecycleプロトコルを実装したレコードとして定義しています。
既存コードへの変更点として、HttpServerレコードに、databaseフィールドを追加しています。このフィールドにDatabaseコンポーネントのオブジェクトを、状態として保持します。
また、create-system関数では、using関数を利用して依存関係を定義しています。上記では、HttpServerコンポーネントがDatabaseコンポーネントを利用する、という関係を記述しています。これによりHttpServerインスタンスのdatabaseフィールドには、Databaseインスタンスが代入されます(依存関係の定義に従って代入が自動的に行われる仕組みをDependency Injectionといいます)。

コンポーネントをringハンドラに渡す

上記までの例で、HttpServerコンポーネントにDatabaseコンポーネントを渡すことはできていますが、ringアプリケーションのエントリポイントとなるハンドラに、直接引数として渡すことはできないため、Webアプリケーション内からDatabaseコンポーネントを利用するためには、さらに一工夫が必要になります。
例として、requestマップ内にDatabaseコンポーネントを追加するringミドルウェアを利用する方法を紹介します。
component.clj

wrap-app-componentミドルウェアで、requestにDatabaseコンポーネントを追加しています。これにより、ringハンドラ内の処理で、Databaseコンポーネントを利用できるようになります。
handler.clj

参考リンク

Comments