😈

良いコード/悪いコードで学ぶ設計入門の感想と注意点

2022/08/04に公開約24,500字

「良いコード/悪いコードで学ぶ設計入門」という本がとても売れているようです。私の所属している開発チームでも、何人か購入した人がいたので、私も購入して一通り読んでみました。
結果として、いくつかの考えが整理され、私としてはこの本によって考えが深まり、本を読んで考えた事自体は有意義であったと思いました。ただし一方で、あまり知識がない状態で(自分の中での判断軸が無い状態で)この本を読むと、色々と誤解が生まれるのではないか?という事を感じました。
一つの技術書がこれだけ売れるという事はそんなに多くはない事だと思うので、つまり、 その内容が改善されるとその効果は相対的に大きい という事になります。そこで、私が本を読んでいて思ったことや、この本の内容で正しいこと、現在も賛否両論とされること、事実として認識が間違っているであろうこと、この本で触れられていないが設計において大事なこと、などについてまとめておきたいと思います。
私の主観による部分も少なからずあるかと思うので、これがすべてという事ではなく、ぜひ色々な方と意見交換ができればと思います。売れる本を書くという事は、私にはあまり真似ができそうにない素晴らしいことなので、私としては多くの人が読んでいるこの本の学びの効果を向上させる事で貢献ができれば、という思いです。

感想・注意点のまとめ

この本の個々の記述に対する具体的な感想については後述するとして、まず全体の感想・注意点をまとめておきます。

この本の要点で、特に抽象的には正しい・妥当と思えること

  • 不完全なオブジェクトがなるべくできないようにしよう・ステートレスにしよう
    • 中途半端/不完全なオブジェクトはそもそも作らせないようにしよう
    • 途中で中途半端な状態に変更されないようにしよう(immutable)
  • なるべくデータとロジックを近くに書こう
  • 名前をちゃんとつけよう

この本の評価、読む上で気をつけるべきこと

  • サンプルコードの品質は低く、仮に私が作業指示者であったとすれば、このサンプルコードだと大域的なリファクタや設計を任せる事はできないと判断する。(目指す方向性として上記の要点が正しいものであっても。)
  • この本はValue Objectについて明確な誤解がある。(Domain Primitiveと混同している。)
  • DDDのすべての技法が可読性の為にあるわけではなく、一部の技法は可読性を犠牲にして安全性を高める。特に、Domain Primitiveの極端な実践はしばしば可読性を損なう。
  • サンプルコードの品質の低さのある程度の部分は、グランドデザインや一貫したポリシーの無さ、あるいはValue ObjectやDomain Modelについて本書のようなアプローチとは別に考えるべき事の欠如から生じているように思われる。これについての記述・抽象論はこの本には出てこない。
  • 根本的な設計技法としてはValue ObjectやDomain Modelに関して考えるべきことが他にあるが、この本の主旨である設計技術に価値が無いという事ではなく、それはそれで意味がある。
  • レビュアーによるレビュー・指摘が不足している。

注意点が多くありますが、注意点を押さえれば大方針としては間違っていない記述もあるので、この本の知見を有効に活用できると思います。

具体的な感想

以下は具体的な感想です。あまりまとまっていないものもあります。
本書を頭から最後まで読み通したときに書いたメモ(ツイート)をベースにしており、基本的には本書の順序に沿っていますがたまに前後する場合があります。
なるべく具体的な記述をするようにしましたが、それによってボリュームが増えてしまいました。どのコメントも開発において重要な観点であると思ったので、省略せずに記載しています。

サンプルコードのインデントについて

私がiPhoneのkindleで読んでいたからかもしれませんが、全体的にサンプルコードのインデントがぐちゃぐちゃになっており違和感がありました。

独自で定義された単語について(全体的に)

いろんな独自で定義された単語、例えば「生焼けオブジェクト」といった単語が出てきますが、一般的な専門用語とオリジナルの語の区別、あるいは専門用語であっても一般と異なる定義をする場合の語の区別がもう少し明確でもいいかなと思いました。

データクラスについて

ストラクチャー(構造体)にかなり否定的な印象を受けましたが、必ずしもオブジェクトに生えるべきメソッドが全てではなく、ストラクチャー的なデータクラスにも有効な場面はあるかなと思いました。例えば、一般に異なるクラスAとBについて、AとBを引数とする関数/メソッドがAに生えるべきか、Bに生えるべきか、別のところにあるべきか、という事は設計上本質的で判断が難しく、そうした考慮の結果としてクラス自身があまりメソッドを持たず、単に構造体としての役割のみを持つという場合も考えられます。
もちろん、データとロジックを集められるシチュエーションでは集めた方が見通しが良い場合が多いという事を否定しているわけではありませんが、そこまで強い言葉で否定されるものではないのかなと感じました。実際、Pythonにおいては、この数年でデータクラスやnamedtuple、TypedDictのようなものが整理されています。これは言語開発者たちが実際に有用なものであると判断して取り入れたものです。

再代入について

2.2、再代入についての記述がありますが、再代入をした方がバグらないコードもしばしば存在します。
例えば、同じような処理を繰り返す場合で、かつ関数に分けにくい/分けた方が読みにくくなる場合は、一つの変数で結果を受け続けた方が登場する変数が増えずに頭のメモリを無駄遣いしない事があります。
※ただし、そもそもそのような処理を繰り返すコードは、設計を見直すことでより小さい関数に分割できるのでは?という指摘もあり得ると思います(シャドーや再代入してよい変数がある部分は、本質的に別スコープで行ける可能性があるということです。)

サンプルのメソッド名sumUpPlayerAttackPowerについて

