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
)