Skip to content

Instantly share code, notes, and snippets.

@blackawa
Last active May 18, 2023 05:55
Show Gist options
  • Save blackawa/4a7a0ca346429d8ec30b939e536c287d to your computer and use it in GitHub Desktop.
Save blackawa/4a7a0ca346429d8ec30b939e536c287d to your computer and use it in GitHub Desktop.
Duct wikiの和訳

boundariesは外部サービスを代理するcomponentのprotocolです。boundariesは、アプリケーションのサービス境界を制御するためのものだと考えてください。外部サービスとやり取りをするすべてのデータはboundaryを通らなければなりません。そのprotocolの関数は、アクセスを許可された外部サービスとの通信を代理します。

boundariesは、ラップした外部サービスとの結合を疎にしておかなければいけません。悪い例を示しましょう。

(defprotocol SQLDatabase
  (get-user [db username])
  (get-post [db post-id])
  (create-post [db subject body]))

これは、アプリケーションが外部サービスに要求するもの(ユーザー情報と投稿情報)とその実装(SQLデータベース)と密結合しています。

一方、良い例はこちらです。

(defprotocol UserDatabase
  (get-user [db username]))

(defprotocol PostDatabase
  (get-post [db post-id])
  (create-post [db subject body]))

componentは複数のprotocolを所有することができます。だから目的に特化した小さなboundariesをいくつも所有するのは良い設計です。

理想的には、外部サービスの各機能はboundaryによって隠蔽されているべきです。たとえば多くのデータベースはトランザクションをサポートしていますが、それをboundaryを超えて外部に公開するのは悪手です。

だから以下の例のような実装は悪いboundaryといえます。

(defprotocol AccountsDatabase
  (with-tx [db callback])
  (credit [db to amount])
  (debit [db from amount]))

トランザクションのような実装の詳細を隠蔽するのが、良いboundaryです。

(defprotocol AccountsDatabase
  (transfer [db from to amount]))

Boundariesはデータがどのようにアプリケーションを出入りするかを制御するだけでなく、テストにも役立てることができます。たとえばデータベースのような外部システムを実際に使用するテストは実行に時間がかかるし、並行実行が難しいことが多いです。Slow tests that need to be run in serial mean you can test fewer scenarios in a test run.(訳注: TODO)

実際の外部サービスを使ったテストは、テストが正確に書かれていることを保証するために重要ではありますが、並行実行できる速いテストはテストケースの数を最大化するために必要です。boundariesはどっちもできるようにしてくれます。実際の外部サービスに対するboundariesの実装をテストすることもできますが、それ以外のテストではモックを使用することもできます。モック化を提供するためにDuctは、devプロファイルでShrubberyを依存関係に持っています。

開発の利便性のために、Ductはboundariesを生成する関数を持っています。たとえば以下のようにすると

dev=> (gen/boundary 'user-database 'duct.component.hikaricp.HikariCP)
Creating file src/foo/boundary/user_database.clj
Creating file test/foo/boundary/user_database_test.clj
nil

Ductは新しいboundaryを書き始めるためのテンプレートを生成してくれます。

(ns foo.boundary.user-database
  (:require duct.component.hikaricp))

(defprotocol UserDatabase
  ;; boundary definition
  )

(extend-protocol UserDatabase
  duct.component.hikaricp.HikariCP
  ;; boundary implementation
  )

System

Ductはその設定をednファイルとmeta-mergedで定義します。デフォルト状態のproductionプロファイルではDuctは、以下のednファイルを使用します。

  • resources/{project}/system.edn

そして開発時には以下の2つも使います。

  • dev/resources/dev.edn
  • dev/resources/local.edn

dev.ednは、すべての開発者の間で共有すべきdevelopment環境での設定情報を定義します。local.ednはバージョン管理されず、各開発者がそれぞれに設定します。

system.ednは以下の4つのキー値を取ります。