sumUpPlayerAttackPowerという名前のメソッドが良いコードとして定義されています。しかし、この名前は「プレイヤーの攻撃力を合計する」みたいな名前になっており、その真の意味/目的は不明です。このような、目的不明で挙動に注目してつけた名前は、本質的には技術駆動命名と同じもので、不適切な名前であるように思い、また設計としても疑問符がつくものであるように感じます。
例えば、攻撃力を出すならsumUpではなくて計算calculateなどが適切なように感じますが、一方でダメージを出すには相手の守備力などが必要であって概念として閉じていません。また、RPGにおいて攻撃力などの計算はゲームバランスを考える上で重要ですが、その調整の際に腕力と武器の力と敵の守備力について単に腕力と武器の力の合計を単純に利用できる計算式でなくなったら、このメソッドの意味自体がなくなってしまいます。
ゲームデザイン的に、このメソッドで計算された値がどのように使われるものなのか・その計算方法は変更余地のないものなのかといった、ゲームに関する本質的な意味・ゲームモデル(ゲームのドメインモデル)がわからないと、個人的には気持ち悪く思いました。このコードが出てくる2.3は全体的に違和感があります。

"Value Object" / Domain Primitiveを徹底することについて

2.4などでHitPointクラスが登場しますが、個人的にはこれは行き過ぎたDomain Primitiveの適用ではないかと思いました。演算子のオーバーロードが存在する言語ではまた別かもしれませんが、単純に+や-で数値の計算を表現できない事によって、私の場合は計算内容を理解するのに時間がかかり、またイライラするかなと思います。
例えば「マイナスの値が入らないようにする」という事を目的としてimmutableなクラスを定義しても、この定義方法(コンストラクタで誤った値を入れたときにException)であれば、実際に発生するのは型エラーではなくて実行時例外であるので、その点においては型チェックの恩恵を得られる訳ではありません(コンパイルができた時点で絶対にマイナスの値が入らないという事ではないため)。
そうすると、例えばhitPointをキャラクターのメンバ変数として持たせて、セットする時に負の値が入ったら例外を出すようなメソッドを経由する(または、どうしてもimmutableにしたいならば、キャラクターのコンストラクタでチェックする)というやり方でも同等のことができ、かつ計算は単純に+や-で表現できます。
HitPointのクラスを作ると、そのクラスの定義を追いかけようとするとどうしても別のファイルを閲覧したりする必要が出てきてしまい、可読性としては下がってしまいます。
常に絶対的にこの方法が間違っているという事ではないですが、この設計を採用するという事は可読性を下げてある種の安全性を上げようとしているという事であって、そこにトレードオフがあるという事は理解しておいたほうがよいかなと思いました。

3.1の「クラス単体で正常に動作するよう設計する」ことについて

3.1の最初の記述などは抽象論としては正しいと思います。ただ、クラスメソッド全否定みたいな否定が先立つ書き方になっているため、3.1.1は賛同しかねますが。(言いたいことはわからないでもないですが。)
3.2.1(コンストラクタで確実に正常値を設定する)については、できる限り守るべきという意味で正しいと思います。3.2.2(計算ロジックをデータ保持側に寄せる、の例として書いてあるMoneyのaddメソッド等)は、上述の通り私は可読性が損なわれるとする立場なので、一長一短と思います。

一般論として、moneyを増やす操作について、元のmoneyが再利用不要なときに

money = money.add(100);

money.destructiveAdd(100);

otherMoney = money.add(100);

のどれが読みやすいかは、人や場面によって難しいと思います。
最後の例のotherMoneyは頭のメモリ負担があり、またmoneyとotherMoneyを書き間違えるリスクが増えます。(moneyを使わない場合は、otherMoneyを出す必要がないため)
読みやすいかどうかとはまた観点が異なりますが、私が過去に実際に書いたバグの中には、

money.notDestructiveAdd(100);

のようなもの、つまり副作用で値を変更しているつもりなのに実は値が変更されていなかった、みたいなパターンのものもありました。副作用が無いように書けばバグが無いかと言うと、そういう事ではなくて、そのメソッドが副作用を持つか持たないかを誤解する事で単純にバグは発生します。

余談ですが、ここでは金額の乗算についての記述がありますが、金額同士だと一般的な用途ではあまりないと思う一方、掛ける相手がintであれば定数倍なので需要があり、そのような意味での乗算は"現実の営み"であって意味があるもので、説明にもう少し改善余地があるかなと思いました。

Value Objectの定義について、およびDomain Primitiveについての補足

既に色々なところで指摘があるかと思いますが、Value Objectの定義は正しくないです。

値オブジェクト(Value Object)とは、値をクラス(型)として表現する設計パターンです。

というものではなく、同一性がそれを構成する値によってのみもたらされるようなもののことを意味します。いわゆるメモリの位置と説明されるオブジェクトとして等価かどうかとか、各オブジェクトに割り当てられたその他のidが等しいかどうかとか、そういう事によって同一性が与えられる訳ではないもの の事です。

日本語では、最近翻訳で書き下ろされた(byくまぎさん)以下の説明が基本的に正しいです。

https://ja.m.wikipedia.org/wiki/Value_object

ちなみに、この本でValue Objectとしているものは、この本の参考文献として挙げられている"Secure By Design"という本のDomain Primitiveの概念とおおむね等価になっているので、以下で説明します。

Domain Primitiveの定義はこちらにあります↓

https://learning.oreilly.com/library/view/secure-by-design/9781617294358/c05.xhtml#h2-294358c05-0002

曰く、Domain Primitiveとは、

What you end up with is a value object so strict in its definition that, if it exists, it’ll also be valid. If it’s not valid, then it can’t exist. This type of value object is what we refer to as a domain primitive.

というもので、つまり作成時からずっと不変であって、かつ不正な値を保持して存在することはあってはならないモノのことです。良いコード/悪いコードの本でValue Objectのなるべく満たすべき条件としていたのはまさに不変性と不正な値で存在できない事であって、このDomain Primitiveの性質という事になりますね。

なお、このsecure by designでは

Nothing in a domain model should be represented by a language primitive or a generic type.

とまで書かれており、ドメインモデルのすべての要素はその言語のprimitiveで表現されるべきでないとまで言われています。私は個人的にはこれに全く賛同できませんが、ただ、これを主張する背景にある目的はsecureであるという事であって、その意味については理解できます。これは可読性や変更容易性を主たる目的とはしていないのです。

