イギリス経験論とイミュータブルデータモデル


チームの Advent Calendar 用 第二弾です。(第一弾

****************

  • 人間は知覚の束である
     by デイビッド・ヒューム
  • エンティティはイベントの束である
     by N氏(職場のエライ人)1

****************

2人の偉人の言葉に相通じるところを感じ,考察してみることにしました。

イギリス経験論

17世紀から18世紀,哲学の一分野である認識論において,イギリスを中心とする経験主義的な「イギリス経験論」とヨーロッパ大陸(フランス,ドイツ,オランダなど)を中心とする合理主義(理性主義)的な「大陸合理論」による論争が続けられていました。2

source
@startuml "近世哲学"

package "近世哲学" as modern_philosophy {

  package "大陸合理論" as rationalism {
  }

  package "イギリス経験論" as empiricism {
  }

  package "ドイツ観念論" as deutscher_idealismus {
  }
}

package rationalism {

  class "デカルト" as rené_descartes {
    + René Descartes
    + 1596 - 1650
    + フランス
    + 近世哲学の祖
    + 方法的懐疑()
    + 心身二元論()
    + 演繹法()
    + 直交座標()
  }

  note right of rené_descartes
    我思う
    ゆえに我あり
  end note

  class "スピノザ" as baruch_de_spinoza {
    + Baruch de Spinoza
    + 1632 - 1677
    + オランダ
    + 汎神論()
    + 中立一元論()
    + レンズ磨き()
  }

  note right of baruch_de_spinoza
    永遠の相の下
  end note

  class "ライプニッツ" as gottfried_wilhelm_leibniz {
    + Gottfried Wilhelm Leibniz
    + 1646 - 1716
    + ドイツ(神聖ローマ)
    + 法典改革()
    + モナド論()
    + 微積分法()
    + 形式言語()
  }

  note right of gottfried_wilhelm_leibniz
    私は
    2+2=4
    になることを
    知るのに
    感覚の助けなど
    借りはしない
  end note

  rené_descartes    --[hidden] baruch_de_spinoza
  baruch_de_spinoza --[hidden] gottfried_wilhelm_leibniz
}

package empiricism {

  class "ベーコン" as francis_bacon {
    + Francis Bacon
    + 1561 - 1626
    + イングランド
    + イドラ()
    + 帰納法()
  }

  note right of francis_bacon
    知は力なり
  end note

  class "ロック" as john_locke {
    + John Locke
    + 1632 - 1704
    + イングランド
    + 自由主義の父
    + タブラ・ラサ()
    + 自然権()
    + 抵抗権()
  }

  note right of john_locke
    人は白紙の状態で生まれ
    経験したことがこの紙に
    書き込まれていく
  end note

  class "ヒューム" as david_hume {
    + David Hume
    + 1711 - 1776
    + スコットランド
    + 懐疑論()
    + 自然の斉一性原理()
    + ヒュームの法則()
  }

  note right of david_hume
    人間は知覚の束である
  end note

  francis_bacon   --[hidden] john_locke
  john_locke      --[hidden] david_hume
}

package deutscher_idealismus {

  class "カント" as immanuel_kant {
    + Immanuel Kant
    + 1724 - 1804
    + ドイツ(プロイセン)
    + コペルニクス的転回()
    + 道徳法則()
    + 目的の王国()
    + 永遠平和のために()
  }

  note right of immanuel_kant
    私たちは何を知りうるか
    私たちは何を為すべきか
    私たちは何を望むのか
  end note

  gottfried_wilhelm_leibniz ..> immanuel_kant
  david_hume                ..> immanuel_kant : "  独断のまどろみ"
}

@enduml

経験論では,知識や観念はすべて五感を通じて得た経験によるものであり,生まれ持った知識や観念は存在しないとします。ベーコンをはじめとするイギリス経験論者は主に帰納法によって正しい知識を身に付けられると考えました。

一方,合理論では,人は見間違いをしたり,実験結果を誤ったりするため,五感による経験はあてにならないとします。デカルトは,人間は生まれながらに(神や善悪といった)生得観念を持ち合わせていると認め,この生得観念をたよりに演繹法によって正しい知識を身に付けるべきだと考えました。

それに対して,ロックは生得観念に疑問を持ち,人は生まれつき観念は持っておらず,白紙(タブラ・ラサ)だと考えました。そして,経験したことがこの紙に書き込まれることで,知識や観念になるのだと主張しました。3

知覚の束

ロックは人間の五感によって捉えられる性質(色,味,匂い,手触りなど)は人間が存在しないと成立しないため,実在しないと考えました。さらにバークリーは「存在するとは知覚されていることである」として,物の存在そのものを否定しました。しかし,2人とも物を知覚している “私” の存在は疑いませんでした。ヒュームはこの “私” すら疑いました。

人間には今この瞬間,五感による何かしらの感覚(知覚)があります。ヒュームは,“私” とは,これらの感覚(知覚)が集まったものに過ぎないと考え,「人間とは知覚の束である」と表現しました。

これは「自己同一性」の問題にも関連してきます。過去の知覚はその後の自己に影響を与えますが,まさに今ここの “私” とは切り離されています。過去の自分も未来の自分も現在の自分とは違うのです。

過去に起こった `知覚` が積み上げられて,その時点のその `人` をつくっている.
かつ,ある時点でのその `人` は,その時点でのその `人` としか言いようがなく,
永久不変の `自己` が存在する訳ではないのです.

イミュータブルデータモデル

イミュータブルデータモデルとは,データベースのモデル設計において,UPDATE を使わずにデータを扱うアプローチを指すようです。CRUD の中で複雑性を増すのは UPDATE なので,(なるべく)排除しようということです。大雑把に言えば「テーブルから更新日付を取り除こう」ということです。

マルチスレッドプログラミングにおいてもデータがイミュータブル(更新不可)であれば,排他制御の必要がなくなり,よりスレッドセーフになりますね。

増田さんの『現場で役立つシステム設計の原則』でも以下のように述べられています。

 たとえば、口座に入金があったら入金テーブルにコトを記録する。そして、残高テーブルのその口座の残高も増やす。口座から出金があったら、出金テーブルにコトを記録する。そして残高テーブルのその口座の残高を減らす。
 データベースの本質は事実の記録です。まず、コトの記録を徹底することが基本です。状態テーブルは補助的な役割であり、コトの記録から派生させる二次的な情報です。
 残高テーブルはUPDATE文ではなく、DELETE文/INSERT文の対で実行します。UPDATE文はデータの不整合が混入しやすい動作です。それは、コトの記録のところで述べた「記録の同時性」に違反するからです。

入金・出金は「事実」であり,残高はそこから導出された「結果(=二次的な情報)」ということですね。
 
ところで,イミュータブルデータベースというと Datomic が想起されます。(前述のスライドでも紹介されていました)

当時,これを知ったときはデータの変更ができなくて何が嬉しいのか全然意味が分かりませんでした。あれから10年近くたって,世の中では関数型言語パラダイムも広まりつつあり,何となく言いたかったことも分かるような(分からないような)感じですね。。ちなみに Datomic は Clojure (JVM上 で動く Lisp) と同じ作者です。

Datomic も裏では DynamoDB を使ってるようですが,最近だと Amazon QLDB というのもあるみたいですね。昨年 (2019年) の AWS のイベントでも結構宣伝してました。(Amazon Managed Blockchain の対比として)

イベントの束

エンティティの現在の状態だけを保持・更新していく代わりに,一連のイベントからエンティティのあるべき状態を再現するアプローチをイベントソーシングといいます。CQRS の文脈でよく登場します。追加専用(イミュータブル)のストアに一連のイベントを保存していくため,UPDATE を排除することによるメリットなどがあります。

エンティティを「イベントの束」として捉える考え方は「知覚の束」の概念に似たところがあります。先ほどの言明を言い換えるなら・・・

過去に起こった `イベント` が積み上げられて,その時点のその `エンティティ` をつくっている.
かつ,ある時点でのその `エンティティ` は,その時点のその `エンティティ` としか言いようがなく,
永久不変の `リソース` が存在する訳ではないのです.

 
このアプローチによるメリットの1つは「システム日付に依存しないシステム」を構築することができることです。エンティティはイベントの適用日からいつでも過去・現在・未来の状態を再現することができます。このイベントの適用日をタイムスタンプではなく外部から設定することで,システム日付(現在日付)に依存せずにエンティティのタイムラインを構成することができます。(副作用を分離するために clock を DI するイメージです)

コンポーネント間の結合テストにおいて,システムがシステム日付に依存している場合,日またぎや月またぎなどの時系列イベントによる処理の評価が難しくなります。とくに結合テスト環境を共有している場合は尚更です。システム日付への依存箇所を追い出し,局所化することで,この問題が解決できることを期待しています。※

※ 最近,検討をはじめた段階ですので,実際にうまく解決できるのかはまだ分かりません。。

サンプルケース

例として,店舗の状態を管理するコンポーネントを考えてみます。

  • コマンド系 API は イベントを INSERT します。
    • 開店日休業日 などの「適用日」は API のパラメーターとして受け取ります。
    • イベント日時 はシステム日付になります。
  • クエリ系 API は イベント を SELECT し,エンティティの状態を返却します。
    • 基準日 を API のパラメーターとして受け取り,イベントの「適用日」からその時点のエンティティの状態を構築します。

source
@startuml "店舗状態管理"

component "店舗状態管理" as com_store_state_management {

  database "店舗状態リポジトリ" as db_store_state {

    entity "開店イベント" as data_store_opened_event {
      + 店舗ID
      + 開店日
      + イベント日時
    }

    entity "休業イベント" as data_store_temporarily_close_event {
      + 店舗ID
      + 休業ID
      + 休業日
      + イベント日時
    }

    entity "再開イベント" as data_store_reopen_event {
      + 店舗ID
      + 休業ID
      + 再開日
      + イベント日時
    }

    entity "閉店ドイベント" as data_closed_event {
      + 店舗ID
      + 閉店日
      + イベント日時
    }

    data_store_opened_event ||---o{ data_store_temporarily_close_event
    data_store_temporarily_close_event ||--o| data_store_reopen_event
    data_store_opened_event ||-----o| data_closed_event
  }

  package "店舗状態受付" as pkg_store_state_reception {

    class "店舗ID発番API" as api_issue_store_id {
      + POST(): 店舗ID
    }

    class "開店API" as api_store_open {
      + PUT(店舗ID, 開店日)
    }

    class "休業ID発番API" as api_issue_temporarily_close_id {
      + POST(): 休業ID
    }

    class "休業API" as api_store_temporarily_close {
      + PUT(店舗ID, 休業ID, 休業日)
    }

    class "再開API" as api_store_reopen {
      + PUT(店舗ID, 休業ID, 再開日)
    }

    class "閉店API" as api_store_close {
      + PUT(店舗ID, 閉店日)
    }

    api_issue_store_id --[hidden] api_store_open
    api_store_open --[hidden] api_issue_temporarily_close_id
    api_issue_temporarily_close_id --[hidden] api_store_temporarily_close
    api_store_temporarily_close --[hidden] api_store_reopen
    api_store_reopen --[hidden] api_store_close

    api_store_open --r> data_store_opened_event
    api_store_temporarily_close --r> data_store_temporarily_close_event
    api_store_reopen --r> data_store_reopen_event
    api_store_close --r> data_closed_event
  }

  package "店舗状態参照" as pkg_store_state_reference {

    class "店舗状態参照API" as api_store_state_reference {
      + GET(店舗ID, 基準日): 店舗状態
    }

    api_store_state_reference --> db_store_state
  }
}

@enduml

状態遷移

店舗の状態遷移は,基本的に「開店」して「閉店」する遷移ですが,途中で一時的に「休業」することができます。そこから「再開」した場合は開店中に戻りますが,そのまま「閉店」することもできます。

source
stateDiagram-v2
  [*] --> 開店中
  開店中 --> 閉店済 : 閉店
  開店中 --> 休業中 : 休業
  休業中 --> 開店中 : 再開
  休業中 --> 閉店済 : 閉店
  閉店済 --> [*]

タイムライン

(IDの発番は省略)

  1. 開店日 12/01 で開店します。
  2. 基準日 12/10 で参照すると 開店中 が返却されます。
  3. 休業日 12/05 で休業します。
  4. 基準日 12/10 で参照すると 休業中 が返却されます。
  5. 再開日 12/10 で再開します。
  6. 基準日 12/10 で参照すると 開店中 が返却されます。
  7. 閉店日 12/15 で閉店します。
  8. 基準日 12/20 で参照すると 閉店済 が返却されます。
  9. 休業日 12/20 で休業するとエラーになります。
    これは その時点では 閉店済 だからです。

最終的なイベントの束が表現しているタイムラインです。

source
gantt
  title タイムライン
  dateFormat  MM-DD
  axisFormat  %d
  todayMarker off
  section 店舗状態
  開店中  : s1, 12-01, 4d
  休業中  : s2, after s1, 5d
  開店中  : s3, after s2, 5d
  閉店済  : s4, after s3, 10d

ポイントは 各 API を呼び出す日はいつでもいい ということです。これによって参照する側が注入した日付での評価ができるので,複数日にまたがる状態遷移を伴う結合テストが1日で行えます。また,最後の「未来のある時点における状態に対するバリデーションチェック」というのは,在り得るケースではないでしょうか。

まとめ

あらためて見てみると「レコードが不変であるということ」と「ある時点におけるエンティティの状態が不変であること」は違うということが分かります。とあるイベントの束が表現しているエンティティの状態のタイムラインはイベントの束が同じである限りは変わりませんが,イベントが追加されるとタイムラインは変化する可能性があります。

時間軸は『2本』あったッ! 4

  1. ドメインモデルの観点で見たエンティティの状態の時間軸
  2. システムの観点で見たイベントが発生した(受け取った)時間軸

前者は任意の時点のエンティティの状態を評価したいという要求ですし,後者は運用や監査のためのトレーサビリティという観点から必要そうです。

こちらのスライドでは「トランザクション時間データモデル」「有効時間データモデル」と紹介されています。

 
結論:
 エンティティはイベントの束である

参考文献


  1. DDD マスターであり,腐敗防止層の番人であり,凄腕の perl 使いです。 

  2. 本当はこの図を plantuml で描いてみたくてこの記事を書き始めました。 

  3. ロックは政治学者でもあり,「自然権」や「抵抗権」などの概念は,その後のフランス革命やアメリカ独立にも大きな影響を与えました。 

  4. 「船は『2隻』あったッ!」