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

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

スクラムマスター研修受けてきた

取り急ぎまとめ。

  • 昨日・今日の2日でスクラムマスター研修を受けてきた
  • 目的はスクラムを導入しているがイマイチうまく言っていない現在のチームの改善、人にスクラムを教わることでよりよいプロセスにできるのではないかという期待
  • 基本的に自分のキャリアのためであること、会社を説得することが面倒であること、そもそも会社が出してくれそうにないこと、また取得後数ヶ月で転職するとさすがに申し訳ないこと(今のところ具体的な転職予定はないが、申し訳無さで転職の選択肢が取りづらくなることを嫌って)私費で行った
    • 税込みで計216,000円也
  • 結論から言うと非常に良かった
    • 一番良かったのはたぶん30~50回行われた参加者とガビィ、原田氏とのQ&Aで、スクラムガイドや一般的なスクラム本で語られないところ、語られても深くは掘り下げれらないところを細かく、背景を交えつつ聞けたところ
  • 参加者のやる気も非常に高かった。AWSの公式講習を受けたことがあるが(こちらも1回2~3日、20万円ぐらいかかる)、ずっと参加者のやる気は低かった
    • 実際に体を動かしたりポスターを作るなどよりアクティブなアクションが多かったのがよかった理由の1つ
  • 参加者の背景も多様で面白かった。組み込み系企業のいわゆるテックリードのように、新しい手法・技術を指導する立場にある人、会社の資格取得推奨制度をきっかけに来ている人(AWSの資格コンプリーター!)、技術コンサルをされている社長さんで、コンサル先でスクラムを指導できるように来た人など
    • 女性の参加者は全体の1/6~1/5ぐらいに見えた(6人チームに平均1人いた感じ)。比較的ソフトウェアエンジニアリングの世界より女性比率が多いように感じたが、講師が女性であるため参加しやすかったのかも
    • 年齢層も幅広く20代から40代までいた
  • たくさん勘違いや心得違いも見つかった。ごく単純な誤解から、背景を理解せず字面だけを読んだ結果真意を理解していないケースまで
  • 基本的に引きこもり・出不精なので、1日目はすごく緊張した。同じチームでやった2日目になるとだいぶお互いの人柄の理解が進んで話がしやすくなった
  • 1日目も2日目も無茶苦茶疲れた。頭いっぱいでへとへとになって自宅に返った感じ。自分は参加者の中では比較的長くスクラム(あるいはスクラムのようなもの)をしていたほうで、キーワードだけは比較的知っている方だったのにそれほど負荷が高かったので、よりスクラムに親しんでいない人はもっと大変だったかも