実際、secure by designでは

Section Problem area
Domain primitives and invariants Security issues caused by inexact, error-prone, and ambiguous code
(後略)

という事がかかれており、secureであることを目的としてのDomain Primitiveの考え方になっています。

この実装によってバグが減るので結果的に変更容易になるといった主張もあり得るとは思いますが、少なくとも直接的に変更容易性を上げるようなものではなく、実際にドメインモデルのフィールドすべてをprimitiveではないものにしたら、そのコードを読むときにも修正するときにも時間は確実にかかります。もちろん、大規模なシステムにおける効果などは議論の余地はあるので、時間がかかるから絶対的にダメというような事ではないです。

なお、特に日本においては、これをそのままValue Objectの性質として扱ってしまっている界隈があったりするようで特別に混乱しているようにも見えますが、Domain Primitiveという考え方自体は日本特有という事でもなく、DDDと切り離してスタンドアローンで導入できる良い方法であるとするQualcommの.NET開発者の方のブログ記事などもあります。
(私は、場面において有用なケースの存在を否定はしないものの、全体的に強いルールで導入することにはあまり賛同しないという立場です)

ところで、このDomain Primitiveの考え方は、Value Objectのうちの特に強い性質を持つものとして定義されているものの、個人的にはDDDのモデリングに関する本質的な部分ではないと思っています。
何をシステムの本質とするのは非常に難しく、システムが安全・厳重に動くという事もまた本質ではないのかと言われると強く反論することはできませんが、少なくとも機能設計とは対処する領域が異なるものです。つまり、「Domain Primitiveを使おう!」という事は、新機能の機能的な設計そのものには全く寄与しないのです。
もちろん、Domain Primitiveを使った設計という概念は存在して、Domain Primitiveを使って実際にものを組み上げるための設計をする事を否定はしませんし、それはそれで有用・意味のある事ではあると思います。しかし、「DDDの最も重要な部分」などではないであろう、従って設計の最も重要な部分でもないであろう、という事です。実際、上記のブログで

Domain primitives are often explained within the context of Domain-Driven Development (DDD), entities and value objects. But they are actually useful all on their own.

と書いてあるのは、DDDのモデリングの概念と切り離してDomain Primitiveの概念が成立するという事にほかなりません(重要性については特に比較はありませんが)。Domain Primitiveのようなテクニックは、機能性の概念と独立に/補うような形のテクニックであって、時に有効であり軽んじられるべきではないが、しかしDDDの核心という事ではない。と思います。

4.2.1 可変インスタンスの使い回しのサンプルコードについて

このサンプルコードのWeaponの引数がintでないAttackPowerで、AttackPowerの引数がintになっていますが、これはどのような設計意図なのでしょうか。一般論としては、このような設計はYAGNIというものに相当するのではないだろうか、と思ったりしました。
もちろん、今後の拡張性等によって意図的にこの設計を選択する可能性はありますが、とすればその判断こそが設計において最も本質的なことです。しかし、この本ではその説明はありません。
また、そもそも的な話をすると、AttackPowerクラスを使わずにintを使っていれば、intは値なので同一オブジェクトを別インスタンスで共有する事による問題などは発生せず、ここで述べたバグは自然と発生しません。
最終的に不変なオブジェクトにすることでバグを防ぐという考え方は結構なことだと思いますが、ここで説明されているバグはAttackPowerというintをラップしたクラスを使うという判断/設計をした事によって生じたバグであるとも解釈でき、そのような点では敢えてバグりやすいコードを書いているという解釈も成立します。

※AttackPowerのコンストラクタはint、WeaponクラスのコンストラクタはAttackPowerを引数にとる、それぞれ1つしかプライベートフィールドを持たないDomain Primitiveなクラスです。

「どんなとき可変にしてよいか」の内容について

ここの説明はmutableと再代入が混ざってる気がしています。値のfinalは確かにimmutable的なところがあり、immutableなオブジェクトを作るためにfinalは使いますが、一方でfinalの効果は再代入禁止であって、immutableという事ではないです。
「ループ処理のスコープでしか使われない事が確実なローカル変数は可変にしてよい」というのは、字義通りにいうと

for(SomeObj obj: objList) {
    obj.value = 100;  // mutable
}

ということです。

※本文ではループカウンタ"など"となっていますが、狭義のループカウンタと限定されているわけではありません。

これを踏まえて、ここの可変が本当にmutableの事を指し示すとすれば、「そのスコープの中で使用するクラスの定義の中ではfinalを使わなくていい」ということで、クラス定義の方ではそのクラスがどういうスコープで使われるか自明ではなく、破綻しています。(上記の例のSomeObj)
ここの可変がmutableのことではなくてfinalでないこととするなら、可変/不変という言葉は適切ではないです。

※finalとmutableの違いについては、たとえば

https://en.wikipedia.org/wiki/Final_(Java)

Note: If the variable is a reference, this means that the variable cannot be re-bound to reference another object. But the object that it references is still mutable, if it was originally mutable.

「5.2.2 生成ロジックが増えすぎたらファクトリクラスを検討すること」について

生成ロジックが増えたらファクトリメソッドを検討するという考えは正しいと思います。ただし、それを実際にサンプルコードとして挙げられている3000と10000の2パターンしかないポイントについて検討するとしたら、それはただの定数でやるべきではないかと思いました。(生成をする必要がない。)

データとロジックの書く場所の整理について

データとロジックを近くに書くべきというのは原則として正しいと思いますが、その場合の課題として

v = a.methodA()
w = b.methodB()
x = c.methodC()

みたいな時に、このメソッドを軸にしてコードを把握するために読む箇所がバラバラになってしまうという事があります。このmethodA, methodB, methodCが本質的に多相性を利用するようなものであれば仕方がないところもありますが、多相性が必要なものではなくこのメソッドの中でだけ利用するような種類のものだとしたら、敢えてクラスの定義の方には書かないほうが見通しが良くなる場合もあり、検討余地があります。

さらに付け加えると、こういうときに、実質的には数値のDomain Primitiveについて+や-ではない独自定義のadd()などを使うと、実際の処理がなにかを把握するまでに時間がかかる場合があります。これは、

  • 共通の言語仕様によるわかりわすさ/なんでもできてしまう事
  • 個別の定義のわかりにくさ/やる事が制限される事

のわかりやすさと制限のトレードオフ的な側面があるので、注意が必要と思いました。
(常にすべてがトレードオフだという事ではないですが。)

また、(必ずしもこの本の内容ではないですが)多相性は便利な一方で、定義がどこにあるか目の前のコードでは確定せず、実際に実行時にどのオブジェクトが渡されるかによって変わってきます。そうすると、コードリーディングだけで内容を理解するための難易度が少し上がり、ある側面では可読性が下がると言えそうです。

メソッドチェーン(メソッドチェイン)について

メソッドチェーンは私も好きではない場合が多いのですが、とはいえライブラリ等によってはメソッドチェーンによる記述が必要となったり、書きやすいという場合もあります。代表的なのはLINQやSQLAlchemy、eloquentなどにおけるクエリや、データ加工に関する記述です。
というか、例で書かれているものはメソッドチェーンではなくて、メンバー変数チェーンなのでは...
プロパティもメソッドであるといえば、それはそうかもしれませんが、サンプルコードはjavaで、

party.members[memberId].equipments.armor = newArmor;

というのはメソッドチェーンではないような気がしました。メソッドチェーンは一般に必ずしも 内部詳細を渡り歩くつくり ではないように思います。
5.6の見出しは「メソッドチェイン」となっていますが、仮にこのようなコードを批判するとしたら、メソッドチェインではなくて「ネストしたメンバ変数の参照やmutation」などの方が適切なように思います(そもそもmutateしているので)

MagicPointやEquipmentsのサンプルコードの品質、ポリシーについて

MagicPointやEquipmentsなど、コード断片だけでは設計意図を見いだせない ようなサンプルコードが並んでいるように思います。
たとえば、MagicPointクラスについて、アイテム等の効果によって上昇するMPの差分をリストで管理するということ自体は、いろいろな上昇補正を考えると合理的に思われる部分もあります。
一方で、このリストにはMP上昇の発生元の情報がないので、装備を変更した際には毎回すべての装備をチェックしてMP上昇量を再計算する必要があります。
また、例えばMPの変更量が比率で決まる場合には、このリストには比率も保持されていないため、例えばレベルアップした時にもパーセンテージ補正は全部リストを作る元ネタを参照して計算し直す必要があります。
そのように考えると、リスト構造自体にはメリットがあり得るとしても、現状のintだけのデータでは結局リストにしている事のメリットが何か?という事が不明瞭で、この場合は合計した上昇数値だけを持っていても事実上の差がありません。

このように、何を意図してリストで持っているのかという設計意図が見いだせない、悪く言うと中途半端なコードが見受けられます。

余談ですが、このような実装において、例えば「HPMP入れ替えマテリア」が要求として上がってきた場合に、それはどう実装するのか?と思いました。
MPとは関係ないですが、オールセブンフィーバーとかもどの層で実装するのか興味がわきました。
HPMP入れ替えマテリアやオールセブンフィーバーというのは、かなりの無茶振りのようにも見えますが、実際には開発を進めていて当初想定を超える要望が出てくるということはよくあります。
このような無茶振りに常に対応しないといけないという事ではないですが、HitPointやMagicPointのクラスを作り込みすぎる事によって、ある特定の"閾値"を超えると途端に修正がしにくくなる、という事もあります。そのバランスをどこに置くか、というのは設計において重要な観点ですが、そのような観点がサンプルコードから伝わらないなあと思いました。(これはちょっと意地悪かもしれませんが...)

別のサンプルで、Equipmentsというものもあります。これは装備中の防具一覧(Equipmentsなのに武器はどこへ??)のようですが、

class Equipments {
    private boolean canChange;
    private Equipment head;
    private Equipment armor;
    private Equipment arm;
    ...
}

となっています。この構造は私にはよくわからず、というのもArmor/Head/Armは従来の主張であればそれぞれ別のクラスであった方がよいのではないかと思うのに、Equipmentクラスで統一されています。腕用の防具、頭用の防具、みたいなことは、それぞれの防具の方が持つべき値だと思いますし、型だけでいえばheadに装備すべき防具をarmorに装備するという事もできてしまいます。これは、従来の型で細かく区別すべきという主張と一貫性が無いように思えます。
HitPointとMagicPointはクラスが違うのに、これらは同じEquipmentなのはなぜ?
私には設計意図が理解できませんでした。

ちなみに、canChangeというのも全体にかかるフラグとして存在するようなもので良いのか?とか、Equipmentクラスがある中でEquipmentsというクラスを作るのか?とか、細かい部分でも気になることはあります。
私としては、Equipmentsと言われるとEquipmentの単純なリストに近いものを想像してしまいますが、実際には「ある一人の人の装備品」みたいな意味を持っているので、例えばMembersEquipmentとか、人と紐づく概念であることを明示しないと厳しいかなあと思います。名前を大事にする本のはずなのに、名前を大事にしてないように感じます。

このような事がサンプルコード全般に多く、本当にただそのサンプルコードだけを切り取ればありふれたコードであったとしても、全体でどうポリシーを持って整理するのかという事が全く感じられません。言葉を選ばずに言うと、「グランドデザインのない作業員が作業のために最適化したコード設計」といったようにも見えます。
この本で紹介されている一つ一つのテクニックそれ自体は効果的にも使えると思いますが、本来コードを書くときにはグランドデザインが必要で、そのグランドデザインが不在で目先の対応をしている、というように見えてしまいます。木を見て森を見ず、というような。

条件分岐に対する抽象的な考え方について

