software-design-philosophy について
John Ousterhoutの『A Philosophy of Software Design』をもとに、開発者向けに要点をまとめた設計ガイドです。複雑性の管理を軸に、モジュール、インターフェース、コメント、エラー処理、コードレビューの原則を整理しています。
主な機能
- 01複雑性の症状と原因の特定
- 02シンプルなインターフェースを持つ深いモジュール
- 03情報隠蔽による依存関係の削減
- 04複雑性をモジュール内部に集約
- 05設計原則に基づくコードレビュー
活用シーン
- 01モジュール境界とインターフェースの評価
- 02コードレビューで設計上のリスクを発見
- 03認知負荷を下げるリファクタリングの計画
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ luoling8192/ software-design-philosophy-skill --skill software-design-philosophyインストール後、Claude Code で/software-design-philosophyを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
LICENSEREADME.mdcodex-skill.md詳細
ソフトウェア設計の哲学 — 要点ガイド
出典:John Ousterhout 著、A Philosophy of Software Design 中心的な主張:ソフトウェア設計の核心的な課題は、複雑さを管理することです。
I. 複雑さ:敵を知る
定義
複雑さとは、ソフトウェアシステムの構造に関わるもので、理解や変更を難しくするものです。複雑さはシステムの規模と同じではありません。小規模なシステムでも複雑になり得ますし、適切に設計された大規模なシステムなら管理可能です。
複雑さの3つの兆候
- 変更の波及:一見単純な変更でも、多くの異なる箇所のコードを修正する必要があります。
- 認知負荷:開発者は、タスクを安全に完了するために大量の情報を把握する必要があります。注:コードの行数が少ない = 単純、とは限りません。認知負荷を減らせるなら、コードが多いほうが実際には単純な場合もあります。
- 未知の未知:どのコードを変更する必要があるのか、タスクを完了するのにどの情報が必要なのかが明確ではありません。これは最も危険な兆候です。
2つの根本原因
- 依存関係:コードの一部を単独で理解したり変更したりできません。他のコードと関連しており、それらも考慮する必要があります。
- 不明瞭さ:重要な情報が明白ではありません。曖昧な名前、ドキュメントの不足、暗黙の慣習、隠れた制約などが該当します。
重要な洞察
- 複雑さは段階的に増していきます:たった一度の致命的なミスが原因ではありません。何千もの小さな判断を通じて蓄積します。
- したがって、一切許容しない姿勢を取る必要があります。「些細な」複雑さもすべて重要です。
II. 戦術的プログラミングと戦略的プログラミング
戦術的プログラミング(アンチパターン)
- 目標:機能をできるだけ早く動作させること。
- 姿勢:「とにかく動かす」「リファクタリングは後でやる」。
- 結果:複雑さが急速に蓄積し、技術的負債が制御不能になります。
戦略的プログラミング(推奨)
- 目標:優れた設計を実現すること。動作するコードはその副産物です。
- 姿勢:開発時間のおよそ10~20%を設計の改善に投資します。
- 実践:
- 変更のたびに、設計を改善する機会を探します。
- コードが動作するだけでは不十分です。設計の品質も同じように重要です。
- ソフトウェア開発の単位は機能ではなく、抽象化であるべきです。
III. 深いモジュール:最も重要な設計概念
基本となるたとえ
モジュールを長方形として考えます。
- 幅 = インターフェースの複雑さ
- 高さ/深さ = 内部に隠された機能の量
深いモジュール:単純なインターフェースと豊富な実装。(優れた設計) 浅いモジュール:複雑なインターフェースと、ごくわずかな機能。(悪い設計 🚩)
代表的な例
- 深い:Unix のファイル I/O — わずか5つのシステムコール(open、read、write、lseek、close)で、強力なファイルシステムを利用できます。
- 浅い:Java I/O — ファイルを読むには、FileInputStream、BufferedInputStream、ObjectInputStream などを組み合わせる必要があります。
実践的な原則
- 最も一般的な使い方をできるだけ単純にするインターフェースを設計します。
- 単純な実装より、単純なインターフェースのほうが重要です。
- まれなユースケースでは呼び出し方が複雑でも構いませんが、一般的な使い方にその複雑さを負わせてはいけません。
IV. 情報隠蔽と情報漏洩
情報隠蔽
- 各モジュールは設計上の決定(知識)をカプセル化し、簡素化されたインターフェースだけを公開するべきです。
- 隠す情報には、データ構造、アルゴリズム、低レベルの仕組み、方針上の決定が含まれます。
- 情報隠蔽によって、モジュール間の依存関係を最小限に抑えられます。
情報漏洩 🚩 危険信号
- 同じ設計上の決定が複数のモジュールに反映されているとき、情報が漏洩しています。
- 時間的分解はよくある原因です。情報隠蔽ではなく実行順序に基づいてモジュールを分割すると、各手順が過剰な知識を共有することになります。
情報漏洩の解消
- 共有されている知識を1つのモジュールに統合します。
- 統合できない場合は、共有情報を単一の深いモジュールの背後にまとめます。
V. 汎用モジュールと専用モジュール
核心原則:汎用モジュールは通常、より深いものです。
- 汎用インターフェースは、少ないメソッドでより多くのユースケースをカバーできるため、特化型のインターフェースよりシンプルです。
- 新しいモジュールを設計するときは、次のことを考えてください。現在のニーズを満たせる、最も汎用的なインターフェースは何でしょうか?
判断基準
- インターフェースは、変更を加えずに複数のユースケースをサポートできるだけの汎用性を備える必要があります。
- ただし、実装は現在必要なことだけを行えば十分です(作り込みすぎないでください)。
- 汎用コードと特化型コードは明確に分離する必要があります。
VI. レイヤーごとに異なる抽象化を提供する
原則
- ソフトウェアシステムには複数のレイヤーがあり、各レイヤーは隣接するレイヤーとは異なる抽象化を提供する必要があります。
- 2つのレイヤーの抽象化が似ている場合、レイヤー構成に問題があります。
パススルーメソッド 🚩 危険信号
- 似たシグネチャを持つ別のメソッドに引数を渡す以外、ほとんど何もしないメソッドです。
- これは、レイヤー間で異なる抽象化が提供されていないこと、つまり責務の分割に問題があることを示しています。
パススルー変数 🚩 危険信号
- 変数を使わないレイヤーを通して、上から下まで渡される変数です。
- 解決策:コンテキストオブジェクト、依存性注入、またはモジュール境界の見直し。
VII. 複雑さを下位へ引き受けさせる
核心原則
- 複雑さを避けられない場合、呼び出し側に押し付けるのではなく、モジュールが内部で吸収する必要があります。
- ほとんどのモジュールでは、開発者よりユーザーのほうが多いため、すべてのユーザーに負担をかけるより、開発者が負担を引き受けるほうが望ましいです。
アンチパターン
- 難しい判断を設定パラメーターにして、システム管理者に押し付ける。
- 不確かな状況で例外をスローし、呼び出し側に処理させる。
- これらは短期的には手間を省けますが、システム全体の複雑さを増大させます。
VIII. エラーが存在しないように定義する
核心的な洞察
- 例外処理は、複雑さの最大の発生源の1つです。
- 処理が必要な例外の数を減らすことは、複雑さを軽減する最善の手法の1つです。
戦略
-
エラー状態が発生しないように、意味を再定義する。
- 例:
unset(key)は、キーが存在しなくても成功します。単に「呼び出し後にキーが存在しない」ことを保証します。 - 例:
substring(start, end)は、範囲外のパラメーターを例外にせず自動的に範囲内に切り詰めます。
- 例:
-
例外のマスキング:低レベルで例外を検出して処理し、呼び出し側に到達しないようにします。
-
例外の集約:各呼び出し箇所にハンドラーを散在させるのではなく、複数の例外タイプを一元的に処理します。
重要な区別
- これはエラーを無視するということではありません。エラー状態が単なるエラーではなくなるよう、より優れた意味を設計するということです。
- 正しく報告する必要があるエラー(例:ネットワークパケットの消失)は、引き続き適切に処理する必要があります。
IX. 2通り設計する
- 重要な設計判断では、決定する前に少なくとも2つの異なるアプローチを考えてください。
- 最初の案が素晴らしく思えても、別案を必ず考えてみてください。
- 比較する観点:インターフェースのシンプルさ、汎用性、パフォーマンス、実装の難易度。
- この習慣により、設計の質は大幅に向上します。
X. コメントの哲学
コメントを書く理由
- コメントは、コードでは表現できない設計上の判断と意図を記録します。
- コメントは抽象化の一部です。インターフェースのドキュメントが優れていれば、ユーザーは実装を読む必要がありません。
- コメントを早い段階で書くと、コードに取りかかる前に設計上の問題を明らかにできます。
- 優れたコメントは、認知負荷を大幅に軽減します。
コメントで説明すべきこと
- 自明でない情報:理由、制約、境界条件、副作用など、コードを見ただけでは分からないこと。
- コメントでコードの内容を繰り返してはいけません。🚩 危険信号:コメントがコードを繰り返している
コメントの階層
- インターフェースコメント:何を、なぜ行うのかを説明します。実装の詳細は含めません。
- 実装コメント:どのように、なぜこの方法なのかを説明します。コードがこのように書かれている理由を記します。
- モジュール間コメント:モジュールの境界をまたぐ設計上の判断や依存関係を記録します。
コメントファーストのアプローチ
- 実装を書く前に、インターフェースコメントを書きます。コメントを設計ツールとして活用します。
- 明確なコメントを書けない場合、設計そのものに問題がある可能性があります。
XI. 命名の技法
良い名前の基準
- 正確:曖昧さがなく、読み手に意味を伝えます。
- 一貫性がある:コードベース全体で、同じ名前は常に同じ意味を持ちます。
- 情報量がある:良い名前は、それ自体が簡潔なドキュメントになります。
命名の危険信号 🚩
- 曖昧な名前:汎用的すぎる名前(例:
data、result、tmp、info)は、有用な情報を伝えません。 - 名前を付けにくい:エンティティに正確で直感的な名前を付けるのに苦労する場合、設計そのものに問題がある可能性があります。
XII. 一貫性
- システム全体を通して、同じものは同じ方法で扱います。
- 一貫性が求められるもの:命名、コーディングスタイル、インターフェースパターン、デザインパターン、不変条件。
- 一貫性によって認知負荷が大幅に軽減されます。開発者が一つのパターンを覚えれば、それをどこでも適用できます。
- 「より良い」方法であっても、その改善が十分に大きく、従来の方法を完全に置き換えられるのでなければ、一貫性を崩さないでください。
XIII. コードは一目でわかるべきです
目標
- 読み手が大きな負担なく、コードの動作と意図をすばやく理解できるようにします。
- わかりやすさは、良い設計を測る究極の基準です。
わかりやすいコードにする手法
- 良い命名と一貫性。
- 構造が見えるように、空白や書式を適切に使用する。
- 自明でない点を説明するコメント。
- イベント駆動型コードでは、暗黙的な制御フローを避ける(必要な場合を除く)。
わかりにくいコード 🚩 危険信号
- 読み手がコードの動作や意味を簡単に理解できない場合、十分にわかりやすいとは言えません。
XIV. 危険信号の早見表
コードレビューやセルフレビューでは、次の兆候に注意してください。
| 兆候 | 意味 |
|---|---|
| 浅いモジュール | インターフェースが実装とほぼ同じくらい複雑 |
| 情報漏えい | 同じ設計上の判断が複数のモジュールに反映されている |
| 時間的な分解 | 情報隠蔽ではなく、実行順序によってモジュールが分割されている |
| 過剰な公開 | 共通 API によって、呼び出し側がほとんど使わない機能まで把握する必要がある |
| 単なる中継メソッド | 類似したシグネチャを持つ別のメソッドに、引数を渡すだけのメソッド |
| 重複 | 実質的なコードが複数箇所に重複している |
| 汎用と特殊の混在 | 汎用コードと特定用途のコードが明確に分離されていない |
| 相互依存するメソッド | あるメソッドを理解するのに、別のメソッドも理解する必要がある |
| コードを繰り返すコメント | コメントがコードを英語に置き換えただけになっている |
| 実装によるインターフェースの汚染 | インターフェースのドキュメントに、利用者が知る必要のない実装の詳細が含まれている |
| 曖昧な名前 | 有用な情報を伝えるには汎用的すぎる名前 |
| 名前を決めにくい | 正確で直感的な名前が見つからない。設計に問題がある可能性がある |
| 説明しにくい | 完全に説明するために長いドキュメントが必要。モジュールが複雑すぎる可能性がある |
| わかりにくいコード | コードの動作や意味を簡単に理解できない |
XV. 設計原則のまとめ
- 複雑さは少しずつ増していきます。細部にも注意を払いましょう。
- コードが動くだけでは不十分です。
- システム設計を改善するために、継続的に小さな投資を行いましょう。
- モジュールは深くあるべきです。
- インターフェースは、最も一般的な使い方をできる限り簡単にするべきです。
- シンプルな実装より、シンプルなインターフェースのほうが重要です。
- 汎用モジュールはより深いものになります。
- 汎用コードと特定用途向けコードを分離しましょう。
- レイヤーごとに異なる抽象化を用いるべきです。
- 複雑さを下位へ押し下げましょう。
- エラー(および特殊ケース)が存在しない状態になるよう定義しましょう。
- 2回設計しましょう。
- コメントでは、コードから明らかでないことを説明しましょう。
- ソフトウェアは、書きやすさではなく読みやすさを重視して設計するべきです。
- ソフトウェア開発の単位は、機能ではなく抽象化であるべきです。
使用ガイド
次のような場面で、このガイドを参考にしてください。
- コードレビュー:設計上の問題を見つけるために、「危険信号」のクイックリファレンスを活用します。
- API/インターフェース設計:深いモジュールを目指し、一般的な使い方ではインターフェースがシンプルになるようにします。
- モジュール分割:実行順序や行数ではなく、情報隠蔽に基づいて分割します。
- エラー処理:例外の伝播を減らすため、「エラーを存在しない状態にする」ことを優先します。
- リファクタリング:変更のたびに、その周辺の設計改善へ10~20%の時間を割きます。
- 命名とコメント:名前は正確にし、コメントではコードに表れていないことを説明します。
コメント
Markdown 対応