以下取ったメモから順不同で書き出す

  • 絵を沢山書こう
  • リファクタリングはストーリー(タスク消化)の一部。タスクと分離しない。恒常的なリファクタリングの時間をタスク見積もりに含める。恒常的にリファクタリングをする
  • 大規模チームでも編成できる。車のファームウェアのチームなどは数百人単位
  • walking skeleton
  • リモートスクラムを最初からやるのは難しい。合宿あるいはonboardingで最初しばらく一緒に仕事をすると成功しやすい
  • output ではなく outcome(機能数より具体的な成果)
  • chaos reportによると80%の機能はほとんど使われていない
  • チームがこなれてくるとスクラムマスターはお互いのタスクを交換して苦手なタスクをやらせる
    • 一時的にベロシティが下がることを許容する。しかしその後よりチームは強くなる
  • 弁当が美味い
  • スクラムマスターはポストイットが基本、jiraなどのツールを最初から使うとツールに踊らされる
  • スクラムマスターはあくまでコーチ的。開発メンバーに指示をせず、解決策も直接提示しない
  • 不要だと判断された機能はきちんと削除する
  • スクラムは経験主義をバックグラウンドとしている
    • 経験主義については個人の経験に基づいての判断ではないことに注意。ここはもうちょっと読んで理解したほうがよいかも
  • CIは重要。完成の定義をストーリーごとにクリアするにはCIで自動化しないと現実的ではない
    • 一方でCIに関するタスクをそのままPBIにしないこと
  • アーキテクチャの改善やサーバ・環境の用意、ホワイトボード・付箋・部屋・PCの用意などはスクラムで規定されていない。それらはスプリント0として扱うような方式もあるが、原田さんはあまり推奨していない。あくまでスクラム開始前に用意しておくこと。
  • 未完了は再見積もりをしてバックログに戻す(つまり未完了になったタスクのベロシティは行ってしまうと失われることになる)
  • スクラムマスターはドミナントにならないよう、必要であれば会議を意図的に欠席したりincognitoしたりする
  • 特に初心者のスクラムマスターは開発メンバーと兼任するべきではない
    • 最初のX日(1週間だっけ?)はチームを観察する、よく観察する
  • やる気のないメンバー、スクラムに反対するメンバーなどに対する手法は基本的にスクラムは規定しない
    • それらはスクラムの外側で対処するべき問題だが、やる気のないメンバーはスプリントごとのデモで自身を取り戻させることができるかもしれない
    • プロダクトオーナーが協力的でない場合は、開発メンバーはビーチに繰り出す
      • 不要な機能を作ることはむしろ害悪。削除に追加でコストがかかる
      • プロダクトオーナーがPBを持ってくる来る時に備えてビーチに繰り出し英気を養うのが開発チームの最善
    • スクラムの反対するメンバー(デイリースクラムに参加しない、など)はチームから一時的に取り除くなどの対処を取る
  • 上にも関係するが、あくまでスクラムはプロダクトマネージメントのフレームワークであり、開発で発生する問題や意思決定すべてに関する回答リスト・規定ではない
  • プロジェクトマネージメントではなくプロダクトマネージメント
  • トヨタの片山さんいわく、イテレーション(繰り返し)ではなく改善のサイクル
  • スクラムマスターとしてはチームに干渉しすぎない。自分たちで問題を発見させ、答えを聞かれたら5 whyメソッドを使って自身に考えてもらう
  • 見積もりがあんまり大変なので、最初はS, M, L, XLの4つ程度に分ける
    • それぞれSP3, 5, 8, 13に割り当てる うまくいくチームがある程度パフォーマンスを出すまで3ヶ月(原田さんのいうある程度のパフォーマンスってどのレベルのイメージだろう?)
  • 2,3ヶ月ごとに2,3名入れ替わるようなチームは安定度が足りない
  • 完成の定義はすべてのPBIに適用される汎用的なもの。acceptance criteriaはPBIごとのもの
  • Goalはステークホルダーからわかることが重要
  • CXOがスクラムに不安を感じている場合は、とりあえずミニマムで試してみることが重要
    • 一方でスクラムが成果を出すまで一定の期間が必要なので、スプリントごとのデモでステークホルダーや開発メンバー自身に手応えを掴んでもらわないと続けにくそう。
  • 優秀なスクラムマスターは日本人的シーンで場が凍っても待つ。いくらでも待つ
    • たぶんそこで待たないとメンバーは責任感を持たず、自主的に動かないからだと思う
  • 報酬制度を個々人でやるのは危険

講義中のメモ

iot企業ハードウェアチームとの連携 絵をたくさん書く

先行しているプロダクト、リファクタリングが必要。 プロダクト要求を進められない。

手分けしやすい、スケールしやすい、 なんでわけるの? PM以内の大丈夫? 大規模チーム? スケールスクラム 早めに手に入る。 要求分析 今は発注側がしんどい スクラムのメインフィールドはイノベーション イノベーションは外注できない

walking skeleton

scrum of scrum

遠隔地は最初からは難しい、一度一緒に飯を食べて、しばらく仕事をすると、リモートにしても成功しやすい。

outputではなくoutcome chaos report

機能数より成果 最初からチームで機能横断的である必要はない、レビューを繰り返すとフルスタックチームになる。

旧来のチーム、前職のようなチームではどうする? スクラム部署を小さく作り始める。デンソーの例

スクラムマスターはいつかけしかける、自分の苦手なタスクをする。

挨拶、ボールゲームで推測の難しさ、 スクラムイベント、うまい弁当、アジャイル宣言

リスクに対するテクニック、ウォーキングスケルトン 細かく作る。 シンプルなケースを作る。 80%の人を救うケースを作る、それがウォーキングスケルトン。

そもそもの問題が見えてない、マネージャーがこない?部屋がコンフリクト?

プロダクトバックログ、あまり長過ぎると管理が大変。 せいぜい100個が上限 でもの重要性

リファインメントはスプリントの10% 2週間でクソみたいなものを出すのは大事。 工数感を共有できるから。

スクラムマスターはポストイットでマスターする。

経験主義は科学分野から

POの最大の価値はプロダクトバックログ

リファクタリングをプロダクトバックログに入れろ、普段のタスクでリファクタリングしろ

機能は削除しろ。

デザイナーのためのCIを回して、積極的に参加してもらう。 チームに参加してもらう。 CIがキモ

セキュリティアーキテクチャ設計書に連絡先を書いてこちらです、にしておく

SIヤーではお客様はプロダクトオーナーではない。プロダクトオーナーは社内で建てる。 お客様はステークホルダーに過ぎない。

featureフラグでdoneにする。 デプロイは開発が任意。

プロダクトバックログには誰でも突っ込める。 優先順位を入れ替えられるのはPOだけ。

二日目

未完了は再見積もりしてバックログに戻す

デイリースクラムはマスターに報告しがちなので、マスターは隠れるなど メールが来る、登録する、日本語と英語がある、50問か30問かわからない、4択、制限時間は1時間、二回受けて2回とも落ちた人は居ない。72%で合格

ドラッカーもdone is better than doing

特に初心者のスクラムマスターは開発メンバーとも兼任するべきでない。 プロダクトバックログはvalidate 開発チームはバックログ通りに作っていることを保証するべき イテレーション、繰り返しではなく改善のサイクル、トヨタの片山さん曰く

優秀なスクラムマスターは待つ。気まずくても待つ。

スクラムマスターとしてチームに干渉し過ぎない。責任をチームに持たせるため、自ら解決させる。

    でも、そうするとスクラムマスターはやることなくない?

プロダクトオーナーが休み、病気は代理を立てる。

ビーチに行っていい?

どこまでやるんだっけ、はプロダクトオーナーと相談

3,5,8,13をSMLXLに割り当てる

high priorityかつXLは潰す。

環境構築のような機能に直結しないタスクはどうなる?

スクラムはプロジェクトマネジメントの手法ではなくプロダクトマネジメントの手法

上手く立ち上がる3/2のチームがある程度パフォーマンスが出るまで3ヶ月ぐらい。 二、三ヶ月ごとに2,3名入れ替えるのは安定度が足りない。

product ownerはend targetを決める。

今、definition of doneは完了の定義ではなく完成の定義という訳語にして曖昧さをなくした。

完成の定義は、汎用的なもの。 acceptance criteria はPBIごとのもの。

サーバの用意やインスタンスの用意はスプリント外で事前に用意する。

Goalはstake holderからわかることが重要

リファインメントはミーティングを短くする

CXOの不安を解消するため、試してみる。

試作品でもwalking skeletonを作る。 ソフトウェアでハードウェアがきちんと動いていることをテストするようにすれば、すごく高速に設計できる。 あとシミュレータをガチガチに作る。 スプリントを回すため、

何がやりたいと聞く。 報酬制度は個々人でやるのは危険

5 whyはガビーもやるテクニック

これはと思ったら、病院へ

結果整合性を使うということはDBで正規化を捨てるということだと君は理解しているか

以下、数ヶ月もんもんと考えていてなかなかアウトプットすることができなかったので自分語り交えながらとりあえず出力することにした。 読みづらいのは勘弁願いたい。

なるほど、これがDDDとしての模範解答なら僕には数ヶ月考えてもたどり着けなかったのに納得がいく

数ヶ月ほど前、アドベントカレンダーに参加したときに以下の記事を書いた。

集約の境界と整合性の維持の仕方に悩んで2ヶ月ぐらい結論を出せていない話 - kbigwheelのプログラミング・ソフトウェア技術系ブログ

そうしたところとても多くの人に反応を頂いて、自分としても一応の結論を出すことができた。