6章、switch文と単一責任選択の原則について冒頭に書いてある抽象論は正しいと思います。ただし、interfaceについては...以下に記載します。

interfaceをどう定義するかということについて

「なんの仲間であるか」がinterface命名の決め手

という記述があり、Fire、Shiden、HellFireというクラスからMagicというinterfaceを"導出"していますが、これは設計が先に無いという点において、必ずしも「決め手」ではないかなと思いました。つまり、現在既に出来上がったコードがあって、それを見て決める場合にはそういう決め方は確かにありますが、このような場合には本質的な設計が先立つ事のほうが多いと思います。その場合には「なんの仲間であるか」という考え方もありますが、それよりも「何を期待するか」という機能的な考え方がよくあるように思います。例えばJavaではSerializable、Cloneable、(Androidだけど)Percelableなどが該当します。

この辺も、ボトムアップな命名法に言及しつつトップダウン/機能的な考え方がないため、グランドデザインなしの設計が前提になっているように見えてしまいます。「グランドデザインなしのテクニカルなリファクタ用」といった印象です。

なお、このサンプルについて多少うがった見方をすると、「火属性魔法」などといった結論にならないように敢えて紫電という魔法を入れているようにも見えます。単に「なんの仲間であるか」という観点だと「火属性魔法」みたいなのも全然ありえてしまいますが、この場で求められている意味・機能性から「Magic」という落し所に暗黙のうちに落とされているように思います。暗黙的にその結論に至らせたような考え方の方にこそ、設計の本質があるのでは、と思いました。

switchの代わりのMapについて

この本ではswitchの代わりにMapを使うことを推奨していますが、確かにMapを使うことでコードの見た目ではswitchによる分岐は消えるものの、結局コードの本質としてはMapを定義するところでswtichで分岐しているのと同じであって、そこにロジック的な違いがあるわけではありません。定義上の循環的複雑度(CC)は下がるので、単にCCを下げるためだけのハックとしては機能しますが、これはツールを騙すためのテクニックのように感じます。
実際、コードカバレッジはむしろみにくくなります。関数がマップされているタイプであれば、コードカバレッジに反映されますが、これは生成したインスタンスがマップされているため、まさにこのマップのこのパターンを通過した というデータはコードカバレッジに出ません。

classの数が増えることについて

HellFireなどのクラスについて、動画モーションや個別の効果なども含めるとどこまでプログラムで制御するのかという設計が難しいと思いますが、仮に200個魔法があったら200クラス作るのか?という事は気になりました。
典型的な魔法については、構造体の配列などで済ませたくなる事もあるのかなと思います。
これはどういうゲームを作るか次第なのかな、とも思いますが。

interfaceと未実装について

interfaceを使うと未実装のままリリースされる事がなくなるという記述がありましたが、適当なスタブを書いてコンパイルを通される可能性はあるので、そんなに本質的ではないかなと思いました。
実際、テストドリブンを厳密にやるとしたら、コードは一時的に「すべての仕様を満たさないが、現時点で定義されているテストケースに対してはグリーンになるコード」という状態になる事があります。例えば、一番最初のテストケースを作った直後のレッド→グリーンのフェーズでは、テスト的にはグリーンですが、最終的に求められる仕様を満足しているかどうかは不明です。この状態でもし誤ってコミットしたりすると、当然コンパイルは通ってテストもできているものの、実質的には未実装という事になってしまいます。

7.2のサンプルコードについて

毒のダメージの処理についてです。従来の論調だと、毒の処理はmembersのメソッドであるべきではという事を思うのですが、ここではそうなっていません。しかも、HP>0の判定がいろんなところで行われているのですが、HPに対してはisZero()というメソッドを2.4などで作っていたはずが、使っていません。
それで、「これこそ似たような判定がいろんなところで行われる実例では??」という事を思ったのですが、このサンプルコードにおけるHP、memberのhitPointはHitPointクラスではなくて、intなのですね。

なぜ?

このような点について、グランドデザイン/設計ポリシーがないという事を感じます。全体的に、目先でコードを改善する対応をしても、結局本質的に設計ポリシーがなくて、悪いコードを書いてしまっている、というように見えてしまいます。
筆者からすれば、サンプル同士の関連はないと言いたいかもしれないですが、一見して一貫性がありそうな似たようなテーマでサンプルを作っておいて、こちらはHitPointクラスを使う(しかもintをそのまま使うことをかなり批判した上で)、一方ではHitPointクラスを使わずintを使う、といった事については、Domain Primitiveの考え方を徹底するとしてもしないとしても、いずれの立場においても一貫性のなさを強く感じます。
やや極論すると、コードに一貫性がなくても良い、というメッセージを発信しているようにすら見えてしまうので、こうしたサンプルコードのあり方については改善されると良いなあと思いました。

全体的な設計に対する違和感について

こうした設計の大きな違和感の一つは、ロジックの概形と分岐させたいポイントを抽出してから設計する、というのがたりないところなのかなと感じました。
例えば、割引についての仕様を整理するとしたら、金額や種類を抽象化して、普遍的に行う作業がなにかを概念化して、それを個別のクラスで実装できるように切り出すという設計の仕方が私にはすんなりときます。
これは、多少の試行錯誤を経てボトムアップに考える事もありますが、自分やドメインエキスパートが割引というものをどのような概念として捉えていて、それをどう整理すると自然になるのか、といった観点でのトップダウン的な整理を行う事もあり、その考え方の両方を用いて最終的にモデルを作り上げていく、というのがよくある設計なのではないかと思っています。
ここでいうトップダウン的な整理が、この本で説明されている技法からは完全に抜け落ちているように思いました。