{:components   {} ; componentのための設定情報
 :endpoints    {} ; endpointのための設定情報
 :dependencies {} ; component, endpointの依存関係情報
 :config       {} ; component, endpointの設定情報

アプリケーションが使用するendpointからhandlerを生成するためには、最低限1つのendpoint、HTTPアダプタとDuctのhandler-copmonentが必要です。最小構成の設定情報は以下のようになるはずです。

{:components
 {:app  duct.component.handler/handler-component
  :http ring.component.jetty/jetty-server}
 :endpoints
 {:example foo.endpoint.example/example-endpoint}
 :dependencies
 {:http [:app]
  :app  [:example]}
 :config
 {:http {:port 3000}}}

HTTPアダプタが:httpキーに紐付けられていることと、それが:appキーに紐付いたハンドラに依存していることに注意してください。そしてハンドラは:exampleキーに紐付いたエンドポイントに依存しています。

システム設定はduct.util.system/load-systemでロードされます。

(load-system [(io/resource "foo/system.edn")])

アプリケーションは複数の設定情報で初期化されうるので、設定情報は順番の決まったcollectionとして指定されています。

しばしば、環境変数から設定情報を取得しなければいけない場合もあります。たとえばよくあるのは、起動するHTTPサーバのポート番号をPORT環境変数から取得する、というようなことです。これを実現するために、Ductは設定情報に置換文字列を指定できるようにしてあります。たとえば、以下のような設定の仕方が考えられます。

{:components
 {:app  duct.component.handler/handler-component
  :http ring.component.jetty/jetty-server}
 :endpoints
 {:example foo.endpoint.example/example-endpoint}
 :dependencies
 {:http [:app]
  :app  [:example]}
 :config
 {:http {:port http-port}}}

この場合、http-portは置換対象の文字列です。これを置換するには、以下のように書きます。

(load-system [(io/resource "foo/system.edn")]
             {'http-port (Integer/parseInt (System/getenv "PORT"))})

このコードは、本番環境で設定情報を環境変数から取得したい時、sec/{project}/main.cljで使うことができます。開発中は、こういう設定情報はdev/src/dev.ednで設定できます。たとえば以下のように書いておけば良いです。

{:config {:http {:port 3000}}}

この設定情報は上記の本番環境用の設定情報にマージされて、http-portを置換します。

Handler

handler componentはすべてのシステムに含まれています。そしてendpointをRingのハンドラに紐付けます。そうすることでHTTPアダプタにendpointを渡すことができるのです。handler componentは以下の2つのキーで構成される設定情報を持っています。

{:endpoints  []  ; ルート一致の試行をするendpointの順番を定義する
 :middleware {}  ; ハンドラに適用するmiddlewareを指定する

デフォルトではendpointはキーの順序に従って追加されるので、:aaa:zzzより先に試行されます。この試行順序は、:endpointsで変更できます。

:middlewareはより一般的に使われるものです。:middleware項目は以下3つのキーからなるmapで構成されます。

{:middleware
 {:functions {}   ; functionの設定情報
  :arguments {}   ; functionがとる引数の設定情報
  :applied   []}} ; どのfunctionがどんな順番で適用されるかをvectorで定義する

たとえば、duct.middleware.not-found/wrap-not-foundというmiddlewareを追加したいとしましょう。このmiddleware functionは、どのendpointもrequestにマッチしなかった場合404 Not Foundページを返却します。

{:middleware
 {:functions {:not-found duct.middleware.not-found/wrap-not-found}
  :arguments {:not-found "Resource Not Found"}
  :applied   [:not-found]}}

前述のsystem.ednに入れると、こんなふうになります。

{:components
 {:app  duct.component.handler/handler-component
  :http ring.component.jetty/jetty-server}
  :endpoints
  {:example foo.endpoint.example/example-endpoint}
  :dependencies
  {:http [:app]
   :app  [:example]}
  :config
  {:http {:port http-port}}
  :app  {:middleware
         {:functions {:not-found duct.middleware.not-found/wrap-not-found}
          :arguments {:not-found "Resource Not Found"}
          :applied   [:not-found]}}}

そしてdev.ednはこうなります。

{:config {:http {:port 3000}
          :app  {:applied ^:replace [:not-found]}}}

注意してほしいことは、dev.edn^:replaceで適用対象のmiddlewareを置換するので、本番環境でも開発環境でも必要なmiddlewareはdev.ednでもsystem.ednでも定義する必要があります。たとえば、ring.middleware.json/wrap-json-responseというmiddlewareを本番環境でも開発環境でも使いたい時は、system.ednには:functions:appliedの情報を定義しておいて、:appliedの情報だけはdev.ednにも書いておくことが必要です。

Reader

Ductはもうひとつ、機能を隠し持っています。duct.util.system/readerというmultimethodを拡張することで自前のデータリーダーを定義できます。デフォルトでは、Ductは一つだけ追加のデータリーダーを所有しています。

(defmethod reader 'resource [_ value]
  (io/resource value))

このデータリーダは、クラスパス上の静的リソースを設定情報の一部としてロードすることができます。たとえば以下のように。

{:middleware
 {:functions {:not-found duct.middleware.not-found/wrap-not-found}
  :arguments {:not-found #resource "foo/errors/404.html"}
  :applied   [:not-found]}}
ductはClojureでWebアプリケーションを開発するためのテンプレートであり、小さなライブラリです。
* [Getting Started](./getting-started.md)
* [設定](./configuration.md)
* [サービス境界(boundaries)](./boundaries.md)
* [互換性のあるライブラリ](./compatible-libraries.md)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment