「集約の境界と整合性(略」に対して頂いたアイデアの分類と現状での僕の回答らしきもの
- 解決策1 集約をマージするタイプのアイデア
- 解決策2 一時的な整合性の破綻を受け入れ結果整合性を使うタイプのアイデア
- 解決策3 アンチパターンではあるが集約間の整合性維持のためトランザクション制御を用いる
- 解決策4 ユースケースの見直しによる再モデリングタイプのアイデア
- 僕の現時点での回答らしきもの
- まとめ
- 2018/12/12 追記
- 2019/01/20 追記
- 関連リンク
先日ドメイン駆動設計 #1 Advent Calendar 2018 - Qiitaの1日として次の記事を公開しました。
当初2,3人からアイデア頂ければ御の字だと思っていたのですが、思っていたよりずっと多くの人からたくさんのアイデアを頂くことができました。本当にありがとうございました。 僕が2ヶ月ぐらい考えても全然思いつかなかったアイデア・意見がたくさんあって、やっぱり一人では発想に限界があるなと痛感しました。
このまま僕だけが頂いたアイデアを知っているのは勿体無いので、この記事では頂いたアイデアを解決策1~4へ分類しつつ更に掘り下げました。 なるべく正確に理解するよう努めたつもりですが、間違って解釈していたらすみません。 はてぶやtwitterで直接メンションが飛んでいないつぶやきなども拾っていますので、もし私的な記述なのでここに載せないでくれという方居られましたらここのコメント欄かtwitterのDMなどで連絡をください。できるだけすぐに消すよう努めます。
解決策1 集約をマージするタイプのアイデア
皆無でした。 やはり多くの人がこの方向の解決はまずいパターンだと認識されているようです。
解決策2 一時的な整合性の破綻を受け入れ結果整合性を使うタイプのアイデア
DDDとして正道であることもあり、最も多くのアイデアが出ました。
A. 組織へ無効状態で追加後、有効化するアイデア
A.i 連続的に追加・有効化を行うアイデア
- TANIGUCHI Kousukeさん: ユーザー追加を無効状態で追加して、アクティベーションする役割を組織の役割として持つのでよいのではないでしょうか? 早すぎる抽象化だとは思いますが、精緻に行うなら有効ユーザー上限は具象であって、そこにあるべきは有効化ポリシーだと思います。…
- nrsさん: 自分がやるときは組織に現在の有効ユーザの id 配列を持たせそう あとは有効化させてから無効にするのではなく、ユーザ追加とユーザの有効化の二つの処理に分割して、ユーザ追加は成功するけどユーザの有効化は失敗する可能性がある、という結果整合性に寄せていきそう…
- こむさん: 一般論なら結果整合で、今回の具体例ならユーザーを無効にしておいてユーザー数制限にカウントしない (略)、かな。
A.ii 追加後、有効化を別スレッドやイベント駆動で行うアイデア
- かとじゅんさん: 集約の境界と整合性問題に関する感想のプラン1 無効状態で追加、ワーカースレッドで自動有効化するアイデア
- nunulkさん: 解決策2、無効化した状態でユーザー登録しておいて、制限範囲内であることが確認されたら有効化する、という逆の流れではダメだろうか。結果整合性を一定時間でチェックするデーモンとともに
- tinsep19さん: ユーザーは無効で追加し、有効化を組織の役割に、で良くない?ユーザー数ライセンスだが、個人ライセンスの付替行為が暗黙的に発生しているとし、アクティベート要求をキューで順次処理かな
B. 組織に所属するユーザー一覧というドメインオブジェクトをアイデア
- haazimeさん: 組織がなくてもユーザーが存在して良いんだったら、所属ユーザーみたいなオブジェクト(別集約or組織の値オブジェクト)に上限の監視をやらせるんですけどね。うーん、難しい。。。…
- azihsoynさん: createユーザーとjoin組織を分けるとかかなぁ
B.i ユーザー一覧を組織集約に持たせるアイデア
- @turanukimaruさん: 集合は集約なのか?ってマサカリを投げてみる話
- cactuaroidさん: 「集約の境界と整合性の維持の仕方に悩んで2ヶ月ぐらい結論を出せていない話」の感想
- 増田 亨.さん: 私なら組織オブジェクトが、現在の有効ユーザ数を持つようにするかな。 ユーザ追加できるか問合わせるメソッドを用意する。 組織オブジェクトは常にメモリ上に存在して、適切に状態が更新されると「仮定」する。 リポジトリをDBアクセスのラッパーではなく、メモリ上のオブジェクト操作と考える。…
B.ii ユーザー一覧を独立した集約にするアイデア
- yojikさん: シンプルにユーザと組織の間に「所属」集約を作るかなぁ (個人的には集約をトランザクション境界にするDDDのアイディアは全く納得してないけど、どちらにしろこのケースなら所属オブジェクトは必ず必要)
- 松岡さん: 結果整合性がどうしてもコストが大きくて、同一トランザクションでやりたい場合、という前提で考えてみました。 5. パターン3の拡張で、ユーザー数の制約を管理する別の集約を作る 企業の中に他集約の数に応じたカウントを持つのはやはり違和感があるので、その部分だけ切り出してしまう。(続)…
C. イベント駆動アーキテクチャの長期プロセス(サーガ)的なアイデア
読んでいてIDDDでいうサーガ(長期プロセス)のイメージなのかなと思いました。解釈が間違っていたらすみません💧
- かとじゅんさん: 集約の境界と整合性問題に関する感想のプラン2 組織ID毎のワーカーがユーザー追加を担当することで2つのプロセスが同時並行に同じ組織へユーザーを追加しないことを担保するアイデア
D. オーバーがNGなら逆に現在ユーザー数を先に増やすアイデア
- morikuniさん: ユーザーの作成を先に行い、結果整合性でカウントを更新すると定員オーバーする可能性がある。ならば先にカウントを更新し、結果整合性でユーザーの作成を行えばいいんじゃないだろうか。 トレードオフはあると思うけど、カウントの整合性を第一にするなら選択肢としてはありそう。…*1
解決策3 アンチパターンではあるが集約間の整合性維持のためトランザクション制御を用いる
こちらは解決策1と比べて一定の言及はあるものの、やはり積極的な支持はほぼありませんでした。
解決策4 ユースケースの見直しによる再モデリングタイプのアイデア
2番目に多かったのがこのタイプです。 ユースケースの見直し、具体的にはユーザー上限ルールの稀な逸脱を許容する方向のアイデアが多かったです。
- katzchangさん: 契約ルールの方を柔軟にするかなぁ。固定金額契約でも、多少のオーバーは運用上許容してるサービスは多い印象がある。リアルタイムにbanする必要はないので、定期チェック機構を組織のほうに作るとよさそう。
- 増田 亨.さん: 完全に防ぐのは難しいですね。 大規模に並列で運用しているところは、厳密な制限の作り込みよりも、ある程度の不整合を許容する前提で設計と運用する方向を模索しているように思います。 事後チェックとか。…
- こむさん: 一般論なら結果整合で、 (略) 同時追加なんてそんな無いはずと信じて31人になるくらいは雑に許容してしまう、かな。
- 認定ジャバさん: 契約ルールの方を柔軟にするかなぁ。固定金額契約でも、多少のオーバーは運用上許容してるサービスは多い印象がある。リアルタイムにbanする必要はないので、定期チェック機構を組織のほうに作るとよさそう。
僕の現時点での回答らしきもの
まず解決策1と3はほとんど支持されていなかったため、一旦除外しました。 一定のアイデアが出ていた解決策4ですが、こちらも一旦許容しない(できない)として解決策2での手法を模索することにしました*2。
次に解決策2タイプのアイデアを見ていくと、ほとんどの人がユーザーの有効/無効の状態をユーザー集約側ではなく組織集約側に持たせようとしています。 考えてみるとたしかにユーザーが有効かどうかは組織側が持っていたほうが自然です*3。
また、頂いていた中でイベント駆動アーキテクチャ的な非同期的、長期プロセス(サーガ)的方法を一旦後回しにしました。 従来の同期的でコンパクトな方法で済むならそのほうが良い気がするのが半分、現状僕がどうしてもそちらの知識・経験が少ないため自身を持って設計できないことが半分です*4。
というわけで、残った解決策2の中から最終的に選択したのがB.i案です。
現時点での回答: 解決策2 B.i 案
ユーザーエンティティが持っていた有効/無効の状態が、組織エンティティ内の有効ユーザーのユーザーID一覧という形に変わっています。 またユーザーの組織への所属関係もユーザーエンティティに持たせるのではなく組織エンティティに持たせるようにしました。
コードは以下のように変わりました。
// アプリケーション層のどこか def `ユーザーを有効状態で追加`( `組織ID`: Int, `ユーザー名`: String, `メールアドレス`: String, `ハッシュ化されたパスワード`: String): Try[`ユーザー`] = { val `組織インスタンス` = `組織リポジトリ`.find(`組織ID`).get if (! `組織インスタンス`.有効ユーザーが上限に達している()) { // ① val `ユーザーインスタンス` = `ユーザーファクトリ`.create(`ユーザー名`, `メールアドレス`, `ハッシュ化されたパスワード`) `ユーザーリポジトリ`.save(`ユーザーインスタンス`) // ここから `組織インスタンス`.有効状態でユーザーを追加(`ユーザーインスタンス`.`ユーザーID`) `組織リポジトリ`.save(`組織インスタンス`) match { // ② case Success(_) => Success(`ユーザーインスタンス`) // ③ case Failure(e: VersionChangedException) => `ユーザーリポジトリ`.delete(`ユーザーインスタンス`.`ユーザーID`) // ④ Failure(e) } // ここまでが追加された } else Failure(new `有効ユーザー数上限Exception`) }
以前と同じ例を使うと以下のようになります。
- 有効ユーザー数上限が30, 現在の有効ユーザー数が29
- ユーザー追加メソッドが並行でほぼ同時に2度呼ばれる(それぞれをコールA, コールBとする)
| 時系列 | コールA | コールB |
|---|---|---|
| 1 | ①が実行され、29 < 30であるため②に進む | |
| 2 | ①が実行され、29 < 30であるため②に進む | |
| 3 | ②が実行される。 読み込んだときから状態(バージョン番号)が 変わっていないため成功して③に進む |
|
| 4 | ②が実行される。 読み込んだときから状態(バージョン番号)が 変わっているため失敗して④に進む |
|
| 5 | Successが返る | |
| 6 | ④が実行され、有効化できなかったユーザーを削除しておく | |
| 7 | Failureが返る |
これにより有効ユーザー数は上限を超えることなく維持できました。
これだとユーザー上限に達していなくても更新が重なるとコールBで例外が発生しますが、メソッド呼び出し側で例外がVersionChangedExceptionだった場合はリトライすればOKです。
④のユーザー削除は意見の分かれる所かもしれません。 私はユーザーインターフェイス層から見たとき、このメソッドがユーザーの有効状態での追加 or 何もしない*5で返してくれると便利だと思いこのようなメソッドにしました。組織に属しない宙ぶらりんなユーザーが残っていると混乱しやすいと思ったためです。 逆にユーザーインターフェイスからユーザーの無効状態の追加と有効化を細かい粒度で制御したい場合はこのメソッドは内容を2つに分離していいと思います。
もう1つ言及したい点が④を実行前にこのアプリケーションやコンテナが死んだ場合で、そのときは宙ぶらりんなユーザーが残ってしまいます。しかし、この状態のユーザーは検出がしやすく残っていても問題になりづらいという点でより許容しやすいと思いました。 これが後述のA.i案よりこちらを選んだ最も大きな理由です。
ほぼBiと同じ: 解決策2 B.ii 案
こちらはB.i案のユーザー一覧に関する値オブジェクトを独立した集約に切り出したものです。 今の組織集約は非常に簡素なため今すぐ切り出す必要性はないように見えますが、組織エンティティに機能や要素がもっと増えてきたらこうした方がよいでしょう。
コード的にはほぼ同様になるため省略します。
一長一短あり: 解決策2 A.i
この案ではユーザーの有効/無効状態のみ組織集約に移しており、どのユーザーがどの組織に属するかの情報はユーザー集約に残しています。 コードはほぼ同じですので割愛します。
このパターンはB.i 案と比べていくつかのメリット・デメリットがあります。
- メリット
- デメリット
- 相互参照は注意して扱わないと簡単に整合性が取れなくなってしまう*7
- 例: 有効な状態のユーザーをユーザーエンティティの組織IDだけ別の組織へ変えてしまうと、元の組織にその組織へ属さないにもかかわらず有効登録されたユーザーIDが残ってしまう
- 相互参照は注意して扱わないと簡単に整合性が取れなくなってしまう*7
また特筆するべき困った点が④を実行前にこのアプリケーションやコンテナが死んだ場合で、B.i案とは違い先に作られたユーザーは実際に組織へ無効状態で追加されています。 そのため、他の単純に無効化されているユーザーと区別することが難しく、リカバーにより手間がかかると感じました。
まとめ
というわけで、一旦現時点では解決策2 B.i案の「ユーザー一覧を組織集約に持たせるアイデア」が良いと思いました。 むろんこれが正解だという確信はまったくありません。細かい条件の違いで如何用にも最適解は変わりますし、僕が説明されていただいたシチュエーションでも2 B.i案が本当に最良か、試してみないことにはわかりません。
頂いたアイデアのおかげで考えがまとまりました。 一人で考えるだけではここまで早く良い案には辿りつけなかったと思います。 本当にありがとうございました。あとは実際にやってみて出した案が本当に良いか確かめようと思います。
2018/12/12 追記
なるほど理解できました。ドメインの不変条件・制約条件とその前提をどう折り合いつけるかという話だったんですね。
— cactuaroid (@cactuaroid) 2018年12月11日
if文の箇所を組織.ユーザー追加可能か()に置き換えるのと、組織.有効状態でユーザーを追加()内部でユーザー数上限チェックをすると良さそうに思います。
cactuaroid(@cactuaroid)さんから上のような指摘がありました。 それと、ファクトリによりIDがcreate時に発行される前提で書いたコードが以下です。 もっとモナドらしい自然な書き方ができると思うんですが、慣れていないので愚直な書き方で失礼します。
ユーザーはsaveできたが組織インスタンスが何らかのエラーでsaveできなかったときのために①を書いていますが、これを省くと更にコードが完結になります。 ただ、その際はそうやって発生する不整合状態を復帰させる(どの組織にも紐付かないユーザーを削除する)ようなデーモンないし仕組みが欲しくなるでしょう。
2019/01/20 追記
AUTO_INCREMENTだと先にユーザー集約を保存しないとIDが決定できない。組織での更新が失敗すると手動でロールバックする。AUTO_INCREMENTやめて事前ID採番やって、ユーザー集約本体を保存せずに、組織を更新したらよいです。失敗したら欠番になるだけでロールバック処理は不要です。
— 加藤潤一(かとじゅん) (@j5ik2o) 2019年1月18日
https://twitter.com/j5ik2o:かとじゅんさんから上のようなアイデアをいただきました。 (僕が勘違いをしていなければ)そのアイデアを元に書いた疑似コードが以下です。
cactuaroidさんから頂いたアイデアのコードと比べると、集約の保存の順番が ユーザー集約 → 組織集約 から 組織集約 → ユーザー集約 に変わっています。 楽観的ロック的に考えると組織集約の更新は同時に同じ集約へ更新が走ると衝突する可能性が一定あるのに対してユーザー集約の新規保存は衝突する可能性が(ほぼ)ありません。 そのため、仮で作ったユーザーを削除する必要がないという点で優れていると言えます。
一方で、組織集約の更新直後にアプリケーションがkillされたりDBクラスタがダウンしたりする可能性を考慮すると少し厄介です。 組織集約のユーザーID一覧に実際には存在しないユーザーのユーザーID(以後ゴーストユーザーID)が残ってしまうため、集約を参照する側はユーザーID一覧内のユーザーIDが実在しない可能性をコード中で考慮する必要が出てきます。 また、そのようなゴーストユーザーIDを定期的にクリーンアップする必要が出てきますがこれは2018/12/12の案での組織に紐付かないユーザーを削除するクリーンアップ処理に対応します。
※ すみません、ここら辺の結果整合性とシステムダウンの関係と復帰のさせ方については勉強不足かつ考え不足で自分の言葉にできていないので、後日また記事を書いて言葉にしようと思います。
関連リンク
集約の境界と整合性の維持の仕方に悩んで2ヶ月ぐらい結論を出せていない話
本記事はドメイン駆動設計 Advent Calendar 2018 - Qiitaの3日目の記事です。
2日目は、grimroseさんのぐるぐるDDDで気をつけてることでした。
4日目は、s_edwardさんのMicroservices と DDDです。
Table of Contents
以下の記事を読むにあたり前提となる知識
以下の話はドメイン駆動設計および実践ドメイン駆動設計を読んでいることがある程度前提になります。 特にIDDD第10章「集約」で説明されている用語定義や例に強く依存しています。ご了承ください。
またサンプル中のコードはScalaで書かれています。それほど難しい構文は出てきませんが、多少の知識がないとわかりづらいかもしれません。
問題
現在仕事で関わっているサービス開発において、集約の境界と集約間の整合性の取り方をどうするか(トランザクション制御 or 結果整合性)について悩んでいます。
サービス詳細
後の説明でイメージしやすいようにするため、以下のようなサービスをイメージしてください。
- 1種の社内向けブログサービス
- 有償サービスであり、個々人ではなく組織単位での課金となる
- サービス料金は組織内の有効ユーザー数1による(Github Teamライク)
ユビキタス言語
| ユビキタス言語 | 詳細 |
|---|---|
| ユーザー | 記事を書く人。 メールアドレス・パスワードによって一意に特定される。 また特定の実在個人と1対1でマッピングされる。 ユーザーは必ず1つの組織に所属する。 組織は有効ユーザー数上限に達しない限りはいつでもユーザーを追加できる。 |
| ユーザーの有効/無効 | 本サービスではユーザーは削除することができず、無効化のみすることができる。 無効状態のユーザーは有効ユーザー数上限に達していない限り再び有効化できる。 無効状態のユーザーは記事を新たに書くことができない。 無効状態のユーザーは有効ユーザー数にカウントされない。 |
| 記事 | いわゆる普通のブログ記事。 ユーザーによって書かれる。 |
| 組織 | 複数のユーザーを含む。 ユーザー数は場合によっては数千にもなりうる。 契約はこの組織単位でのみ行われる。 また契約時にはアクティブユーザー数上限を決める。 |
| 有効ユーザー数上限 | 組織は有効状態のユーザーが契約上の有効ユーザー数上限に達しているとき、新たにユーザーを追加できない。 |
※ 実際にはこんなに単純ではないと思いますが主題にフォーカスするため絞って書いています。
重要なビジネスルール
- 組織内の有効状態のユーザーの総数は契約時の有効ユーザー数上限を常に超えない(以後ユーザー上限ルール)
- 無効状態のユーザーは記事を書くことができない
モデリング
以上をドメインエキスパートから聞き取りを行ったのち、一旦以下のように集約やエンティティをモデリングしました。
基本的にモデリング過程はDDD本例6.1の「購入注文の整合性」およびIDDD本10章の例を踏襲しました。 組織、ユーザー、記事はそれぞれが一つのエンティティでありかつそのエンティティ1つのみを含む集約の集約ルートでもあります。 IDDDで推奨される集約間はIDでのみ相互参照するべき、というプラクティスに従い、組織とユーザーおよびユーザーと記事の親子関係はID値オブジェクトで表現しました。
各リポジトリのメソッドはscalikejdbcで自動生成されるものと同じ機能とします2。
上の何が問題?
重要なビジネスルールの1つであるユーザー上限ルールが組織とユーザーという2つの集約にまたがってしまっています。その結果、ユーザー上限ルールを維持することが難しくなっています。
// ドメイン層のどこか def `ユーザー追加`( `組織ID`: Int, `ユーザー名`: String, `メールアドレス`: String, `ハッシュ化されたパスワード`: String): Try[`ユーザー`] = { val `組織インスタンス` = `組織リポジトリ`.find(`組織ID`).get val `有効ユーザー数` = `ユーザーリポジトリ`.count有効ユーザー数(`組織ID`) // ① if (`有効ユーザー数` < `組織インスタンス`.`有効ユーザー数上限`) // ② Success(`ユーザーリポジトリ`.create(`組織ID`, `ユーザー名`, `メールアドレス`, `ハッシュ化されたパスワード`)) // ③ else Failure(new `有効ユーザー数上限Exception`) }
IDDD本に従いトランザクション制御は集約内(≒単一リポジトリ内)で完結させているとすると3、上のコードは一切集約間の整合性の維持を気にかけていません。ですのでユーザー上限ルールは以下のように容易に破綻します。
- 有効ユーザー数上限が30, 現在の有効ユーザー数が29
- ユーザー追加メソッドが並行でほぼ同時に2度呼ばれる(それぞれをコールA, コールBとする)
- コールAで①が実行され、29が取得される
- コールBで①が実行され、29が取得される
- コールA/Bどちらでも②は
29 < 30であるため③に進む - コールAで③が実行される
- コールBで③が実行される
- 最終的に有効ユーザー数は31となり、有効ユーザー数上限を超えてしまう
DDD的には以下の4つの解決策が提示されています。
- 解決策1 集約をマージして一つのトランザクションにする
- 解決策2 集約間の整合性維持の基本プラクティスである結果整合性を用いる
- 解決策3 アンチパターンではあるが集約間の整合性維持のためトランザクション制御を用いる
- 解決策4 ユースケースの見直し
解決策
解決策1 集約をマージする
2つの集約が1つになればそのままトランザクション制御で整合性を維持できるので一時的な整合性の破綻も起きなくなります。DDD本例6.1では集約A(注文書)と集約B(個別の品目リスト)を一つの集約として扱えると判断した結果、この問題を解決しています。
ただし集約内の要素へのアクセスは必ず集約ルートを通じて行う必要があるため、ユーザーが独立した集約だったときのように記事エンティティがユーザーIDを持つことはできません。 このアイデアを適用使用した結果、最終的に以下のようなモデルになりました。
ユーザーエンティティと組織エンティティを一つの集約にしたことにより、ユーザー追加メソッドは以下のようになりました。
def `ユーザー追加`( `組織ID`: Int, `ユーザー名`: String, `メールアドレス`: String, `ハッシュ化されたパスワード`: String): Try[`ユーザー`] = { val `組織インスタンス` = `組織リポジトリ`.find(`組織ID`).get // ① if (`組織インスタンス`.`有効ユーザー数が上限に達している`()) { val `ユーザーインスタンス` = `組織`.addユーザー(`組織ID`, `ユーザー名`, `メールアドレス`, `ハッシュ化されたパスワード`) `組織リポジトリ`.save(`組織インスタンス`) // ② Success(`ユーザーインスタンス`) } else Failure(new `有効ユーザー数上限Exception`) }
もし仮に2つのメソッドコールが同時並行に走ってA①, B①, A②, B②と実行されたとしても、バージョン番号による楽観的ロックを行っていればB②でエラーが発生します。その場合は再度このメソッドをやり直せば良いです。 あるいはユーザー追加メソッド自体をリポジトリのメソッドにすれば楽観的ロックを使わずトランザクション制御を直接使うこともできます4。
この方法の問題は大きく2つあります。 1つは集約が大きくなることで、今回の場合ユーザー数は1000を超える可能性がありメモリ消費量5や同時編集の可能性、クエリのパフォーマンスの劣化などの問題が出てきます。 もう1つはオブジェクトグラフが複雑になることで、ユーザーエンティティが集約ルートではなくなるため記事エンティティは必ず組織集約ルートにアクセスしてからユーザーへアクセスする必要があります。 例えば記事IDから記事タイトルとユーザー名をペアにして返すようなメソッドを書く場合、以前であれば記事エンティティ内のユーザーIDから単純に辿れたものが、今の状態だと組織リポジトリのfindユーザーメソッドを使う必要があります6。
- 利点
- 整合性の一時的な破綻の可能性はなくなる
- 欠点
- ユーザーエンティティの情報を参照するときに必ず組織集約ルートを経由する必要があるためややこしくなる
- 集約が大きくなることの影響(更新のコンフリクト・ロック時間の増大・DBからの読み込み/への書き込みパフォーマンスの悪化)
解決策2 一時的な整合性の破綻を受け入れ結果整合性を使う
IDDD本の書くとおり、複数の集約間の整合性は結果整合性で行うパターンです。
モデルは以前のままですが、コードに手を入れる必要があります。
def `ユーザー追加`( `組織ID`: Int, `ユーザー名`: String, `メールアドレス`: String, `ハッシュ化されたパスワード`: String): Try[`ユーザー`] = { val `組織インスタンス` = `組織リポジトリ`.find(`組織ID`).get def `有効ユーザー数`(): Int = `ユーザーリポジトリ`.count有効ユーザー数(`組織ID`) if (`有効ユーザー数`() < `組織インスタンス`.`有効ユーザー数上限`) { val `ユーザーインスタンス` = `ユーザーリポジトリ`.create(`組織ID`, `ユーザー名`, `メールアドレス`, `ハッシュ化されたパスワード` if (`組織インスタンス`.`有効ユーザー数上限` < `有効ユーザー数`()) { // ① `ユーザーインスタンス`.`無効化`() `ユーザーリポジトリ`.save(`ユーザーインスタンス`) Failure(new `有効ユーザー数上限Exception`) } else Success(`ユーザーインスタンス`) } else Failure(new `有効ユーザー数上限Exception`) }
正直なところ、今回のケースでうまく結果整合性により整合性を維持する方法がうまく浮かびませんでした。 そこで、ユーザー追加後にもう一度有効ユーザー数をチェックしてもし上限を超えていた場合は作成したユーザーを無効化しています。 ただ、この手法にも多くの問題があります。最も問題なのは①のタイミングでDBが停止したりアプリケーションが停止した場合に整合性が破綻したままで残ってしまう問題です。 KinesisやKafkaのような永続化キューを経由することでその問題は解決できますが、今までドメインイベントやPub/Subモデルを利用していなかった場合はかなり大掛かりな変更になるでしょう。 他にも無効化したユーザーが残ってしまう点も問題になるケースはあるかもしれません。例えばユーザーエンティティ単体でメールアドレスが一意になる制約を課している場合、結果整合性のために自動的に無効化されたユーザーがいるとハンドリングが難しくなる気はします。
- 利点
- 欠点
- 瞬間的とはいえ整合性が一時的に破綻することを許容しないといけない
- トランザクション制御と比べ確実に整合性を維持するためのコードは増える。また仕組みも大掛かりになる
解決策3 アンチパターンではあるが集約間の整合性維持のためトランザクション制御を用いる
IDDD本では解決策1でも2でも4でもうまくいかないときの例外として、複数の集約間でトランザクション制御を使う場合について言及しています。 今回のケースだとIDDD本p354の「理由その2:技術的な仕組みの欠如」に当たるでしょう。
モデルはほとんど以前のままですが、集約エンティティに現在の有効ユーザー数とそれを増減させるメソッドを持たせています。
def `ユーザー追加`( `組織ID`: Int, `ユーザー名`: String, `メールアドレス`: String, `ハッシュ化されたパスワード`: String): Try[`ユーザー`] = { DB localTx { session => val `組織インスタンス` = `組織リポジトリ`.find(`組織ID`)(session).get val `有効ユーザー数` = `ユーザーリポジトリ`.count有効ユーザー数(`組織ID`)(session) if (`有効ユーザー数` < `組織インスタンス`.`有効ユーザー数上限`) { val `ユーザーインスタンス` = `ユーザーリポジトリ`.create(`組織ID`, `ユーザー名`, `メールアドレス`, `ハッシュ化されたパスワード`) `組織リポジトリ`.save(`組織インスタンス`.`有効ユーザー数+1`())(session) Success(`ユーザーインスタンス`) } else Failure(new `有効ユーザー数上限Exception`) } }
- 利点
- 整合性が一時的にでも破綻する可能性はなくなる
- 集約のモデリングは自然なまま。ユーザー集約と他の集約間の関係も自然に表現できる
- 欠点
解決策4 ユースケースの見直しによる再モデリング
そもそものモデルのユースケースを見直すことで、集約やエンティティなどを再度モデリングする方法です。 ドメインエキスパートを交えてある程度検討してみたのですが、もともとユーザー追加というかなり単純なユースケースであることもあってユースケースの見直しというアプローチは厳しそうでした。
まとめ
とりあえず今どうやっているか
解決策2の結果整合性による手法を取っています。 とはいえこれは積極的な選択によるものではなく、もともとそうやっていたこと、実際に破綻が起こる可能性はほぼありえないことなどが理由です。
ただ、ほとんど起こり得ないとはいえ整合性が破綻した瞬間にアプリケーションが停止するとDBの整合性が取れなくなる可能性があること、現状それを能動的に検知する手段を確立していないことなどは確実に心理的負担になっています。
最終的にどうするべきだと考えているか(2018/12/01時点)
解決策2の結果整合性による方法か、解決策3の部分的なルール違反を受け入れるかのどちらかだと思っています。
ただし前者を選択する場合は今よりもっとコードを書く必要があるでしょう。また今まで使用していないドメインイベントや永続化キューの導入が必要になります。
後者の場合はそのような仕組みづくりは必要ありませんが、集約をまたがってトランザクションを制御する必要があるためそれをドメイン層やプレゼンテーションアプリケーション層へ持ち込むのか、持ち込む場合はどういった方法を取るのか(トランザクションの概念を抽象化する/しない)、持ち込まない場合はどうやってインフラストラクチャ層でトランザクション制御を完結させるのかなどを検討する必要があります。
ソリューション募集中
こうしたらいいんじゃないか、この論理展開おかしくない?エヴァンスやヴァーノンはそうは書いていないぞ/解釈間違っているぞ、等々ありましたら是非教えてください。twitterでもブログのコメント欄でもなんでも構わないです。
2018/12/04 追記
2,3人ぐらいから反応してもらえたら御の字と思っていたんですが想定していた以上のフィードバックがあって本当に嬉しく思っています。 このままだと頂いたアイデア・考えが散ってしまってもったいないので、できれば今週中ぐらいで頂いたアイデア・考えをまとめて(あとそれらを教えてもらった上で今僕がどうするべきだと思っているかも)このブログの次の記事としてアップしようと思います。乞うご期待ください。
2018/12/10 追記
書きました!
参考文献
Clean Architecture
Clean Architecture 達人に学ぶソフトウェアの構造と設計【委託】 - 達人出版会
簡潔でよくまとまった本文、質の高い翻訳。 DDD本がいささか冗長気味な点に対してこちらは要点を絞って書いてある。 章が多い代わりに簡潔であとで読み返しやすい。
本書で最重要なのはSOLIDの原則で、すべての章で出てくると行ってもいいほど重要。 またそれを起点に1アプリケーション内だけでなく会社規模でのアーキテクチャについてもかなり説明してくれていたのはありがたかった。これは今の会社での仕事ですぐに生きそう。
良い本だった。
clean architecture 読んで心に残った箇所の覚書
Clean Architecture 達人に学ぶソフトウェアの構造と設計【委託】 - 達人出版会
アーキテクチャは昔から作り方がずっと一緒、おれがその最終解を教えてやる
眉が唾液でベトベトになる
第3章 パラダイムの概要
if for whileは50年変わっておらず、プログラミングの主要なパラダイムは関数型、構造化、オブジェクト指向の3つで、これらは全て1958〜1968にみつかった。だから最終解がある。
なるほど、それを聞くと一定の説得力がある気がする。
第4章 構造化プログラミング
だいたい同意できる
第5章 オブジェクト指向プログラミング
cを引き合いに出してooのカプセル化をディスっているところ、論理展開がよくわからない。訳が微妙なせい?
やたらcを引き合いに出す。さすがに偏見を感じる。
第6章 関数型プログラミング
エベントソーシングとの関連付けは見事。
繰り返し述べられる、プログラミングパラダイムは進化していないと言う主張を今では完全に信じている。
第III部 設計の原則
バズワード的に捉えていたsolid原則がすんなり頭に入る。
第7章 単一責任の原則
前半の説明は違和感なく納得できるが後半の説明がわからない。
関数を別のクラスへ分離するのはともかく、employeesataを共有するということはデータ構造、あるいはデータそのものも共有している?それだと意味なくない?
ここから風呂で本を読んでメモを怠る
どこだかで書かれていた、最終的にはトップレベルではDIを注入しないといけないという記述は自分がやっていることが正しいと裏付けできて非常に心強くなった。
第IV部 コンポーネントの原則
第14章 コンポーネントの結合
ここで出てくるメトリクスを昔のように乱用しないこと。
あくまで重要なのはここでの考え方が重要で、メトリクスはその目安にすぎない。 メトリクス指標を追いすぎると本来の考え方が軽視されてしまう(手段と目的の逆転)。
第V部 アーキテクチャ
第15章 アーキテクチャとは?
話がかなり抽象的になったためかまた一気にわかりづらくなる。
言いたいことはなんとなくわかるが古い例え話が多いこともありすっと頭に入ってこない。今後の章を読んで理解が深まることを期待して一旦理解を保留。
第16章 独立性
気をつけてほしい。反射的に重複を排除する罪を犯してはいけない。その重複が本物かどうかを見極めるべきだ。
DRY偏重な考え方に対するカウンター。確かに僕もこれで痛い目を見たことが何度か。
私の好みは、いざというときのために、サービスを作れそうなところまで切り離すというものである。
なるほど、だいぶわかってきた。
私が言っているのは、システムの切り離し方式は時間とともに変化する可能性があるということだ。
第17章 バウンダリー:境界線を引く
著者が開発していたFitNisseについて、最後までストレージの意思決定を遅らせた結果、当初予定していたMySQLは最終的に不要とわかりファイルシステムへ書き出すだけでOKになった。
> これも単一責任の原則(SRP)である。単一責任の原則(SRP)はどこに境界線を引けばいいかを教えてくれる。
これは、依存関係逆転の原則(DIP)と安定度・抽象度等価の原則(SAP)を適用したものであると認識するべきだ。依存性の矢印が詳細レベルから抽象レベルを指すようになっている。
第18章 境界の解剖学
モノリスからパッケージレベルの分離、そしてもっとも大きな粒度の分離である(マイクロ)サービスレベルの分離など。
第19章 方針とレベル
度々引き合いに出されるSOLID則の汎用性にはちょっとビビる。 これを覚えておけば、すべての間違ったアーキテクチャへの反論として使えるのでは?
いや、でもそれ相手の知らない言葉で相手を押さえつける一番僕が嫌っていた手法だったわ。 相手を納得させたいなら、あくまで相手の言葉と相手の問題意識に沿った方法でやらんといかん。
第20章 ビジネスルール
では、なぜエンティティが上位レベルで、ユースケースが下位レベルなのだろうか? ユースケースはアプリケーション固有なので、システムの入力と出力に近い。エンティティは複数のアプリケーションで使用できるように一般化されているので、システムの入力と出力から遠く離れている。したがって、ユースケースはエンティティに依存し、エンティティはユースケースに依存していないのである。
clean architectureとlayerd architectureのパット見てわかる差別点である、ドメイン層とエンティティ層/ユースケース層の区別。 clean architectureではビジネスロジックを実装するための層としてエンティティ層とユースケース層を分けている。 その背後には上のような、両者のレベルの違いという考えがあるようだ。
第21章 叫ぶアーキテクチャ
うーん、書いてあることは理解できるがまとめるに困る章。 システム設計者がエンティティ・ユースケースへちゃんとビジネスロジックを集約していることと、そのコードを読む人がclean architectureを理解している前提がないと、自明なアーキテクチャという理解の合意は難しいと思うんだがね。 まあ提供方法次第か。
第22章 クリーンアーキテクチャ
ヘキサゴナルアーキテクチャ、DCIアーキテクチャ、BCE(そしてたぶんレイヤー化アーキテクチャも)、いずれも「関心事の分離」という同じ目的を持っている。中略。また、それぞれ少なくとも、ビジネスルールのレイヤーと、ユーザーやシステムとのインターフェイスとなるレイヤーを持っている。
例の有名なクリーンアーキテクチャの図、コントローラとプレゼンターがクラシカルなウェブアプリの入力(HTTPリクエスト)と出力(HTTPレスポンス、それもHTMLをイメージした)であることがわかったことが収穫。
第23章 プレゼンターとHumble Object
まずは 、前提を明らかにしよう 。 O R M (オブジェクトリレ ーショナルマッパ ー )というものは存在しない 。
相変わらず主語が大きいことを言い切るスタイル。
O R Mシステムはどこに属するのだろうか ?もちろんデ ータベ ースのレイヤ ーだ 。
まあじか、僕が思っていたより相当多層的だな
第24章 部分的な境界
YAGNIの法則には反するが、著者は予防的な構造化を行う。 ただし、明確に境界を定めないとやがて境界線が曖昧になり依存性が双方向など違反してしまいがち。
第25章 レイヤーと境界
GameRulesの内部を見ると、GameRulesに含まれるコードが使用して、Languageのコードが実装しているポリモーフィックなBoundaryインターフェイスが見つかる。同様に、Languageに含まれるコードが使用して、GameRulesのコードが実装しているポリモーフィックなBoundaryインターフェイスも見つかる。
長いこと読んで初めて違和感のある訳に遭遇。 これは原文に忠実に訳しようとした結果日本語へマッピングしづらかった例なのでまだ解読しやすい。 語順を無視して日本語として読みやすい形にすると以下ぐらいか。
GameRulesの内部を見ると、Languageのコードで実装されているポリモーフィックなBoundaryインターフェイスがGameRulesに含まれるコードで使用されているのが見つかる。同様に、GameRulesのコードで実装されているポリモーフィックなBoundaryインターフェイスがLanguageに含まれるコードで使用されていることも見つかる。
ここまでにやってきたことは何を意味するのだろうか? 200行のKornshellで実装できるシンプルなプログラムに、どうしてわざわざアーキテクチャの境界を作ったのだろうか? この例は、アーキテクチャの境界があらゆるところに存在することを示している。我々アーキテクトは、それがいつ必要になるかに気を配らなければいけない。また、境界を完全に構築しようとすると、コストが高くつくことを認識する必要がある。
ふんふんとそれらしいなと読んでいて章の最後でちゃぶ台返し。 そうよね、やっぱり境界区切ればよいってもんじゃないよね。
しかもこれは、1回限りの決定ではない。プロジェクトの開始時に、実装する境界と無視する境界を決めればいいわけではない。常に見張る必要がある。
アーキテクチャは常に進化・変化すること。またアーキテクチャが常にリードエンジニアでもあるべき理由。
第26章 メインコンポーネント
すべてのコンポーネント/レイヤーの最も外側、もっとも下位レイヤーであるmain関数の話。 (DIはここで行われる。これ重要。)
clean architectureの図だけ見ているとこのDIや最も汚い組み立てをどうするのかがわからなくなりがち。 というのもこの章で語られていることはかなり控えめにしかかかれないから(この章も最終盤だし)。 個人的にはclean architectureの最も外側の層として図示してもいいレベルで重要だと思うんだけど(初学者がわからなくなりがちな点も考慮して)。
Mainをアプリケーションのプラグインと考えよう。初期状態や構成を設定して、外部リソースを集め、アプリケーションの上位レベルの方針に制御を渡すプラグインである。プラグインなので、アプリケーションの設定ごとに複数のMainコンポーネントを持つこともできる。 たとえば、開発用、テスト用、本番用のMainを用意することもできる。あるいは、デプロイする国別、権限別、顧客別に用意することもできるだろう。 Mainをアーキテクチャの境界の背後にあるプラグインとして考えると、設定の問題はもっと解決しやすくなるはずだ。
なるほど、この考え方はテスタビリティを改善するかも。
第27章 サービス:あらゆる存在
●サービスが互いに分離されているように見えるから。あとで説明するが、これは部分的にしか正しくない。 ●サービスが開発とデプロイを独立させているように見えるから。これもあとで説明するが、部分的にしか正しくない。
初手ビーンボールはMartinの変わらぬスタイル。
これは「横断的関心事」の問題である。あらゆるソフトウェアシステムは、サービス指向であろうとなかろうと、この問題に直面することになる。図27-1のサービス図のような機能分割は、すべての動作に影響を与える新機能の追加に対して非常に弱い。
なるほど、機能ベースでのマイクロサービスへの分割はこのような結果になるのか。
これしかしKitten配達サービス始める前にこのような抽象化を始めるのはYAGNIの法則に強く違反していないかなあ。それこそ抽象化の方法はいくらでもあるように思える(1つのりんごを2つに切る方法が無数にあるように、抽象化の方法も無数にあるのでは?)
横断的関心事
ココらへんがちょっとよくわからない。 横断的関心事についてあとで調べたほうが良いかも。
第28章 テスト境界
こうした状況は深刻化する可能性がある。共通のシステムコンポーネントを変更すると、何百や何千というテストが壊れる可能性がある。これは、 脆弱なテストの問題(Fragile Tests Problem) と呼ばれている。
確かに。
テストAPIの目的は、テストをアプリケーションから分離することである。この分離は、UIからテストを切り離すことだけではない。アプリケーションの 構造 からテストの 構造 を切り離すことが目的である。
テストAPI
具体的なイメージがわかない。 関数/メソッドレベルでの結合を避ける必要がある? わかりづらいのでここは後で読み直そう。
第29章 クリーン組込みアーキテクチャ
この章長いが、ちょっと読むと本当に組込ソフトウェアの話がメインっぽいので流し読みでも良いかも。
Kent Beckが、ソフトウェアを構築する3つの活動について、以下のように説明している(強調は著者による)。 1. まずは、動作させる。動作しなければ、仕事にならない。 2. それから、正しくする。あなたやほかの人たちが理解できるようにコードをリファクタリングして、ニーズの変化や理解の向上のためにコードを進化させていく。 3. それから、高速化する。「必要とされる」パフォーマンスのためにコードをリファクタリングする。
基本にして奥義、みたいな話やな。
この章で言いたいことの半分くらいはコレの気がする。
組込みアプリケーションが特殊なツールチェーンを使用している場合、「便利な」ヘッダーファイルが提供されていることがある
唐突に挿入されるインデントタグ。多分コピペで入っちゃって校正すり抜けちゃったっぽい。
第VI部 詳細
(訳注:「詳細」を意味する「detail」には、「些細」という意味もある。本書では「詳細」で統一しているが、両方の意味が含まれることに注意してほしい。)
いいっすねー。こういった手を抜かない翻訳姿勢。 こういった注釈を書く、書ける、書く価値がある、書くテクニックがあることは自分が翻訳文章書くときのために覚えておこう。
第30章 データベースは詳細
過去スタートアップでRDBMSの不要さを説いて、結果的に追い出された話、切ない。 そういうもんよな。
第31章 ウェブは詳細
計算をサーバサイドで主にするかクライアントサイドで主にするかの揺れ・揺り戻しを振り子と表現するのは言い得て妙。
ただ、WebサイドのアプリケーションでGUIとビジネスロジックの分離が難しいと著者が認めているのは2018年時点では現実的か。 それでもreactなどの解答はできつつある気はする。
第32章 フレームワークは詳細
フレームワークの作者に対して辛辣すぎじゃない?w rails系の人への批判なんだろうか。少なくともsinatraはそんな思想ではないと思うけど。
まあたしかにplayやakka-httpのテストフレームワークはダサすぎてあれ一緒に使うの矯正されるのは嫌だけど。
これは現在のプロジェクトでakka-httpを別レイヤーとして分離すること、できたことで多少なりともメリットを実感できた(多くのboundaryクラスを書くことと引き換えだったけど)。 どちらかというと長期的なメンテナンスで徐々にこのメリットを感じていくと思う。
第33章 事例:動画販売サイト
意外と短くて例としてはあまり役に立たなかった。 これ後で書こうとも思っていたけど具体例がかなり少ないのが辛い。 実践ドメイン駆動設計を読んでいる途中のためもあるけど、もっと現実に近い例がみたいね。
第34章 書き残したこと
今のチームでそろそろ導入しようと思っていた、コンパイル前のimportのルール違反チェック機構がここで紹介されているのを見て、ああ、他にも同様に考えている人がいたという安心。
そうよね、そしてやはりコンポーネントによるパッケージングはいわゆるsbtでいうモジュール、javaでいうjar単位の分割か。
「コンポーネントによるパッケージング」の大きな利点は、もし注文に関するコードを書きたいのなら、OrdersComponentだけを見ればいいという点だ。
これなんよなあ。大抵のカウボーイ型のコーダーは本質的に自分しか見ることを考えないから、こう、変更必要な箇所が絶対的に特定かつ限定されるというオープンクローズドの原則の力を過小評価しがちなんよな。
あと読んでいて機能ベースのパッケージングを早急にしないとと思った。 あれ縦割り横割りって選択肢じゃなくて両方やるもんなのな。 機能単位でパッケージングしたのち、レイヤーの違いはコンポーネントで表現すると。
オブジェクト指向についてそろそろ一席ぶっておく
きっかけ
たまたま?最近よくオブジェクト指向の理解方法について、続けていくつかの言及を読んだ。
オブジェクト指向を「わからなく」する三つの方法。
— 増田 亨. (@masuda220) 2018年9月26日
1. 現実世界の「物」をオブジェクト指向の「オブジェクト」だと説明する。自動車、犬猫、炊飯器に電子レンジ...
2. 継承、ポリモーフィズム、カプセル化をオブジェクト指向の三点セットと説明する
3. UMLクラス図ではデータモデリングする
1つ目はまさに。
— 🤓k.bigwheel🤓 (@k_bigwheel) 2018年9月26日
結局自分は4,5年迷走したのち習うより慣れろで自分の回答はある程度出た。 https://t.co/FDHWm83iU0
2つ目についてはカプセル化が一番重要でそのほかはかなり劣ると思う。
— 🤓k.bigwheel🤓 (@k_bigwheel) 2018年9月26日
継承もポリモーフィズムもjavaのインターフェイス、rubyのmixinなど今では少し機能を制限した手法が主流だし。 https://t.co/FDHWm83iU0
オブジェクト指向との付き合いも長いし、これについて考えたり話すの結構好きなのでそろそろ一席ぶっておこうと思う。
オブジェクト指向との付き合い
15歳のときにC++を初めてそろそろ15年になる。少なくとも高専2年のときぐらいからオブジェクト指向について悩んでいたためそこそこうるさいと自認している。
僕のオブジェクト指向(記述)の書き方の理解のベース
僕はオブジェクト指向の記述方式をあくまで手続き型言語(ミュータブルな変数と関数)の延長線上だと捕えている。 これはCからC++へ流れてきた人間にとっては理解しやすい考え方だと思う。 構造体と関数がセットになったものがクラスとよく説明されたからだ。
なぜオブジェクト指向の理解方法についてはフレームしやすいのか
今回の一連の流れの発端となった?Qiitaの記事についても案の定反応は荒れている。 はてなブックマーク - オブジェクト指向が5000%理解できる記事 僕も正直この説明はどうかと思うが、オブジェクト指向について明確な定義がないため*1おまそう*2以上の反論がしづらい。辛いね。
そもそもオブジェクト指向は難しい
オブジェクト指向について理解するためには、そもそもプリミティブな型と関数を最低限理解する必要がある。 そこをちゃんと理解して、さらに構造体*3も理解した上でないとそもそも言語が提供するクラスという機能を理解することすら難しいのだ。
そのためプログラミング初学者が最初からクラスを使おうとするのはかなり無謀だと思う。
オブジェクト指向に明確な定義はない。たぶん・・・
XXであるのがオブジェクト指向である、というような一般に言われる定義はまだない。 たぶん各言語仕様で提供されるクラスに関係した機能と、それの一般的な利用法から事実上定義されているにすぎないと思う。
本題、僕のオブジェクト指向との付き合い方
前置きが長くなった。本題に入る。
僕はオブジェクト指向で書くとき、常に手続き型、つまりクラスのない変数と関数のみの構成から始める。 main関数内にそれらをベタ書きし始めるのだ。
そのうち、というよりすぐにコードで溢れてくる。 そうなってきたら、この各変数と関数を点として頭の中でマップのように配置する。 お互いの関係性が強ければ近い位置に、関係性が薄いかまったくなければ離す。 こうしてできた関数と変数の集合をそのままクラスにしている。
この話、ポリモーフィズムについてはほぼ全く考えていないが、実際ポリモーフィズムや継承はあんまり重要視していない。 それらはなくても大抵の場合問題ないし、大抵の場合使ったことにより問題が起こることのほうが多い。 言語の標準ライブラリやプリミティブ型が継承やポリモーフィズムを強制するのでない限り、少なくとも最初の方は自分のコードで継承・ポリモーフィズムは使わないほうが良いと思う。 でないとそれらの機能に振り回されて、本来自分のやりたいことから注意が逸れてしまいがち。
この考え方の背景
www.amazon.co.jp www.amazon.co.jp www.amazon.co.jp
これらを読んだにもかかわらず、18歳の時点で僕はオブジェクト指向をよくわかっていなかった。 結局のところ僕のオブジェクト指向への見解は習うより慣れろでコードを書き、時に読んで試行錯誤した中、うまく行ったパターンを繰り返して洗練させたものにすぎない。
ただ、上のような脳内マッピングイメージで理解し始めたのにはきっかけがある。 それは高専の哲学の授業で、言葉とは何か、その成り立ちについての講義のとき、 言葉、もっというと名詞とは点ではなく線で囲われた面であるという話があった。 海外には昔は津波という単語がなかったが、それが日本から輸入されたときそれまでbig waveでひとくくりにされていた言語範囲が線により二分され、片方はtsunamiに、もう片方はbig waveになった、というような説明だったと記憶している。
slackでの連絡は直接口頭でのやり取りが面倒くさいからじゃないよという話
slackでの連絡は短期的なメリットと長期的なメリットそれぞれ1つずつある。
短期的なメリットはこれが口頭でのやり取りではなく非同期式コミュニケーションであることで、 特にコンテキストスイッチのコストが非常に大きいプログラミング作業中のエンジニアにとって非常に便利である。
長期的なメリットは単純ながら記録が残ることで、年単位で務めると威力を発揮しやすい。 一方でDirectMessageによるやりとりは検索に素直に出てくれないので困る。 slack開発元には可視性がPublicのDMを実装してほしい。
swagger-playやswagger-akka-httpのようなフレームワーク連携swaggerドキュメント生成ライブラリは概ね役に立たない
swagger-playやswagger-akka-httpのような、フレームワークと連携してswagger ドキュメントを自動生成してくれるようなライブラリがある。 このようなライブラリは直接API仕様をyaml/jsonで書く必要がなくなり、swagger仕様も読み解く必要がないため一見便利に見えるが使ってみたところ以下の欠点があり現時点ではプロダクトで使うには足りなかった。
1. フレームワークとの統合に限界があり、それほど自動生成してくれない
ライブラリがフレームワークから自動生成してくれる情報はせいぜいAPIパスとメソッド(GET/POST/...)などで、ヘッダ情報やレスポンスのステータスコードのパターンなどは結局自分で書く必要がある。
2. アノテーション情報が非常にコード品質を落とす
上で述べた通りほとんどの情報は自動生成されないのでメソッド/クラスへのアノテーションとして各種情報を書く必要があるが、swaggerドキュメントの品質をあげようとするとこのアノテーションは1APIにつき簡単に10行以上にもなりコードの可読性を著しく下げる。 そのためswaggerドキュメントの品質を上げようとするとコードの品質が下がり、コードの品質を上げようとするとswaggerドキュメントの品質が下がるというジレンマが発生する。
3. ライブラリのコード品質が低い
上のような欠点により多くの人が早めに見限るので、ライブラリの品質が基本的に低い。 swagger-akka-httpとswagger-playを試したがどちらも有名ライブラリと連携している割にはstarが少なく、またコードの品質が低かった。 具体的には以下を指す。
- アノテーションで表現できないswagger仕様がある
- swagger仕様のバージョンアップに追従していない
- case classとswagger modelを紐付けるなど比較的簡単に思いつきそうな機能がない
結論
swaggerドキュメントとコードは切り離して管理したほうが良い。 その方がコード/ドキュメントそれぞれの品質が上げやすくなる。 コードとドキュメントの歩調を合わせる手間の問題についてだが、個人的にはAPIサーバのrootにアクセスしたときswagger UIでそのAPIサーバのswaggerドキュメントが動くAPI仕様として使えるようにすればかなり楽になると思う。 例えば https://api.petshop.io/api/v1/xxxx のようなAPI集合がある場合、https://api.petshop.io/ へアクセスしたときswagger UIで https://api.petshop.io/api/v1/xxxx 系のAPIの仕様すべてを表示する。 これには以下の利点がある(番号が若いほど重要)。
- API利用者がswaggerドキュメントの場所を探す必要がなくなる
- 動くAPI仕様書は使いやすく、その結果API使用者・記述者両方がよく利用するようになる。その結果仕様の間違いや足りない記述などが発見されやすくなり、ドキュメントを修正しやすくなる。最終的にAPI仕様書の質が上がるようになる
- APIの追加/機能変更とドキュメントの更新を同じパッケージ(jar)で賄うことができ、それによりswaggerドキュメント変更とコード変更の歩調が簡単に合わせられる
- Swagger UIと対象APIが同じホスト名ならCORSを考慮する必要がない
動くAPI仕様書と実際のAPIを同じドメインにまとめることの利点はWeb API Design(その4) - winplusの日記の「ひとつのサブドメインに、APIのリクエストをまとめる」でも語られている。