似たような話として、データの役割を設計できてない部分がある、という事もあるのかなと思います。
永続化するデータとか、入稿するデータとか、要はプログラマが直接書く外でできるデータをどう扱うかの設計です。
RPGの攻撃力や防御力、魔法の効果といったものは、特に一度作って納品をしたら終わりというタイプのゲームであればプログラムの中に組み入れられれば十分なものだと思います。しかし、業務システムにおいて日常的にユーザーによって追加登録される割引の概念というような見方をすると、「ある特定の割引をプログラムの中に組み入れれば終わり」という事にはなりません。
例えば、通常割引と夏季特別割引を含む割引の仕様を扱う場合には、システム的には柔軟な仕組みで設計しておいて、業務ユーザーが金額などを最終設計してマスタデータ管理業務で入力する、といったことも普通に行われます。その場合には、その割引が通常割引か夏季特別割引かという概念はあまり重要ではなくて、ある程度汎用的な割引をどのようにして登録可能・適用可能にするか、という事が重要です。
そのような全体設計が、サンプルコードの端から見えるようなものになっていればよかったのですが、サンプルコードはツギハギというイメージで、全体設計が見えるような作りにはなっていないと思います。

こうした全体設計不在という事については、他にもサンプルコードの「PositiveFeelings」などにも見受けられます。
(PositiveFeelingsという感情によって行動の効果が上昇するという仕組みについてのサンプルコードです。)
PositiveFeelingsは内部的な制御ではありますが、それ以前にゲームシステムであり、根本的な設計です。
たとえばファイアーエムブレムみたいに隣同士で戦うとPositiveFeelingsが上がるとか、PositiveFeelingsのCRUDのようなものを考えて、はじめてどう持たせるべきかという事が決まるものであると思います。それを、部分で切り取って説明するのは、本質的な設計についての説明・議論・検討になっていないのではないかと思いました。

「9.8 設計秩序を破壊するメタプログラミング」について

メタプログラミングを濫用すると読みにくい・デバッグしにくくなりがち、という事には賛同するので、濫用よくないという主張は否定しませんが、ここで実際に破壊されているのは設計秩序というよりもテクニカルなリファクタのしやすさかなと思いました。
例えば、メタプログラミング的な手法で書かれたコードであっても、「ここにクラス名を文字列として使っている」といった構造が読んでわかるような書き方をされているならば、静的解析ができなくても秩序はあります。
その意味で設計秩序が直ちに破壊されるという事ではないように思います。

名前設計の考え方について

名前で関心を分離するということ、それ自体は正しいので10.1と10.2は主張としてはわかります。
ただし問題は、実際の画面においてはその分割された商品という情報+α以上がすべて一画面で処理されるような場合が多々あるということかなと思います。
その一つの画面の実装を理解するために、個別の定義を見るのかという認知的な負荷の上昇について、構造が細かく分割されている事と比較してどう評価するか?というのは難しい問題であると感じます。

驚き最小の原則のサンプルコードの命名について

tryAddGiftPoint()というメソッドがあるが、私はtryと言われると失敗するのかなと思ってしまいます。
判定条件によって何かを行うという事ではなくて、例えばtry/catchが中で使われるとかそういうようなイメージです。ここでやりたい事は、つけるべきポイントがあればポイントを付与するという事なのだと思いますが、私としてはポイントが0の場合など関係なくとにかくトライする、といった印象を受けてしまいます。
個人的には、文法的に意味のある単語はなるべく違う意味で使わない方がよいかなと思いました。
もし私が名前をつけるとしたら、addGiftPointIfNeededとか、そういったものになりそうです(英語としてもっと良い表現があるかなと思いますが...)

11の「退化コメント」について

退化コメントについての指摘は非常に正しいと思います。10と11は大意としては正しいと思います、サンプルの品質を除いて。

13章、モデルの定義やモデリングの考え方について

13章に出てくるモデルの定義、

動作原理やしくみをかんたんに理解・説明するために、物事の特徴や関係性を図式化したものをモデル

システム構造を説明するために、単純な箱で図式化したものをモデルといいます。

これはちょっと違和感があります。私としては、図式はあくまでもモデルの図式であって、モデル自体は抽象化された存在の事であると思いますが、「図式とモデルが同型だから図式の事をモデルという」という言い方も成立しなくはないので、ギリギリ許容範囲かもしれません。私の感性とはちょっと違いますが。

また、

ここで、商品はどういうモデルになるでしょうか。商品にはさまざまな付帯要素(情報)があります。
(中略)
これらすべてを盛り込むと、モデルの目的がわからなくなります。取り扱うデータが爆発的に増え、現実的ではありません。

この記述はある側面では正しくて、関係ないものは当然分解していくべきであり、困難は分割せよという前提はあると思います。しかし一方で、システムができる限り多くのものを統合して簡便に扱えるとしたら、それは相対的に優れたシステムであると言えます。そのような意味で、多くの性質を持つ要素を、その瞬間瞬間では着目すべき部分を限定しながらも、全体では統合されたモデルとして組み上げた場合、それは優れたモデルであり得ます。
それを踏まえて、ある程度は統合したシステムの設計を目指すとき、分割する事自体にコストがあったり、そもそも分割すべき概念か否かの判定をしたり、というのは本質的で難しいことです。
例えば、何度も出てくる夏季割引価格ですが、これは割引という汎用概念のうちの時期指定割引として整理ができて、それは普通に割引に登録できる内容として、システム上の概念として整理する("線を引く")事もできます。
一般に、システムで取り扱う様々な概念について、それらのどこに概念を分割するための線を引くかは設計の本質的なテクニック・関心事です。DDDにおいて「ドメインと向き合う」というのは、システムで取り扱う概念についての線引きを行うとき、実際のドメインにおける線引きと最終的には一致するように為されるべき 、ということです。(ここで、システムの細かい内部的な仕組みまで含めすべて一致するかは一旦おくとして。)
やや極論でいうと、ドメインエキスパート/実際の利用者の頭の中で、それらの割引を同列に扱っているなら同列に扱える仕組みが望まれるし、異なる扱いをしているならシステムとしても異なる扱いをする仕組みが望まれる、という事と思います。
※ただし、実際には新しい設計を導入してそれに合わせてドメインエキスパートを教育するという事も考えられるので、必ずしも今まさに実践されている事がベストという事ではないです。最終的にシステム利用者の頭がシステムの外部設計と一致すべき、という事です。

