kbigwheelのプログラミング・ソフトウェア技術系ブログ

プログラミング・ソフトウェア技術系のことを書きます

新・自転車“道交法”Book 感想

https://www.amazon.co.jp/dp/B071ZMCK32/

 

都心での自転車走行が余りに危険なため正しい走り方を法律面から知りたくなって読んだ。

知りたかったことでわかったこともあったが、道交法ブックを歌っているにもかかわらずやはりこの本でもxxxの法律は現実的ではないから違法でもこうするべき、という記述が出てきて閉口した。

「集約の境界と整合性(略」に対して頂いたアイデアの分類と現状での僕の回答らしきもの


先日ドメイン駆動設計 #1 Advent Calendar 2018 - Qiitaの1日として次の記事を公開しました。

kbigwheel.hateblo.jp

当初2,3人からアイデア頂ければ御の字だと思っていたのですが、思っていたよりずっと多くの人からたくさんのアイデアを頂くことができました。本当にありがとうございました。 僕が2ヶ月ぐらい考えても全然思いつかなかったアイデア・意見がたくさんあって、やっぱり一人では発想に限界があるなと痛感しました。

このまま僕だけが頂いたアイデアを知っているのは勿体無いので、この記事では頂いたアイデアを解決策1~4へ分類しつつ更に掘り下げました。 なるべく正確に理解するよう努めたつもりですが、間違って解釈していたらすみません。 はてぶやtwitterで直接メンションが飛んでいないつぶやきなども拾っていますので、もし私的な記述なのでここに載せないでくれという方居られましたらここのコメント欄かtwitterのDMなどで連絡をください。できるだけすぐに消すよう努めます。

解決策1 集約をマージするタイプのアイデア

皆無でした。 やはり多くの人がこの方向の解決はまずいパターンだと認識されているようです。

解決策2 一時的な整合性の破綻を受け入れ結果整合性を使うタイプのアイデア

DDDとして正道であることもあり、最も多くのアイデアが出ました。

A. 組織へ無効状態で追加後、有効化するアイデア

A.i 連続的に追加・有効化を行うアイデア

A.ii 追加後、有効化を別スレッドやイベント駆動で行うアイデア

B. 組織に所属するユーザー一覧というドメインオブジェクトをアイデア

B.i ユーザー一覧を組織集約に持たせるアイデア

B.ii ユーザー一覧を独立した集約にするアイデア

C. イベント駆動アーキテクチャの長期プロセス(サーガ)的なアイデア

読んでいてIDDDでいうサーガ(長期プロセス)のイメージなのかなと思いました。解釈が間違っていたらすみません💧

D. オーバーがNGなら逆に現在ユーザー数を先に増やすアイデア

解決策3 アンチパターンではあるが集約間の整合性維持のためトランザクション制御を用いる

こちらは解決策1と比べて一定の言及はあるものの、やはり積極的な支持はほぼありませんでした。

解決策4 ユースケースの見直しによる再モデリングタイプのアイデア

2番目に多かったのがこのタイプです。 ユースケースの見直し、具体的にはユーザー上限ルールの稀な逸脱を許容する方向のアイデアが多かったです。

僕の現時点での回答らしきもの

まず解決策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 案と比べていくつかのメリット・デメリットがあります。

  • メリット
    1. ユーザーが必ず1つの組織に属すること*6ドメインモデルから自明にできる
  • デメリット
    1. 相互参照は注意して扱わないと簡単に整合性が取れなくなってしまう*7
      • 例: 有効な状態のユーザーをユーザーエンティティの組織IDだけ別の組織へ変えてしまうと、元の組織にその組織へ属さないにもかかわらず有効登録されたユーザーIDが残ってしまう

また特筆するべき困った点が④を実行前にこのアプリケーションやコンテナが死んだ場合で、B.i案とは違い先に作られたユーザーは実際に組織へ無効状態で追加されています。 そのため、他の単純に無効化されているユーザーと区別することが難しく、リカバーにより手間がかかると感じました。

まとめ

というわけで、一旦現時点では解決策2 B.i案の「ユーザー一覧を組織集約に持たせるアイデア」が良いと思いました。 むろんこれが正解だという確信はまったくありません。細かい条件の違いで如何用にも最適解は変わりますし、僕が説明されていただいたシチュエーションでも2 B.i案が本当に最良か、試してみないことにはわかりません。

頂いたアイデアのおかげで考えがまとまりました。 一人で考えるだけではここまで早く良い案には辿りつけなかったと思います。 本当にありがとうございました。あとは実際にやってみて出した案が本当に良いか確かめようと思います。

2018/12/12 追記

cactuaroid(@cactuaroid)さんから上のような指摘がありました。 それと、ファクトリによりIDがcreate時に発行される前提で書いたコードが以下です。 もっとモナドらしい自然な書き方ができると思うんですが、慣れていないので愚直な書き方で失礼します。

// アプリケーション層のどこか

def `ユーザーを有効状態で追加`(
    `組織ID`: Int,
    `ユーザー名`: String,
    `メールアドレス`: String,
    `ハッシュ化されたパスワード`: String): Try[`ユーザー`] = {
  val `組織インスタンス` = `組織リポジトリ`.find(`組織ID`).get
  if (`組織インスタンス`.`有効状態でユーザーを追加可能か`) {
    val `ユーザーインスタンス` = `ユーザーファクトリ`.create(`ユーザー名`, `メールアドレス`, `ハッシュ化されたパスワード`)
    `ユーザーリポジトリ`.save(`ユーザーインスタンス`) match {
      case Success(_) =>
        val `新組織インスタンス` = `組織インスタンス`.`有効状態でユーザーを追加`(`ユーザーインスタンス`.`ユーザーID`)
        `組織リポジトリ`.save(`新組織インスタンス`) match {
          case Success(_) =>
            Success(`ユーザーインスタンス`)
          case f =>
            `ユーザーリポジトリ`.delete(`ユーザーインスタンス`.`ユーザーID`) // ① 組織への追加に失敗した場合は作成したユーザーを削除しておく
            f
        }
      case f =>
        f
    }
  } else
    Failure(new `有効ユーザー数上限Exception`)
}

ユーザーはsaveできたが組織インスタンスが何らかのエラーでsaveできなかったときのために①を書いていますが、これを省くと更にコードが完結になります。 ただ、その際はそうやって発生する不整合状態を復帰させる(どの組織にも紐付かないユーザーを削除する)ようなデーモンないし仕組みが欲しくなるでしょう。

関連リンク

*1:最初読んだとき天才か・・・と思いましたがトリッキー過ぎて書いた自分以外にうまくこのロジックの意味を伝えるのが大変かなと思いました

*2:解決策2が駄目だったら4へフォールバックしたらいいかなと

*3:ここについてはRDBMSの正規化の視点から抜け出せていなかったなと反省しました

*4:イベント駆動的ソリューション・アイデア分野は本当に力不足を感じます、すみません。。。

*5:ユーザーすら作れなかった

*6:組織に属さないユーザーや2つ以上の組織に属するユーザーが存在しないこと

*7:DDD本でも相互参照は基本的に避けるべきという記述があったと思います、たしか

集約の境界と整合性の維持の仕方に悩んで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、上のコードは一切集約間の整合性の維持を気にかけていません。ですのでユーザー上限ルールは以下のように容易に破綻します。

  1. 有効ユーザー数上限が30, 現在の有効ユーザー数が29
  2. ユーザー追加メソッドが並行でほぼ同時に2度呼ばれる(それぞれをコールA, コールBとする)
  3. コールAで①が実行され、29が取得される
  4. コールBで①が実行され、29が取得される
  5. コールA/Bどちらでも②は29 < 30であるため③に進む
  6. コールAで③が実行される
  7. コールBで③が実行される
  8. 最終的に有効ユーザー数は31となり、有効ユーザー数上限を超えてしまう

DDD的には以下の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 追記

書きました!

kbigwheel.hateblo.jp

参考文献


  1. 詳細は後述

  2. ファクトリとリポジトリが一緒になっていてよくない、というのはあるかもしれません

  3. グローバルトランザクションなどで暗黙的なトランザクション共有もしていないということ

  4. これでも良いはずですが、今回は組織エンティティにaddユーザーメソッドがあったほうがより表現が豊かかと思い例ではそのようにしました

  5. 集約内のすべての値オブジェクト・エンティティは基本的に一度にロードされます

  6. これの何が問題かというと、記事とユーザーという2要素だけで考えられた処理に組織という概念の不純物が入ってしまう点です

  7. とはいえグローバルトランザクションよりは遥かに危険性は抑えられているのですが

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だけを見ればいいという点だ。

これなんよなあ。大抵のカウボーイ型のコーダーは本質的に自分しか見ることを考えないから、こう、変更必要な箇所が絶対的に特定かつ限定されるというオープンクローズドの原則の力を過小評価しがちなんよな。

あと読んでいて機能ベースのパッケージングを早急にしないとと思った。 あれ縦割り横割りって選択肢じゃなくて両方やるもんなのな。 機能単位でパッケージングしたのち、レイヤーの違いはコンポーネントで表現すると。

オブジェクト指向についてそろそろ一席ぶっておく

きっかけ

たまたま?最近よくオブジェクト指向の理解方法について、続けていくつかの言及を読んだ。

オブジェクト指向が5000%理解できる記事 - Qiita

オブジェクト指向との付き合いも長いし、これについて考えたり話すの結構好きなのでそろそろ一席ぶっておこうと思う。

オブジェクト指向との付き合い

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になった、というような説明だったと記憶している。

*1:本当にそうか調べてないけどね

*2:お前が思うんならそうなんだろう、お前の中ではな、という強引な主張に対する万能反撃

*3:ここではC言語のそれのようなメソッドを持たない2つ以上の型のペアのそれ

slackでの連絡は直接口頭でのやり取りが面倒くさいからじゃないよという話

slackでの連絡は短期的なメリットと長期的なメリットそれぞれ1つずつある。

短期的なメリットはこれが口頭でのやり取りではなく非同期式コミュニケーションであることで、 特にコンテキストスイッチのコストが非常に大きいプログラミング作業中のエンジニアにとって非常に便利である。

長期的なメリットは単純ながら記録が残ることで、年単位で務めると威力を発揮しやすい。 一方でDirectMessageによるやりとりは検索に素直に出てくれないので困る。 slack開発元には可視性がPublicのDMを実装してほしい。