前振り
以下の勉強会に参加してきた際の、議事録を残す。
今回の範囲は、7つの設計原理のうち、
・単純原理
・同型原理
・対称原理
の3つである。
全体の概要
この7つは、初歩的なことをいっており、今までの復習に近い内容の構成となっていた。
これらの観点から多角的にレビューを行うことで、障害が起きやすいコードになりにくい。
ちなみに、議論の中で
「コードレビュー指針を整備してる人はいるか?」
という質問が出たので、
どの品質軸の考察は、学習の5段階のどの階層にいるのか?をプロットし、
定期的に更新していく。
と回答しました。
レベル3まで突入したら、
最早誰にも言われずとも、変更容易性の高いコードを書くとか、
セキュリティの観点で「信頼境界の位置はこれで本当にいいだろうか?」
をチーム全体でできている状態。
レベル4までいったら、コード規約として明文化するまでもない。
7つの原理の関係性
7つは互いに独立したものではなく、
それぞれが互いに原因と結果や、鶏と卵の関係性があると感じたため、
以下の図を描きました。
階層構造がSLAP式になっているからこそ、単純でわかりやすくなっている。
わかりやすい構造であるからこそ、安全にコードを修正できる。
といった流れである。
単純原理
これは 「シンプルさにこだわれ」 ということを指しています。
ちょうど、KISSの法則のことですね。
シンプルさのためのループバックチェックによる名前設計も重要です。
これは、モデルでもそうだし、コードでもそう。
なんでシンプルに?
コードが複雑になりやすい部分は、バグの温床です。
しかもそういうところは、テスト計画も立てにくい。
負債を生み出しやすくなり、コスト増大⇒利益が減る要因となります。
シンプルでないと、意図が不明瞭にもなりやすい。
条件が「&& or && or &」みたいなところは確かにバグの温床。
それをアクティビティ図とかで表現しようとすると、
も~~図が複雑怪奇になってしまって、「結局この図って何を言いたいの?」
ってなってしまいます。
しかも、
仕様変更があった際に、その図のどこを変更していいのか?わからない。
⇒ ソースコードのどこを変更していいのかわからない。
てことになりえます。
コアドメイン部分とかは、どうしてもただでさえ複雑化しやすいため、
シンプルさを保たないと、あっという間に意味不明になり、
負債を踏み出しやすい箇所へと姿を変えてしまいやすい。
対処法
特定の人しか読めない高級なコードを書かない
「できるひとほど理解しやすくてメンテナンスしやすい、
普通のコードを書いてくれる印象があります。」
という議論が出ました。
これは、ダニングクルーガー曲線を参考にしていただけるといいと感じます。
どういうことかというと、
下図の①「バカの山」では、以下の状態が該当します。
覚えたてのデザインパターンを使いまくって、
結局その設計にしようとした意図が特にない。
⇒ あまり変更されない箇所に、不必要にinterfaceが存在し、意図不明。
難しい実装パターンで己の凄さを誇示している。
どちらも他の人がコードや設計レビューをする際に、意図がくみ取れません。
②の「絶望の谷」を経験した上で、だんだんと他人の読みやすいコードや
モデル図を描くように意識するようになってきます。
これは先ほどの「学習の5段階」とも密接に関わっています。
ボーイスカウトで常に単一目的を意識する
最初は認知負荷が小さい単一責務なコードであっても、
仕様追加など起きた際に、
本当にこのままで単一責務になっているかな?
と批判的にチェック(ボーイスカウト規則)をすることなく、
放置してしまうと、あっという間に複雑怪奇&認知負荷が高いコードになります。
バリエーションをシンプルな表で表現しておく
RDRAには、下図の青枠で囲った部分のように、
バリエーション・条件図というものがあります。
この表のように複雑な部分をシンプルに見やすくしておくことも重要です。
これをもしもアクティビティ図の◇分岐だけで表現しようものなら、図がカオス化します。
同型原理
定義が同じと見なす型のものは、例外なく同様に扱うということ。
これは、クラス定義Aの具体的なオブジェクト、
a1やa2は、同じ変域内での動作をするにもかかわらず、
a3だけは、例外的な動きをする などを許さずに、一様に扱うということ。
わかりやすく言えば、日本の学校教育のような
【型にはめて、同一のものとして扱う】ってこと。
なんで同型にするの?
端的に述べると、型を定義していることで、動作の予測が立てやすくなるからだ。
すると、例外的なものが抽出しやすくなる。
つまり、リスコフの置換原則に違反したものがあぶり出せるのだ。
この違反したものをそのままにしておくと、
バグの原因になるし、可読性も悪くなってしまう。
このことは、型定義による動作の推論と関連性が高いため、後述のHowで触れる。
どうすればいいか
推論と型定義の関係性を抑えれば、一貫したコードを書けるようになる。
帰納法と型定義
静的側面からの帰納法による型
クラス定義はオブジェクト図の一般化である。
下図を参照してほしい。
具体的な値を持つオブジェクト、A・B・C すべてが同じ属性定義を持つ。
それらを帰納的に抽出したものが、クラスの定義となる。
この時に、Cの田中さんだけ血液型という属性を持たない ということはあってはならないのだ。
もしもそのようなときには、オブジェクトCの田中さんは、人という型からは除外されることになる。
それだけでなく、もしも【人】という型の生年月日が、
たとえば 1970/01/01~1990/12/31 までしか許容されないような場合、
上図のオブジェクトA:鈴木さんは、この人というクラス定義からは除外される。
動的な側面からの帰納法による
上記ような関係性は、属性リストだけでなく、操作のリストにおいても言えることである。
戻り値の変域や、投げる例外など、とあるオブジェクトのみ他と異なるということはあってはならない。
その場合には、別のクラス定義として扱うことになる。
演繹法との関係性
型をメタモデルとして定義しておくことによって、その後の動作を容易に予測可能とすることができる。
これは、推論でいう所の演繹法に該当する。
一貫性を持たせる
同型になるようにするためには、
制約を設けて
アクターモデルも同型原理を適用せよ
たとえば、運転手というビジネスアクターがいるとします。
このアクターは、割り当てられたロジックとして、
荷物を届ける、配達先から出発する、支払いを受け取る、商品を回収するなどがあるとします。
ここで、運転手というグループの中の一部の人たちは、
上記の4つの処理をすべて行うが、一部の人たちは荷物を届けるしか行わないというケースが起きたとします。
このような状態は、この同型原理に反していることになります。
よって、グループの境界を改善し、アクター名も変更する必要があります。
解決策は、ユースケースモデルで、以下のように表現することです。
この際に、暗黙的にPIE原則を適用しています。
荷物を届ける という処理しかしていないのに、運転手という命名は、
明らかに違和感があります。
配達員という名前に変えることで、責務との矛盾がないようにしました。
これをさらにわかりやすくしたら、下図のようなユースケース図になります。
ただし、この場合には、運転手というアクターは、
配達員という型の特性をすべて継承する という縛りが生まれます。
ビジネスを設計する際に、本当にそうしていいのか?検討の余地があります。