それを踏まえて、様々な考えられる線引きのメリデメをきちんと論じて、例えば今回は夏季割引を分ける、みたいな話ならわかりますが、そういう話の仕方ではなくて、一面的にこの本で述べられているような解釈ができるという事だけを根拠にして夏季割引とその他割引を分割せずに扱うことをNGとしているのが、正しくないあり方であるように思います。

長くなりましたが、必ずしも「最終的な結論として夏季割引を通常の割引と分けるか分けないかのどちらかであるべきだ」という主張ではなくて、きちんとした検討や対話を経てその結論を出すというアプローチであるべき、というのが私の言いたいことです。このあたりを、ロールプレイ的に別の考え方についても検討をした上での結論として書いていないので、私はこの本のアプローチに同意できないと思いました。

可読性について

この本ではよく可読性という言葉が出てきますが、そもそも本書で推奨される手法、例えばDomain Primitive(この本の中ではValue Objectと書かれているもの)を徹底することで可読性が上がるかというと、 むしろ可読性は下がる場合があります。 この事ははっきり書かれていませんが、重要な観点なので注意しましょう(Domain Primitiveについては既にこの感想内でも述べていますが、それに限らず)。
一部のテクニックは、可読性を多少下げてでも安全性を担保しようとしているのです。

その他、可読性の難しさについて以下言及しておきます。

この本、というよりは設計についての議論全般の中で、「ある種のダイアグラムがわかると意味がわかるが、ダイアグラムがないと読み解けないコード」というものがしばしば出てきます。このようなコードについて、可読性が高いと言えるのかは、中々難しいです。「図式があれば/一度理解してしまえば内容を深く理解できるが、その理解まで至らなければ難解で意味不明なコード」といったものもあり得るため、特定の記述の可読性が高い、低いというのは簡単な事ではありません。

他にも込み入った例はいくつもあり、例えば、純粋に宣言的な定義になっていれば、プログラムはその定義の箇所を見たり追いかけたりでどうにかなります。しかし、実際には多くの場合ステートがあり、現在の状態がどうなのか、といった事を考えないと読み解けない場合があります。また、システムが実際に動くという場面では「どこで誤ったデータが混入したのか」みたいな事を特定する必要がどうしても出てきますが、そのような場合においては、やや極論すると、初期化処理が細かく分割されたりポリモーフィズムに依存している設計よりも巨大関数のほうがまだ読みやすいという事も十分にあり得ます。(だからといって直ちに巨大関数がよいとは言っていません、念の為。)
一般に「可読性」という指標はおそらく一直線上に並べて順序をつけられるようなものではなく、かつ読み手にも依存するものだと思うので、そのような点においても可読性は複雑で難しい概念です。
そこで、可読性という概念については、もう少し解像度を上げて、例えば処理の追いかけやすさとか、想定しないといけない状態の量とか、ポリモーフィズムで考慮する必要のある組み合わせのパターンとか、他にもいろいろな事をそれぞれ別に考慮する必要があるのかなと思います。

14章の大意について

14章の大意、テストを書いてから意味を把握してリファクタしていくという流れ自体は正しいと思います。
14.5も抽象論として正しいです。

14章のサンプルコードについて

14章では、なぜ3章で定義したMoneyクラスを最終的に使うようにリファクタしていないのだろうか?と思いました。
3章「一方、Money型のように独自の型を用いると、異なる型の値が渡された場合にコンパイルエラーで弾くことができます。」

ここで使ってない理由は何で、コンパイルエラーで弾かなくても良いという判断はどのように為されたのでしょうか。このあたりの理由がないと、どうも一貫性がないように感じてしまいます。

15章の大意について

抽象論としてはあまり間違っていないとは思うのですが、知識以外に何とトレードオフになって技術的負債が生じるのか、という事を論じないと片手落ちではあるように感じました。
無限には時間がない、というニュアンスの記述も後半にありますが、単純な時間の有無だけではなく、他にトレードオフになっている要素もあると思います。

動くコードを速く書くことについて

「動くコードを速く書くこと」についての話がありますが、私の経験では、確かに一部例外的に「早いけどコードめちゃくちゃ」という人は確かにいるものの、経験的にはコードを書く速さと正確性には正相関があるように感じます。あくまでも私の経験の範囲ですが。
同一の人が実装をするときに、テストコードを先に書くほうが効率が良いか、後に書くほうが効率が良いかは中々難しいですが、大体の場合は速く書けること自体は効果的です。
少し言い方を変えると、速く書けないという事は多分その人にとって難しい/シンプルではない事をやろうとしているという解釈もできるため、そのような難しいコードを書くべきでないのかもしれません。

実装や保守の時間を最適化することについて

たった一度の設計では、良き構造は見いだせません。

という記述があります。そういう部分は確かにあると思いますが、まさしくこの本に書いてあるとおり時間は無限にないので、そのような"時間不足"を踏まえてどこまで設計を丁寧にやるのか、判断して決める必要があります。端的に言えば、じゃあ一度でないならば何回設計すればよいのか、という事です。何を目安に設計を"切り上げる"のか、といった実用的な知識が求められているように思いました。

また、実装上のパフォーマンスについて、

パフォーマンスが落ちるからクラスを追加しない

という意見に対する反論を述べており、特に最初からパフォーマンスを求めるのはやりすぎという論調で話を進めています。Webアプリ等で、限られた個数のデータをインスタンス化するときに十分無視できるという事は同意しますが、例えばcsv出力などの機能で、100万件のデータについて、複雑な階層化されたクラスのインスタンスを生成するとしたら、パフォーマンスに影響が出てしまいます。
このようなレベルで、最初から明らかにパフォーマンスに影響する箇所とそうでない箇所のアタリがつくのであれば、アタリをつけておく事によって大幅に後で効率化する時間を減らせる場合があります。はじめから極端なパフォーマンス向上を求めていたとしたら問題かもしれませんが、元々シビアな処理であるという事がわかっているならば、はじめからパフォーマンスを求めるという判断は十分にあり得ます。そのような意味で、すべての事について「計測してから高速化する」というのは誤りで、過去の経験やその他の書物等で最初から速く動くように書くことが推奨される箇所については、最高速を追い求めるようなことはしなくても、普通に想定される要件を満たすようなコードを最初から書く 習慣をつける事は重要です。