「集約の境界と整合性(略」に対して頂いたアイデアの分類と現状での僕の回答らしきもの - kbigwheelのプログラミング・ソフトウェア技術系ブログ

このとき多くの人の意見を読ませてもらい自分の結論に至って考えたのは、この結論は自分一人で考えていては絶対にたどり着けなかったということだ。

僕がどの解法を自分の結論としたかは上の記事を見てもらうとして、なぜ自分一人ではたどり着けなかったというとこの結論が正規化していないテーブル構造だからだ。

テーブル構造の正規化

テーブル構造の正規化、およびそのパターンについては他の記事・サイトに説明を譲るとして*1、 使わないほうが良い説もそこそこ聞く外部キーと比べてDBの正規化がアンチパターンだと語る話はほとんど聞いたことがない。 テーブルの正規化はRuby系のDRYにも似た概念であり、無駄なデータの重複を作らないことは更新漏れを予防したりデータ保存効率を上げたりする点でも勧められるよい設計方針だ。 RDBMSでの正規化はOOP系のプログラミング言語とのインターフェイスインピーダンスミスマッチを起こす要因として 挙げられることはあるものの、我々プログラマーRubyActiveRecordやORマッパーの機能、時には自分たちでDAO/DIOを書くことで そのミスマッチをプログラム側の努力で解消してきた。 それはそれだけDB領域の正規化を重要視してきたからである。

通常1対多のテーブル関係がある場合、多の方のカラムに1の方のIDを持たせるだけでよい。 データ保存効率がよく、また間違って多対多の関係になってしまうことも防げる。良いことづくめである。 たびたびで恐縮だが、上記リンクの回答編、最終的な結論ではDBを非正規化する解法を選択している。 なるほど、その選択肢は僕の頭には一切なかった。

なぜRDBMSを使用しているにもかかわらず非正規化しているのか

端的に言えばトランザクション境界をスパッと切るためである。 上の例で言うと組織集約とユーザー集約は別の集約(=リポジトリ)でありトランザクション境界で別れている。 DB正規化の観点からすればどのユーザーがどの組織に属しているかはユーザー側のテーブルで管理するべきだが、 オブジェクト指向、というかDDDの集約(リポジトリ)の見地からすればそれは組織集約の方で管理するべきなのだ。 1対多の関係が多対多になるリスクを負うことになるが、それでもである。

結果整合性を使うということはDBで正規化を捨てるということだと君は理解しているか

私は今までごく少数の例外(forgeign keyや論理削除・物理削除)などの問題を除けばRDBMSの基本原則を信奉していたし、 インピーダンスミスマッチを認めつつもそれはプログラム側で努力して解消するもの、または工夫により解消できるものと信じてきた。

特に正規化については重複を避けるプログラミングの原則にもマッチしているし、RDBMSを使う根本理由であるとすら思っていた。

その考えが今揺らいでいる。

慣習的に従いたいのは正規化の方針だ。しかしDDDとIDDDを読み、多くの先達の意見を聞いた今理性は正規化を捨てるときだと言っている。 実際そういった思いでDDD・IDDDのリポジトリトランザクションの章を読むと正規化を捨てるという案は全編で肯定されている。 DDDに従うならば、必要に応じて正規化を捨てるという手段は当たり前のもののようである。

しかし、思うのだ。 我々は主にウェブプログラミングへマッチさせるために外部キーを捨て、論理削除の概念を生み出した。 この上正規化を捨てるとなるともはや使っているRDBMSの機能のほうが少なくなってくる。 集約でトランザクション境界を切るとなるとJOINすることも稀になるからだ。 もはやこの用途ではRDBMSよりスキーマ強制されるNoSQLを使うほうが用途にあっている気さえする (CQRSでJOINをフル活用する必要がある場合はRDBMSが生きると言えるが、あれは邪道な気がしてどうにもまともに捉える気になれない)。

DDD本にも何度か出てくるが過去オブジェクト指向DBというものが何度も考案されては実用化できずに消えていった。 しかし、このDDDに沿ったリポジトリ実装だとどうにもこのオブジェクト指向DBというものが最適な気がしてならない。

我々は、ドメイン領域と外界とのインピーダンスミスマッチを最小にするため、ソフトウェアエンジニアリング史何度目かのオブジェクト指向データベースへの挑戦をするべきなのではないだろうか。

蛇足

今回の経験で、結果整合性を使うために集約間でトランザクション境界を切る場合は、それぞれのテーブルを別のスキーマ(MySQLなんかだと複数スキーマ間でJOINなどできてしまうが一旦それはなしと考えて)に置くとしてテーブルを設計すればいいことに気がついた。

*1:実際のところ他人に説明できるほど理解していないのである

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


先日ドメイン駆動設計 #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時に発行される前提で書いたコードが以下です。 もっとモナドらしい自然な書き方ができると思うんですが、慣れていないので愚直な書き方で失礼します。

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

2019/01/20 追記

https://twitter.com/j5ik2o:かとじゅんさんから上のようなアイデアをいただきました。 (僕が勘違いをしていなければ)そのアイデアを元に書いた疑似コードが以下です。

cactuaroidさんから頂いたアイデアのコードと比べると、集約の保存の順番が ユーザー集約 → 組織集約 から 組織集約 → ユーザー集約 に変わっています。 楽観的ロック的に考えると組織集約の更新は同時に同じ集約へ更新が走ると衝突する可能性が一定あるのに対してユーザー集約の新規保存は衝突する可能性が(ほぼ)ありません。 そのため、仮で作ったユーザーを削除する必要がないという点で優れていると言えます。

一方で、組織集約の更新直後にアプリケーションがkillされたりDBクラスタがダウンしたりする可能性を考慮すると少し厄介です。 組織集約のユーザーID一覧に実際には存在しないユーザーのユーザーID(以後ゴーストユーザーID)が残ってしまうため、集約を参照する側はユーザーID一覧内のユーザーIDが実在しない可能性をコード中で考慮する必要が出てきます。 また、そのようなゴーストユーザーIDを定期的にクリーンアップする必要が出てきますがこれは2018/12/12の案での組織に紐付かないユーザーを削除するクリーンアップ処理に対応します。

※ すみません、ここら辺の結果整合性とシステムダウンの関係と復帰のさせ方については勉強不足かつ考え不足で自分の言葉にできていないので、後日また記事を書いて言葉にしようと思います。

関連リンク

*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つ以上の型のペアのそれ