この記事の続きです。
第4章 視点〜プログラマの観る角度〜
4.1 凝集度
モジュールに含まれる機能の純粋さを表す尺度でレベルは7段階ある(数字が高いほど良いモジュール)。
レベル1:暗合的強度
モジュール内の要素間に、特別の関係が認められない。
例:あるモジュールの中でたまたま重複している命令群があったので、結合して1つのモジュールにした。
このようなモジュールは自モジュール内の他の要素と関連性が弱い半面、他モジュールと強い関連を持っている傾向があり、保守が困難になる。
レベル2:論理的強度
ある機能を抽象的に捉えてまとめたもの。
-
悪い面
- 関連したいくつかの機能を含み、条件によって実行パスが異なるため一部の機能しか実行されずモジュール強度が弱い。
- 複数個の機能に対して同一のインターフェースを用いるためプログラマのミスを誘いやすい。
-
良い面
- 機能的に共通するものを含むので一部の論理を自モジュール内で再活用できる。
- プログラマの思考を集中させられる。
- 特定のデータ処理を1つのモジュールに局所化させる効果がある。
レベル3:時間的強度
特定の時点に連続して実行する複数の機能を1つのモジュールにまとめたもの。
機能間に強い関連性はないが、特定の時点に連続して実行している。
例として「初期処理モジュール」がある。
レベル4:手順的強度
問題を処理するために関係している複数個の機能のうちのいくつかを実行する。
この複数機能を順番に利用していくことになる。時間的強度の特性を持つが、手順的な関連性がある分、時間的強度より強い。
フロー・チャートの一部をモジュール化した場合などがこれに相当する。
レベル5:連絡的強度
手順的強度にプラスしてモジュール内機能間でデータの受け渡しをしたり、同じデータを参照する。
レベル6:情報的強度
特定のデータ構造を扱う複数機能を1つのモジュールにまとめたもの。
同一のデータ構造はできるだけ特定のモジュールでアクセスしようとする発想。こうしておけば情報隠蔽にもなり、データ構造を変更した影響もこのモジュール内に収めることができる。同じく情報隠蔽の特徴を持つ論理的強度モジュールとの違いは入り口を複数個持つことで各入り口は単一で固有の機能を実行するため、パラメータの扱いにくさを解消できる。
レベル7:機能的強度
モジュール内の全ての命令が1つの役割を実行するために関連しあっているモジュール。機能的強度を持ったモジュールの変更は、そのモジュールだけで処理でき、ほかモジュールへの影響度は他の強度のそれより小さくなる。
凝集度の高いモジュールのメリット
- コードの設計の明確さと理解のしやすさが高まる。
- コードの保守と拡張が容易になる。
- コードの再利用性が促進される。
- モジュール間の粗結合性も同時に促進させる。
逆に凝集度が低いとこれらが難しくなるので、開発の際はできるだけ機能的強度を持ったモジュールを目指す。
4.2 結合度
モジュールの結合度を表す尺度でレベルが高いほど疎結合で良いモジュール。
レベル1:内容結合
あるモジュールが他のモジュールの一部を共有するようなモジュールの結合の仕方。
他モジュールの外部宣言してないデータを直接参照したり、命令の一部を共有している。この場合内容変更がかなりの確率で障害に直結する。
レベル2:共通結合
「グローバル変数」のような共通域の定義データを、いくつかのモジュールが共同使用するような結合形式。
以下のようなデメリットがある。
- 共通域のデータはモジュール間のインターフェースに現れないので、コードの解読を非常に困難にする。
- 共通域のデータは、もともとそのデータに無関係なモジュールでもその気になれば利用できてしまうので、コードの安全性が低くなる。
- 共通結合しているモジュールは、共通域のデータを通じていろいろなモジュールとつながるため再利用性の阻害になる。
一方で以下のようなメリットも存在する。
- モジュール間の受け渡しパラメータの数を少なくすることができる。
しかし、そもそも受け渡しパラメータの数が多い時点でモジュールの設計がいまいちなので再設計してパラメータの数を少なくするのが良い。
レベル3:外部結合
public宣言された変数のような外部宣言したデータを共有したモジュール間の結合形式。
共通結合との違いとしては、必要なデータだけを外部宣言しているため、本来不必要なデータまで共有することがないこと。
レベル4:制御結合
呼び出す側が呼び出される側のモジュールの制御を指示するデータをパラメータとして渡す結合方式。
呼び出す側は呼び出される側の論理を知っている必要があるため結合度が強い。
また、呼び出される側のモジュールの凝集度が論理的強度になってしまう欠点もある。
レベル5:スタンプ結合
共通域にないデータ構造を2つのモジュールで受け渡しするような結合形態。
受け渡すデータ構造の一部は使用しないことがあるので、結合度を少し強くしている。
レベル6:データ結合
スカラ型のデータ要素だけをパラメータとして渡す結合形式。
モジュール間の結合は、明確化されたパラメータでデータを受け渡す、データ結合が一番良いとされる。
呼び出す側は入力と出力が何かということだけを知っていてそのなかでどのような変換が起こっているのかなどを知らない。
対応
- データの受け渡しはできるだけ引数で行う。
- データの置き場所はローカル変数に置くようにする。
- 渡す値に応じて動作が変わるようなコードを書かないようにする。
4.3 直交性
コード同士が「独立性」「分離性」を持つようにし、互いに影響を与えないようにする。
直交性のメリット
- 生産性の向上
- コードの局所化による開発期間とテスト期間の短縮。
- リスクの軽減
- 問題があったとしても影響範囲を最小限に抑えられる。
対策
モジュール間の結合度を最小にする。つまり、不要な情報は他のモジュールに公開せず、他のモジュールの実装を当てにしない。
そのためにレイヤー化のアプローチが有効。それぞれが独立した機能を持ったレイヤーを階層化してその積み重ねによっってソフトウェアを構成する。このとき各レイヤーは直下レイヤーの提供する抽象機能のみを利用する。
また結合度を強めないためにグローバル・データは用いないようにする。
レイヤー化の弱点
データを登録するソフトウェアのユーザーインターフェースにおいてはあるフィールドを追加するだけで全てのレイヤーにそのフィールドをついかしなければならないなど、変更が連鎖する場合。
4.4 可逆性
やり直しができるような設計にするということ。
そのためにコードに柔軟性を持たせ、特定の技術に依存することをやめる。
特にサードパーティとの間接化のレイヤーを設けるなどの決定は、アーキテクチャ設計段階でないと間に合わない。
4.5 コードの臭い
コードの中で理解しにくい、修正しにくい、拡張しにくいと感じられる部分のことを指す。
プログラマはそうした「臭い」を察知し、適切なコードに改善していかなければならない。
対応
「コードの臭い」の兆候を把握する。またいくつかの傾向があり、それぞれの「どういう状態が悪臭なのか」ということと「なぜこれが悪臭なのか」ということを把握することが重要。以下にいくつかの傾向を述べていく。
- よく見る
- 長すぎる
- 大きすぎる
- 多すぎる
- モジュールの過度な分割はこの問題を引き起こすため仕事をしていない仲介役などは削除したりして対応する。
- 名前が合わない
4.6 技術的負債
技術的負債とはコードにおける「修正しにくい」「理解しにくい」といった問題のある汚いコードのこと。
これらはビジネスにおける「借金」と似た概念で、問題を放置すればそれだけ「利子」が伴い問題が肥大化する。
対応
- まず、時間がかかってもいいので「きれいなコード」を書くことをベースの考えとして持ち、可能な限りそうする。
- しかし緊急で対応が必要な問題に対して時間をかけてしまうことで、現実のビジネスの被害を拡大させてしまうことは必ずしも良いとは言えない。その場合は技術的負債の考えを受け入れ一時的に素早く汚いコードを書いて対応する。
- 難を凌いだら変更前の状態に戻し、本来あるべき「きれいなコード」に素早く修正することで負債を返済する。
- 仮に負債を返済する時間がない場合は、本来書くべきであったコードの設計をドキュメントに残す。
第5章 習慣〜プログラマのルーティーン〜
5.1 プログラマの三大美徳
- 怠慢
- 全体の労力を減らすために気力を惜しまない気質。
- 繰り返しの「作業」をソフトウェアを作って「自動化」してしまう。
- 短気
- PCが効率的に動いていなかったり意図通りに動いていなかったら怒りを感じる気質。
- 起こりうる問題を想定して仕事をし、変更される可能性がある部分は応用が効くようにしておく。
- 傲慢
- 高いプライドを持ち人様に対して恥ずかしくないコードを書く。
- 自分で作ったソフトウェアはプロ意識を持って保守を容易にしておく。
5.2 ボーイスカウトの規則
ボーイスカウトには「自分がいた場所は、そこを出ていく時、来たときよりもきれいにしなければならない」という規則がある。
プログラミングにおいても自分が作業をする場所を完璧じゃなくてもいいので「少しでも」きれいにしてコミットするようにする。
5.3 パフォーマンスチューニングの箴言
パフォーマンスチューニングは早いコードを書くことだが、まず「正しい」「読みやすい」よいコードを書くようにする。
パフォーマンスチューニングには以下のようなデメリットがある。
- 可読性の低下
- 品質の低下
- 複雑性の増大
- 保守の阻害
- 環境間の競合
- 作業量の増大
- 最適化は長い時間を伴う作業なので、実用上問題ないレベルで動いているのであれば他の優先順位の高い仕事をしたほうが良い。
ソフトウェアのパフォーマンス
ソフトウェアのパフォーマンスはコード以外にも様々な要因によって左右される。
- 実行環境
- デプロイまたはインストールの設定
- 使用しているミドルウェア
- 使用しているライブラリ
- 相互運用している旧システム
- アーキテクチャ
こうしてみるとコード一行一行よりはるかにパフォーマンスに影響を与える項目がある。
アーキテクチャのパフォーマンス
アーキテクチャの影響は広範囲に及び後からの変更は困難を極めるため、アーキテクチャ設計時はパフォーマンスを考慮したほうがいい場合もある。例えば通信プロトコルや永続化形式ひおいてはパフォーマンスの足かせとなる設計を避けるべき。
このような部分に対しては早い段階でパフォーマンステストを実施し、その後も継続して行うことでパフォーマンスが急降下する変更が分かるようにする。
パフォーマンスチューニングの手順
ソフトウェアの特性によってコードの最適化が必要なことはよくあるので、パフォーマンスチューニングを試みる場合の鉄則を紹介する。
- 最適化の必要性を証明。
- パフォーマンスを計測し、ボトルネックを特定。
- パフォーマンスの低下の原因となる「ホットスポット」を見つける。
- プロファイラを利用する。
- 最適化の過程で何度も使うことになるので自動化しておく。
- ボトルネックのコードを最適化。
- パフォーマンスを計測し、最適化の効果を確認。
- 最適化したコードの動作に問題がないことを検証する。
5.4 エゴレスプログラミング
プログラミングの目的は自分の能力をアピールすることではなく「よりよいものを作る」こと。他人にコードを見せる時はアドバイスを謙虚に受け入れ、他人のコードを見るときは尊敬と経緯を持って接し、批評は人ではなくコードに対して行う。
エゴレスのバランス
自我は全ての原動力でここから効率的な仕事が生まれる可能性があるので、自分の個性を殺さずエゴのバランスを取ることが大事。
5.5 1歩ずつ少しずつ
プログラミングは一度に小さな一つのことをやるようにする。
例えば、モジュールの移動とモジュール名の変更は別々に行う。少しずつテストコードと本番コードを書く。
思考も「1歩ずつ、少しずつ」
- 考え始めて、すぐ結論に飛びつくのは危険。条件を満たすものが1つ見つかったときに思考停止せず、思い込みを排除して他の可能性も検討する。
- すでに考えたことを記憶し「思考のループ」に陥らないようにする。
- 書きながら考える。
5.6 TMTOWTDI(There's more than one way to do it.)
他の人に使ってもらうツール(プログラミング言語、DSL、API)を設計する場合、達成したいことの手段を複数用意する。これによりツール自体は複雑になるが、使う側はシンプルなコードが書けるようになる。
選択肢の多いツールは習熟に時間がかかるというデメリットがあるが、慣れてくればシンプルで美しい使い方ができる。
なぜツールは複雑でも良いか
ツールはその性質上、作る時間より使われる時間のほうが多くなる。したがって、全体最適を考えたときにツール側を複雑にして、使う側のシンプルさを求めたほうが良い。
第6章 手法〜プログラマの道具箱〜
6.1 曳光弾
「曳光弾」とは発光することで、飛んだ軌跡が分かるようになっている弾丸のこと。
プログラミングにおいては優先的に検証したい部分を先行的にプログラミングすることを指し、簡易で構わないので実際の環境下で動作して確認できるエンド・ツー・エンドのソフトウェアを作成する。また、ここで作成するコードはフレームワークであり、土台であり、最終的な成果物にも残る「本物のコード」。
これを作るメリットは以下。
- ユーザーからのフィードバックが得られる
- プログラマが活躍できる舞台を早めに整えられる
- コードに具体化することで早々にプログラマの舞台が整い全員が生産的になることができる。
- デバッグやテストが早く正確にできる
- デモ可能なソフトウェアを確保できる
- 進捗が明確になる
プロトタイプとの違い
- プロトタイプはハリボテの使い捨てのコード、曳光弾は本物のコード
- プロトタイプは真の目的が学習した経験にあり、曳光弾を発射する前の「偵察」「諜報活動」にあたる。
6.2 契約による設計
「契約による設計」においては以下の3つのことを守るようにする。
- 「呼び出す側」は関数の呼び出し前に「事前条件」を満たしているかチェックする。
- 「呼び出される側」(関数)は契約通りの値を返せているかという「事後条件」をチェックする。
- いずれかの契約条項が履行できなかった場合、例外の送出やソフトウェアの終了などの救済策を起動する。
この設計によるメリットは以下。
- 要求されたことの以上も以下も行わない正しいコードを書くことができる。
- コードをシンプルにすることができる。
- 問題の早期発見が容易になる。
対応
- 呼び出し側で外部入力値の妥当性を検証。不正な値ならエラー通知。
- 呼び出される側(関数)で契約通りの値が渡ってきたか、返せるかの事前チェックと事後チェックを行う。
- アサーションを利用する。
- 関数側のアサーションをユーザの入力チェックに使用しない。
- 外部入力値が正しいかということと、関数の引数が正しいかということは別
- 関数側では想定は厳格に、確約は寛容に
アサーション
想定外(矛盾する条件、予想外の状態、不正な値)のことはアサーションを用いてそれを表現する。
アサーションの使用には注意点がある。
- 変数の値を変えてしまうような副作用を持たないようにする。
- リリース版のソフトウェアには組み込まないようにする。
- アサーションは開発と保守で使うもの。
6.3 防御的プログラミング
「こうなるはずだ」と決めつけず、「何が起こるかわからない」と考えてプログラミングをする。例えば、
- 外部ソースからのデータ入力の値を確認する
- ここで確認するのはデータのバグや不正な値
- 関数の入力引数の値を確認する
- ここで確認するのはコードのバグ(エラー処理が適切にできていない)
これらの防御的プログラミングを行うことで開発中には不正なデータを早めに見つけ、デバッグの効率を上げることができる。また運用中には不正なデータに早めに対処することで運用中の問題が大きくなることを防ぐことができる。
対応
バリケードを構築して被害を封じ込める。コードにバリケードを構築するには、特定のインターフェースを「安全地帯への境界」
として使用し、その境界を通過するデータを検証して、不正なデータには適切な措置を執るようにする。
安全地帯の内側にあるモジュール、外側にあるモジュール、間で消毒を行うモジュールの役割配置を決定する。
アサーションとエラー処理の区分け
- バリケードの外側ではデータに対して何かしらの想定をするのは危険。よってバリケードの外側にあるモジュールには「エラー処理」を適用する。
- 一方検証したはずの値が入る関数においては「想定外のエラー」として「アサーション」を適用する。
エラー処理におけるバリデーション
- 無害な値を返す
- 次のデータで代用する
- 前回と同じ答えを返す
- もっとも近い有効値で代用する
- ログに記録する
- エラーを返す
- 呼び出し階層の上位関数がエラーを処理してくれることを願ってエラーが検出されたことを報告するに留める。
- エラー処理の部分とエラー報告の部分をしっかり分けることが重要。
- エラー処理関数を呼び出す
- 共通のエラー処理関数に一元化する。
- この場合、コード全体がこの一元的な機能を使用し、結合することになるので一部を再利用しようと思ってもエラー処理メカニズムを引きずってしまう。
- エラーメッセージを表示する
- 処理を中止する
- 各箇所で最適なエラー処理を選択する
- エラー処理の一貫性がなくなる
- プログラマには大きな自由が与えられる
6.4 ドッグフーディング
リリース前に自らがユーザとなって開発したソフトウェアを使い、不具合やいらない機能を見つけるべきということ。安全性確保や、ユーザに見放されることを防ぐといった効果がある。
6.5 ラバーダッキング(バスタブにあるようなゴム製のアヒル)
発生している問題の解決法がわからないときは「誰か」に説明していると、自ずと解決することがあるということ。この「誰か」はまず無機物であるといいが、どうしても他の人に話したい場合は趣旨を説明し、許可を取ろう。
6.6 コンテキスト
プログラミングにおいてコンテキストを2つの側面からうまく利用しようということ。
- コードの読み書きに利用
- 思考のツールとして利用
対応
- コードを書くときはコンテキストを先に示す。
- これにより読み手は状況判断できて「何についてのコードなのか」を理解することができる。
- コードを設計するときは達人の思考を模倣する。
- 達人の思考とはコンテキストを直感的に鑑み、自分の体験と照らし合わせて持っているパターンの中から状況にあった解決策を施す思考。
- 経験は真似できないが思考方法を真似することはできる。
- コンテキストの存在を認識し、問題のコンテキストを掘り下げ、その状況や目的に応じた判断を下せるよう訓練する。
- コンテキストから切り離された絶対的なルールから距離を置き、直感に重きを置くようにする。
- 作業にあたってはまず、コンテキストを含む全体を大きく見て、構想を練る。そして小さなことを積み重ねる。
達人に対する依頼
プログラミングの達人に作業してもらうときはコンテキストを構築するに足る情報を伝えるだけで構わない。逆にルールを押し付けるとその人のポテンシャルを十分に発揮できなくなる。
コンテキストと物事の理解
人は最初にこの後「今からこういう情報が入る」というヒントをもらうだけで劇的に処理能力が高まる。
これをプログラミングに置き換えると最初にコードにおける見出しやモジュールの名前がわかりやすければ読む側はすぐにコンテキストを構築できる。
第7章 法則〜プログラミングのアンチパターン〜
7.1 ブルックスの法則
スケジュールが遅れているときに人を追加でいれても、逆にさらなる遅延を招くということ。
理由
- 人数が増えると、タスクが分割され依存関係が増えるので調整に時間がかかるようになえる。
- 教育に時間を取られる。
対応
リスケジュールを行い、ユーザと調整しながら機能に優先度を付けて段階的にリリースする。
人を入れ替えても生産性は同じではない
一人ひとりのプログラマには能力に差があり、生産性は同じになりえない。(一方で、荷物を運ぶなどの物理空間の生産性は大して差がない。)例えば以下のような差が生まれる。
- できることの差
- ある人には実装できる機能がある人には実装できないことがある
- バグの差
- 保守コストに大きな影響がある
- 実行速度の差
- できない人が重いコードを書くことでユーザの時間が奪われ、業務効率化という本来の目的を達成できなくなる。
- コードの読みやすさ
- 変更容易性、変更の局所化による影響範囲の制限などに差が出る。
アンチパターン
ソフトウェア開発において成功に致す良質なパターンがある。例えばデザインパターンやエンタープライズアプリケーションアーキテクチャパターンなど。
一方で、悪質なパターンをまとめたものはアンチパターンと呼ばれる。
7.2 コンウェイの法則
ソフトウェアのアーキテクチャ(構造)は組織の構造を反映するということ。
しかし、組織の都合でできたアーキテクチャは本来の目的にとって必ずしも最適な設計になっているとは限らない。
よって、ソフトウェア開発の際にはアーキテクチャの検証を十分に行った後、組織構成に入るのが効率的。
7.3 割れた窓の法則
ソフトウェアにおいて悪い設計やコードがあると、プログラマの中に「残りのコードもこんな感じであろうから自分も適当に作業しよう」という感情が生まれ、ソフトウェア全体がごく短期間で腐敗してしまうこと。
対応
コードを清潔に保つ。そうすれば他の人間もコードを汚す最初の人間になりたくないと感じる。
7.4 エントロピーの法則
コードは自然に任せておくと「無秩序に」増大するということ。
対応
コードが腐る「兆候」を見過ごさないように迅速に対応する。「兆候」には以下のようなものがある。
- 硬さ(ある1か所を変更しようとするとそれに依存する要素を連鎖的に直さなければならないような状態)
- 脆さ(たった1つの変更によって他の多くの部分が壊れてしまうような状態)
- 移植性のなさ(ほかへの環境の移植が難しい状態)
- 扱いにくさ
- コードの扱いにくさ:設計構造に柔軟性がなく、設計構造を維持したまま容易に変更ができない。
- 環境の扱いにくさ:開発環境が遅くて非効率なときに、できるだけ時間がかからない方法で変更しようと設計構造を保持できない変更をしてしまう。
- 複雑さ(将来の変更を見越した無駄な要素が多くある状態)
- 繰り返し(同じようなコードが散見されるような状態)
- 不透明さ(人に伝わらないコードを書いてしまっている状態)
腐敗を許さない
- 初期設計には時間をかけないが、設計を可能な限りクリーンかつシンプルに保ち、できるだけ頻繁にユニットテストや受け入れテストを行う。
- シンプルを常に意識した設計を行う。
7.5 80-10-10の法則
ソフトウェア開発においてユーザの求めることの80%が驚くほど短期間で実現でき、10%は実現は可能だが相当な努力が必要なもので、残り10%は完全に実現が不可能だという法則。つまり、ユーザの要求に100%応えようと思うと開発が立ち往生してしまう。
対応
- ソフトウェア開発において全ての問題に対応する万能薬はないことを認識。
- 適応分野を絞って、いいバランスを狙った高水準なツールを積極的に利用する。
- ただしツールを盲信して使うことは良くない。
80:20の法則
- 障害の8割は2割のコードに集中している。
- 処理にかかる時間の8割は、2割のコードが占めている。
7.6 ジョジョアツリーの法則
名前がないものはその存在を認識できないということ。
対応
チーム内で統一された言語である「ユビキタス言語」を利用して、お互いの意思疎通を簡単にする。
7.7 セカンドシステム症候群
セカンドリリースのソフトウェアが機能過多で使いにくいものになりやすいということ。
対応
- ユーザの定義を明確にする。(誰なのか、何を必要としているか、何が必要と考えているか、何を望んでいるか)
- ユーザからの要望を無秩序に導入せず、他のシステムの組み合わせなどを検討する。
7.8 車輪の再発明
既にあるものを利用せずに1から世の中にある同じ「車輪」を作ること。
自分が再発明したものより世の中に既にあるもののほうが大抵は品質が優れており、工数も無駄にならないので積極的に既にあるものを利用していくのがよい。
車輪を再発明すべきとき
- ビジネス上核となる部分で、そこに致命的な問題が発生するとビジネス上回復できないダメージになるところ。
- ビジネス上差別化したい部分
- 学習目的
7.9 ヤクの毛刈り
ある問題を解こうとしているときに別の問題が発生し、またその問題を解決するために・・・と問題が芋づる式に繋がってしまい、元の問題が何だったのか忘れてしまうこと。
対応
- このような状態に陥ったら立ち止まり、そもそも何が目的だったのか思い出す。
- 事の顛末をメンバーにドキュメントのような形で共有する。
- ヤクの毛刈りをしなければならない状態になったときは問題を書き留めながら、丁寧に、順にひたすら問題を解決していく。