ただし、正しいコードを速く書くこと自体にもまた意味があります。結果が正しいことを保証するためのプログラミングやテストの時間を評価して、クラスを追加したほうがその時間が短くなるならば、一旦クラスを追加しておいてから、リファクタによってクラスのないコードも作って、そちらを動かす(ただしテストとしてクラスを残す)といった戦略も十分に考えられます。
いずれにしても、単にクラスを追加する/しないという手法だけを評価するのではなくて、実装に総合的にかかる時間や、いま大事にすべきリソースが何かを考えて、大事にすべきリソースを特に最適化するような戦略を立てるという事になると思います。
(例えば本番環境で動くコンピュータリソースと、人間がプログラミングするリソースと、どちらが大事なのかは大企業と零細企業では異なる場合があります)
これは具体的な設計においても本質的に同じ事だと思いますが、このように必要な要件を考えてそこから逆算してあるべき姿を考えるのが設計する事の重要なポイントであって、決してボトムアップ的に"最適化"することだけが設計という事ではありません。

やや蛇足かもしれませんが、そもそも、本書で紹介している手法は「知識がありさえすれば、同じ手間でより保守性の高いコードを書けるようになる」「一部は実装に時間がかかるように見えても、トータルで見ると保守性などの点で効果がある」というものですが、これは 「必ずしもプログラムを書いている瞬間や書き終わった瞬間には差を評価する事ができない品質」 を向上させるためのものです。ここで、もしパフォーマンス問題についてYAGNIのような事を主張するとしたら、論理構造的には同じような事を本書で紹介している手法に対しても言えてしまいます。具体的には、Domain Primitiveを細かく分けて実装するのは、実際に改修などでそれが問題になっていない限りはYAGNIである、というような事です。後述する話題においては、実際に筆者がレビュイーからそのような意見によって設計改善の提案を退けられた、という事が述べられていますが、私としては、このような論理構造の定量性のない反論は、いずれの立場から見ても本質的に無意味な意見なのだと思います。

心理的安全性に関する即落ち2コマ

本書より、

レビューの心理的安全性についてのコメント、

あかんやん!

一応補足をしておくと、たしかにこのコード自体は他人が書いたものではないかもしれませんが、たとえ自分のコードであっても、普段からひどいとか悪魔とかいう言い方をしてしまうと、(作者を害する意図がないとしても)そのような言葉をうっかり使ってしまう可能性があります。
この本が教科書的に使われるべき入門書であるならば、こうした言葉遣いには注意した方が良いのかなと思いました。

即落ち2コマと言ったな?あれは嘘だ

以下、本書より。


個人的には、この矛盾はなかなか深刻であるように思われました。
というのも、レビュイーに十分な能力があって善意であると仮定するなら、「管理は管理でいいじゃないですか」でおしまいです。
レビュアーは仕様変更時の課題になりそうと言っていますが、実際に仕様変更が発生しやすいか否かなどの根拠は(少なくともこの本の中では)明示されず、説明不足で、レビュイーに退けられてしまいました。
にも関わらず、レビューがまともに機能しない、設計力がない、などと他責的な結論に至っています。この判断が本当に誤っているのかも、また判断がレビュアーとレビュイーで分かれた理由も設計力の問題なのか前提の違いや判断方法の差なのか、この会話だけだと何もわかりません。
「説得が一番」などと書いてあるのは飛躍しすぎで、「まずはもっときちんと話をしろ」という事でしかないように思います。
そもそも、真に心理的安全性のあるチームであれば、レビュイーが自分の意見を主張するのは当たり前のことで、それを「自分が正しいのに相手が理解しない」というような姿勢で話すのは、根本的に間違っていると思いました。これは、必ずしも設計技術と直接関係無いことかもしれませんが、非常に大事なことだと思います。なぜ本書のレビュアーの誰も、こんな大事なことを指摘しなかったのだろう、と思いました。(私が大事だと思っただけで、実際には大事ではないのかもしれません。)

※補足ですが、このような他責的なあり方が常に悪い効果を生じるという事でもなく、むしろ他責的であるからこそ(自分自身が何らかの改善をして読解困難なコードに対応するという方向を向かないからこそ)コードの記述を改善する方向に目が向く可能性もあるので、そのスタンスが直ちに悪いという事でもないと思っています。ただ、この本の中で示されているレビューのあり方と上記の内容は反しているように感じたので、それを以って深刻な矛盾と表現しました。

むすび

具体的な感想は以上です。色々と考えるところはありますが、少なくともそのような考えるポイントを提供しているという点において、この本は既に重要な役割を果たしていると思います。ただ、そのまま無批判にすべての内容を受け入れてしまうと、一般的な定義と乖離があったり、筆者の主観であったりといった事柄について、偏った知識が身についてしまうと思います。設計の入門という意味では、たしかに有る種の設計の入門かもしれないけれども、王道的な設計の入門・まずはじめに抑えるべき設計とはちょっと違うのでは?という気もしました。モデリングのもっと本質的な方法論を学んだり、ポリシーを持ってコードを書く練習をする方が、設計の基礎としては有効なように思います。
私個人でいえば、Domain Primitiveの定義などは、この本を読まなければしばらくは接する事がなかったかもしれないので、そのような知識を進んで得た・自分で考えたという事には価値があったと思います。そのきっかけを得られたのはまぎれもなくこの本のおかげであり、感謝しています。その"恩返し"がこの記事によってできれば、と思います。

Discussion

ログインするとコメントできます