第1章 前提〜プログラミングの変わらぬ真実〜
1.1 プログラミングに特効薬はない
理由
プログラミングの成果物である「ソフトウェア」は本質的に「困難性」を持つ。
そしてその「困難性」は以下の4つの性質がある。
- 複雑性
- 数千万行のコードも珍しくない。
- 構成要素間の依存関係も規模が大きくなれば非線形に増大
- 同調性
- 複雑な実世界と同調している。
- 可変性
- ユーザーのさらなる要求
- 不可視性
- 概念の集積であり、目に見えない
- 図面にしても全てを表現することは不可能
このように取り巻く状況が複雑で、問題が多岐にわたるため、全てを解決する特効薬は存在し得ない。
戦い方
- 複雑さに対抗する様々な手法や考え方を学ぶ。
1.2 コードは設計書である
意味
ハードウェアでは、まず「設計」をして「設計書」をアウトプット。それをインプットとして物理的に「製造」を行う。
ソフトウェアにおいてそれぞれを置き換えると
- 設計:「上流の基本設計」「詳細設計」「プログラミング」「テスト」「デバッグ」
- 設計書:コード
- 製造:リリースビルド
設計行為の成果物であるコードは設計書。
プログラミングは設計
- プログラミングは創造的な行為で、高度に訓練された優秀なプログラマの仕事
- 設計における作業は全ての人間が携わる。
- コードが設計書ならなるべく早く書き始めないと不明確な部分だらけでいつまでも設計が終わらない。
ロゼッタストーン
将来の保守担当者に対する簡単な手引書
- ビルドとテストのプロセスを実行する方法
- 全体像を表す図
- 設計理由
などを記述しておく。
1.3 コードは必ず変更される
意味
ソフトウェアはリリースされた後、機能の拡張や障害などに対応するため必ず変更される。
またリリースする前でも前日の自分のコードに新しいコードを追加したり、リファクタリングを行ったりする。
対策
そのコードが変更されるという前提で「変更に強いコードを書く」
その際に、「コードが読みやすいか」を意識する。コードは書く時間より読む時間のほうが長いので読む時間を短縮できるようコードの書き方を工夫する。
第2章 原則〜プログラミングのガイドライン〜
2.1 KISS(Keep It Short and Simple)
意味
コードを書くときの最優先の価値を「単純性」「簡潔性」に置く。
構成する各要素がシンプルでそれぞれが担う役割が最小限になっている。
読みやすく、理解がしやすく、修正が容易。
なぜそうする必要があるか?
何も意識せずに修正をしていくと無秩序で複雑なコードになる。そして読みにくく、修正しにくいコードとなる。そしてこのまま突き進んでしまうと誰も理解できず修正のできないコードになる。
対策
- 新しく覚えた技術をむやみに使わない
- 必要になったときに必要なコードしか書かない
- 勝手に要件を加えない
これはコードのみならず、外部仕様にも適用できる。機能が多くて複雑なものは誰も使おうと思わない。
2.2 DRY(Don't repeat yourself.)
意味
同じコードを重複して書いてはだめ。
重複したコードがなぜだめか?
- コードを読む作業が難しくなる
- 量的に「より多く」、質的に「より難しく」なる
- コードを修正する作業が難しくなる
- 複数箇所を正確に修正する必要が生じる(修正漏れの危険性)
対策
コードを「抽象化」することで重複を排除する。そのために処理のまとまりに名前をつけて「関数化」「モジュール化」する。これをすることで以下のメリットが生まれる。
- コードの量が減り、読む量が減る。
- 「名前」がつくのでコードが読みやすくなる。
- 修正箇所が1か所で済み、修正がしやすくなる。
- 抽象化した部分は再利用しやすくなる。
DRYとプログラミング技術
プログラミング技術の多くはコードの重複の排除を目的としたもの。
- 構造化プログラミング
- オブジェクト指向プログラミング
は重複を排除するための手法を含んでいる。
DRYと設計手法
同じような問題について何度も繰り返し解決策を考える「思考の重複」が起きないようにする手法。
例としてデザインパターンがある。これはコードを再利用可能にするためのコード構造パターンを提供する。
OFOP(One Fact in One Piece)
OFOPはデータベース論理設計における、テーブル設計の要でデータベースに格納させるデータの重複を禁じている。「1つの事実は、1つの場所のみに存在させる」ことによりデータの冗長性や不整合を防ぐ。このために正規化を行う。
2.3 YAGNI(You Aren't Going to Need It.)
意味
2.1でも書いたが、コードは必要になったときに必要なものだけを書く
理由
いろいろな事態に備えてコードを盛り込んでも、結局利用されないことがほとんど。また、使われていないコードが盛り込まれていると往々にして難解で保守しにくいコードができあがることがある。
2.4 PIE(Program Intently and Expressively.)
意味
コードを書くときに大切なことは意図を明確に表現するように書く。
コードで意図を明確に表現する理由
ソフトウェア開発においては「要件定義書」「基本設計書」など様々なドキュメントが存在するが、動作を把握するにはコードを読むしかないため、コードで意図を伝えるしかない。
コードは読みやすさが最優先
コードは書く時間よりも読む時間のほうが長いので、読む効率を優先させるべき。よって、「小賢しい」「難しい」書き方をした人が読めないコードは「利口な」コードではない。
コメントは「書く」
本来コメントがなくても分かるようなコードを書くべき。しかし、コミュニケーションが円滑に進むのであればコメントは書くべき。とにかく目的はコードを通して円滑なコミュニケーションを取ること。
2.5 SLAP(Single Level of Abstraction Principle.)
コードのレベルを合わせる
コードを書くときに様々な関数を書く。中には関数の中に関数を含む複合関数もあるが、このときに重要なのが、
「関数を抽象レベルに沿って分割していき、同じ関数に属するコードの抽象レベルを揃えること。」
理由
関数が分割されコードの抽象レベルが統一されていると、関数の一覧が目次のように働き、その関数の中身は小さなコードの塊となって閲覧性が良くなる。
SLAPの手順
優れた文章を書くときを考えると「内容を書くこと」「内容を分かりやすく伝えるための構成を考えること」は別作業。これをコードを書くときに当てはめると「具体的な処理を書く部分」「抽象化レベルを揃える作業」と分けられる。このように別作業として取り組むと作業がやりやすい。
2.6 OCP(Open-Closed Principle)
意味
コードは下の2つの属性を満たすべき。
- コードの振る舞いを拡張できること(Open)
- 拡張しても、その他のコードは全く影響を受けない(Closed)
これが揃っていると既存のコードに全く影響を与えず、機能の拡張ができる。
対策
- コードにインターフェースを用いる
モジュールの設計をする場合、モジュールの使用者であるクライアントが提供者であるサーバを直接呼び出す構造にしてしまうと別のサーバを呼び出すときにクライアントを変更しなければならず、変更に弱い設計になってしまう。
その間にクライアントインターフェイスを置くと新機能のサーバが追加されたとしても、クライアントはサーバの入れ替わりによってコードを変更することはない。
2.7 命名の重要性
意味
コードでは命名は最重要課題である。「名前をつける行為」「名前そのもの」はそれぞれ重要な価値がある。
- 名前をつける行為
- 適切な名前をつけられるということはその要素の役割を正しく理解できているということ
- これができると設計の大部分は完成している。
- 名前そのもの
- 名前はコードを通してコミュニケーションを取るための材料
正しい命名のメリット
- 関数名を見ただけで大体の処理が分かるので、中身を適当に読み飛ばしても良くなる
- 名前によって目的や使用方法が分かるので、呼び出すのが簡単。
プログラマの目的はコードを読んでよく理解した上で、修正したり、機能追加したりすること。コードを読む行為に脳のリソースを多く取られてしまうと考えがまとまらず、先に進めなくなる。
命名の際の注意点
- 「短いコメント」と考えて端的により多くの情報を詰め込めるようにする。
- 誤解されないか自問自答
- 「効果」「目的」を説明
- 正しく名前が使われているかをテストしてチェック
- 「発音可能」「検索可能」
- 標準で使わている用語を用いる。
- 名前から説明文(コードの詳細)が説明できるようにする。
第3章 思想〜プログラミングのイデオロギー〜
3.1 プログラミングセオリー
プログラミングセオリーを支える価値
- コミュニケーション
- シンプル
- 柔軟性
価値を実践に導入するための原則
- 結果の局所化
- 対称性
- 繰り返しの最小化
- 宣言型の表現
- ロジックとデータの一体化
- 変更頻度
以降の節でそれぞれの価値と原則について説明。
3.2 コミュニケーション〜価値1〜
コードは人に見せる「文書」であり、「コミュニケーションツール」。ソフトウェア開発におけるコストの大半は保守にあり、そのコストを節約するにはコードを読みやすくする必要がある。そのために、他の人の視点に立ってコードを書くということが必要。
3.3 シンプル〜価値2〜
コードの「余分な複雑性」は排除するということ。この「余分な複雑性」を排除することによってコードにおける本質的な部分が目立つようになりコミュニケーションが容易になる。一方、シンプルにしすぎてコミュニケーションに支障をきたす場合がある。このときはコミュニケーションを第一優先で少し冗長にする。
3.4 柔軟性〜価値3〜
コードの変更が容易であること。いずれ発生するコードの変更を考えて変更や拡張のしやすいコードを書くこと、そしてその拡張がほかに波及しないように設計をすることが大切。ただし、柔軟性を求めるあまり複雑なコードになると複雑さを増すだけになってしまうので注意が必要。
3.5 結果の局所化〜原則1〜
変更の影響が局所にとどまるようにコードを構成するということ。局所化が進んでいると修正の影響範囲が狭まり、段階的に必要な部分だけを修正すれば良くなる。代表例はモジュール化である。局所化をするために関係性の高いコードを密集させ、低いコード同士が依存しないようなコードにしよう。
3.6 繰り返しの最小化〜原則2〜
同じコードの重複を極力排除することを意味する。重複の排除は結果の局所化にもつながる。そのためにコードをたくさんの「小さい部分」に分割して管理すると良い。コード内でどこが「完全に同じ」でどこが「似ていて」、どこが「全く異なる」部分なのかを明確に伝えられるようにする。
3.7 ロジックとデータの一体化〜原則3〜
「ロジック」「ロジックが操作するデータ」を互いに近くに置くようにすること。なぜならコードを修正するときにロジックと操作対象のデータは同じタイミングで変更になるから。最初からベストな解はわからないので仮配置をして書いていく中で妥当な場所に移し替えると良い。
3.8 対称性〜原則4〜
少し難しいが「グループ内のレベル合わせ」をするということ。関数が分割化され概念が統一化されているとコードがスムーズに頭の中に入る。具体的には
- 「追加」メソッドがあれば「削除」メソッドも生成する。
- あるグループにある関数は全て同じ引数を取る。
- あるモジュール内のデータは全て生存期間が同じ
- ある関数内で、呼び出す関数の抽象度は同じレベルにする。
3.9 宣言型の表現〜原則5〜
コードで意図を伝えようとするときはできるだけ「命令型」より「宣言型」で表現すると良い。宣言型は順序や条件分岐がなく、純然たる事実が宣言的に書かれているためコードが読みやすくなる。一方、命令形は「状態」「制御」「データのフロー」を頭に描かないと事実を正確に理解できない。とはいっても、実際のシステムにおいては命令型のプログラミング言語が多く用いられている。アノテーションなどを用いて宣言型の表現を取り入れると良い。
参考資料:宣言型と命令型
3.10 変更頻度〜原則6〜
コードを修正するタイミングが同じであるということ。同じタイミングで変更されるものは同じ場所に置き、そうでないものは異なる場所に置くと良い。例えば「一般的な計算ロジック」「年ごとに固有なロジック」があるとしてこれらは明らかに変更のタイミングが異なり、「年ごとに固有なロジック」を変更しても「一般的な計算ロジック」の品質は完全に担保される。
単一責任の原則
モジュールを変更する理由が複数あると1つの変更が関係ない箇所の故障につながる脆いモジュールになる。モジュールはある役割に対して「ピュア」であるべき。
3.11 アーキテクチャ根底技法
よいソフトウェア・アーキテクチャ構築のための基礎原理。特定のソフトウェア技術から生み出されたものではない10の根底技法を次の節から紹介していく。
3.12 抽象
抽象とは明確な「線引き」を行うこと。その線引きに従ってあるモジュールをそれ以外のモジュールから明確に区別する。そのために「捨象」「一般化」の2つの観点が大切。
- 「捨象」は複雑な対象のいくつかの性質を捨て去り、特定の性質に目を向けること。
- 会社の人材のデータから誕生日や趣味などを捨て去る
- 「一般化」は具体的な対象から共通の性質を抽出し、より汎用的な概念に定式化すること。
- りんご、バナナ、ぶどうは果物
3.13 カプセル化
関係性の高いデータ群とロジック群をモジュールという膜で包むこと。
これにより以下のメリットがある。
- 関係のない要素が混じらなくなり、コードが見やすくなる。
- 変更の影響がモジュール内にとどまることでコードの変更が容易になる。
- 独立した部品になるため、再利用性が高まり複雑な問題に対処できるようになる。
3.14 情報隠蔽
モジュールが持つデータやロジックを、クライアントから隠蔽するということ。
理由
知る必要のない内部の詳細部分を隠蔽すれば、インターフェースが小さくなり、やりとりがシンプルになることでコード全体の複雑性を下げることができる。クライアント視点でも余計な情報が見えないのでモジュールの使い勝手が良くなる。
3.15 パッケージ化
ある程度ソフトウェアが大規模になってくるとモジュールが多くなるので、そのモジュールをグルーピングする必要が生じる。これをパッケージ化と呼ぶ。これにより依存関係が整理され、モジュールの管理がしやすくなるメリットがある。また、修正に対する影響度がパッケージ内に留まる可能性が高くコードが変更しやすくなる。
3.16 関心の分離
ソフトウェアの機能や目的ごとにコードを集めて独立したモジュールとすること。関心の分離の代表的なパターンは「MVC」。プログラミングにおいては「アスペクト指向プログラミング」が代表的な技術。
メリット
- 関心ごとに修正が入るので読む範囲が限られる。
- 影響範囲が関心内
- 分業して並行して開発を進めることができる。
アスペクト指向プログラミング
関心事の中でも各関心に横串で刺さるような関心をうまく分離する技術。たとえば、ログ機能、データベーストランザクションなどがその対象。これらの対象を結合ルールによって各通常関心に「後から」「編み込む」ことで、コードによる直接呼び出しを回避し、関心を分離する。
3.17 充足性、完全性、プリミティブ性
モジュールの担っている抽象の表現は「充足」「完全」「プリミティブ」を満たすべき。
- 充足性
- 表現しようとしている抽象が伝えるために十分であるか
- コレクションの表現でremoveがあってaddがなければ不十分
- 完全性
- 抽象が全ての特徴を備えているか
- コレクションが要素数を取得するsizeを提供してなければ不完全
- プリミティブ性
- 抽象が全て純粋であるか
- addがあれば、10個追加するadd10などはいらない余分な要素
これらが不足するときの問題点
- 充足性の欠如:情報が不足して、クライアントはモジュールの本質を見失う
- 完全性の欠如:クライアントが安心して利用できない。足りない機能が出ると手詰まりになる。
- プリミティブ性の欠如:クライアントが使用しにくくなる。使用のタイミングや方法が分からなくなる。
対策
モジュールがどのような抽象を表現するか明確にする。
3.18 ポリシーと実装の分離
モジュールは「ポリシー」か「実装」を扱うが、1つのモジュールでその両方を扱ってはいけない。
- ポリシーモジュール:ソフトウェアの前提に依存するビジネスロジックやその他のモジュールに対する引数の選択。
- 実装モジュール:ソフトウェアの前提に依存しない独立したロジック部分。ソフトウェアの前提条件は引数で与えられる。
分離する理由
実装モジュールはソフトウェアに依存しないため、他のソフトウェアでも再利用できる。一方でポリシーモジュールはソフトウェアに特化しているので、ソフトウェアの変更に対して弱い。実装に対してポリシーが混ざってしまうと、ソフトウェアに依存しないはずの実装がソフトウェアの変更に弱くなってしまうので注意。
3.19 インターフェースと実装の分離
モジュールは「インターフェース」パートと「実装」パートの2つの分離した部分から構成する。
- 「インターフェース」パート
- モジュールが持つ機能を定義し、モジュールの使用方法を定める部分
- クライアントによってアクセス可能な関数のシグネチャから構成される。
- 「実装」パート
- モジュールの機能を実現しているコード部分
- 内部で使用するロジックとデータが含まれる。
分離する理由
クライアントは実装の詳細を知る必要がなくなり、モジュールを容易に使用することができる。また、インターフェースと実装の独立性が確保でき、クライアントへの影響を考えることなく実装の修正を行うことができる。
方法
インターフェースを用いたコードを設計する。モジュールの設計原理について「インターフェースに対してプログラミングするのであって、実装に対してプログラミングするのではない」という格言がある。モジュールの実装はインターフェースの背後に隠れるようにして、直接呼び出すことを許可しないようにする。
3.20 参照の一点性
モジュールの要素は宣言され定義されるのは1回限りにする。つまり、初期化したらそれ以降は一切値を変更しない。
理由
ある機能によってモジュールの状態が変化するような副作用のないプログラミングが可能となる。状況依存による障害の発生が抑えられる。
対策
「単一代入をする。」つまり、値の再代入を行わない。
実際の開発において
実際のプログラミングにおいて数多くの変数を管理しなければならない。その中で障害が発生しやすいのは「可変な変数」の部分なので「不変な変数」を増やすようにすると品質が向上する。また、「可変な変数」についてはアクセスする手段を極力減らす。
3.21 分割統治
大きな問題はそのままで解決しようとすると解決困難なので小さな問題に分割して個別に解決すると良い。
3.22 アーキテクチャ非機能要件
機能面以外での全般的な要件のことで以下の観点がある。
- 変更容易性
- 相互運用性
- 効率性
- 信頼性
- テスト容易性
- 再利用性
これらを以降の節で説明する。
セキュリティ
非機能要件の1つに「セキュリティ」がある。情報のセキュリティの正式な定義は「情報の機密性、完全性及び可用性を維持すること。」
- 機密性
- 認可していない個人、エンティティまたはプロセスに対して、情報を使用不能または非公開にする特性
- 完全性
- 資産の正確さおよび完全さを保護する特性
- 可用性
- 認可されたエンティティが要求したときに、アクセス及び使用が可能である特性
ただ過度なセキュリティは利便性を低下させるのでバランスをチェックする。
3.23 変更容易性〜非機能要件1〜
ソフトウェアがどれだけ容易に改善できるかを指す。4つの指標がある。
- 保守性
- 障害が発生したときのコードの修正のしやすさ
- 拡張性
- 新しい機能の追加、モジュールの新しいバージョンへの置き換え、不要な機能やモジュールの除去のしやすさ
- 再構築
- モジュール間の関係の再組織化の行いやすさ
- 移植性
- 様々なハードウェアプラットフォームやプログラミング言語への適合させやすさ
3.24 相互運用性〜非機能要件2〜
他のソフトウェアとの連携がどれだけしやすいかということ。接続性が高いことはソフトウェア用途の選択肢を大きく広げる。
やり方
プロトコルやデータ形式の選定は業界の標準的な規格を選択。すでにあるソフトウェアに合わせる形にする場合は、接続使用をよく確認し、仲介するソフトウェアの導入を検討する。
3.25 効率性〜非機能要件3〜
時間やコンピュータ資源といった観点からリソースの使用効率を考える必要がある。
やり方
アーキテクチャ設計段階でモジュールへの責務分散とモジュール間の結合を適切に行う。
関接化と効率性
間接化とはモジュール間の直接的な結合を避けるため「媒介モジュール」を導入することを指す。これにより疎結合性を維持し、高い保守性や拡張性、再利用性が確保できる一方で、処理そのものが冗長になるためバランス調整が必要。
信頼性〜非機能要件4〜
予期しない方法や不正な入力などで例外的な場面に出くわしても機能を維持する能力。2つの側面がある。
- フォールトトレランス
- ソフトウェアで障害が発生したときに正常な動作を保ち続ける能力。
- 例外の発生し対して正しいふるまいを保証し、内部的には修復を行う。
- ロバストネス
- 不正な使用方法や入力ミスから、ソフトウェアを保護する能力。
- 様々な使用方法に対して、システムとして定義された状態に移行する。
対策
- フォールトトレランス
- 二重化などの内部的な冗長性をもたせる
- 障害時は提供する機能を絞り込み大事な機能のみを提供して処理継続を優先する設計も考慮
- ロバストネス
- 障害時にその部分を切り離す設計を検討。
- 障害が発生しないようにユーザが誤った操作を行っても安全に可動させる設計も考慮。
テスト容易性〜非機能要件5〜
テストが「効果的」かつ「効率的に」テストを行う能力。
「効果的」とは、テストによってソフトウェアの隅々まで漏らさずその品質を保証できること。
「効率的」とは、テストのコストや労力が少ないこと。
対策
アーキテクチャ策定の時点から、検証方法の観点も含めた設計を行う。テストのためのコードが本番にあっても良い。それだけテストは大切。モジュール間の依存関係を排除して小さい単位でテストができるようにする。
3.28 再利用性
ソフトウェアの全体でも一部でも、別のソフトウェアの開発に再利用する能力のこと。以下の2つの側面がある。
- 再利用するソフトウェア開発
- 既存のモジュールや以前のプロジェクトのモジュールなどを再利用して開発を行う。
- 再利用のためのソフトウェア開発
- 将来のプロジェクトで再利用できるモジュールを、現在のソフトウェア開発で行う。
やり方
再利用するソフトウェア開発では、アーキテクチャの構成を既存の構造やモジュールに「プラグイン」できるようにする。
再利用のためのソフトウェア開発では、開発中のソフトウェアから自己充足的な部分を取り出すことのできるアーキテクチャにする。自己充足部分は変更を加えることなく他のシステムで再利用できることが求められる。
再利用の「3の法則」
- 難易度三倍の法則
- 再利用可能なモジュールは特定のソフトウェアのモジュール開発より3倍難しい
- 一般化した問題とは何か?それらのもんだいをどう処理するかを考えなければならない
- テスト3種類の法則
- 再利用可能なモジュールは共有化する前に3つの異なるソフトウェアでテストする必要がある
3.29 7つの設計原理
障害を作り込まないために考慮すべき、コード構造上の7つの核心観点。以降の節で詳しく説明していく。
コードレビュー
ソフトウェアの品質を担保する方法として、コードレビューがかなり有効。しかし、コードレビューは人が見るため一定の価値観や観点がないと指摘が散漫になる。このときに7つの設計原理のような核心的なチェック観点が必要。
3.30 単純原理〜7つの設計原理1「シンプルにこだわる」〜
ソフトウェアの障害はもれなく複雑なところにある。そのため障害の余地がないほどシンプルなコードを書く。
3.31 同型原理〜7つの設計原理2「形にこだわる」〜
同じことは同じように表現する。例えばあるモジュールの数値の単位や公開関数の引数の数を統一する。
これをすると逆に「違うもの」が目立つようになり、障害を見つけやすくなる。
3.32 対称原理〜7つの設計原理3「形の対称性にこだわる」〜
ある処理についてその対称となるものを考えること。
対称性があることで予測が付きやすくコードの理解が早まり、また設計時に考慮漏れを防ぐことができる。
対応
「set/get」「start/stop」などの対を考える。また、例外的な状況を考えてなるべくそれを排除する。
3.33 階層原理〜7つの設計原理4「構造が階層であることにこだわる」〜
物事の主従関係や前後関係を常に意識し、整理された関係性を構築する。
明確に表現された階層構造があると全体を抽象化して理解し、必要に応じて階層を降りて詳細な把握をすることができる。
対応
抽象レベルを意識して階層構造を構築。上位から見たときは、下位レベルは外側から見ているような視点で記述すると良い。
3.34 線形原理〜7つの設計原理5「処理の流れは直線であることにこだわる」〜
ある機能はいくつかの機能の重ね合わせ(線型結合)でできているのが良い。
複雑な条件文や繰り返し文、GOTO文などで障害は多発する。処理の分岐を少なくし、流れが直線的に読めるコードを書く。そのために特殊な振る舞いを主処理に混ぜて書かないようにし、複雑なコードになっていないか俯瞰する。保守していくうちに複雑になりすぎたものについては再構築も検討する。
3.35 明証原理〜7つの設計原理6「ロジックの明証性にこだわる」〜
一見して明らかに正しいと言えるコードを書くこと。コードで不十分な場合はコメントやドキュメントで正しさの証明を行うこと。
コードの不確実性を取り除き、直感的で分かりやすいロジックを書くこと。人が読むことを強く意識する。
3.36 安全原理〜7つの設計原理7「安全性にこだわる」〜
ありえない条件をあえて考慮してコードを書く。例えば、あるif文に対してありえないと思ってもelse文は書く。要件や機能を理解し、場合分けを正しくコードに表現できるとソフトウェアが安全に動作する確率が高くなる。そのため、ありえない条件でも、あえて考察の対象として設計・プログラミングしておく。人によってブレが出る可能性があるので、ある程度は規約として書いておく。
3.37 UNIX思想
UNIX文化の中で生まれた優れたプログラミングを行うための経験に基づいた実践的な「技」の集合。以降の節で17個の原則について説明する。
UNIXの設計判断の正しさ
UNIXは1969年に生まれた技術で、情報技術の進歩の速さを考えると驚異的な実績。歴史の風雪に耐えた実績があるので、UNIX思想の各原則を設計の指針として利用するとよい。
3.38 モジュール化の原則〜UNIX思想1〜
コードの中で関連性の高い要素を集めて、できるだけシンプルなインターフェースを持ったモジュールを作成する。
3.39 明確性の原則〜UNIX思想2〜
コードを読むのはマシンではなく人なので、分かりやすく明確なコードを書く。
対策
コードを読むときに、3回以上繰り返して読まないと解読できないコードは書かない。
3.40 組み立て部品の原則〜UNIX思想3〜
他のソフトウェアと簡単に連携することができるソフトウェアを作る。そのために単純なインターフェースとなるテキストストリームを読み書きする、コマンドラインで使用できるソフトウェアを設計する。
3.41 分離の原則〜UNIX思想4〜
メカニズムからポリシーを離す。
- ポリシー:ソフトウェアの前提に依存する部分(ビジネスロジックやユーザーインターフェースなどの不安定な部分)
- メカニズム:前提に依存しない独立した部分(エンジン的な役割を果たす安定した部分)
意味
これにより、メカニズムとポリシーそれぞれにおいて互いに影響を与えることなく単体でのテストが可能になる。
対応
サービス系アプリケーションの場合、クライアントからの要求を受け付けるフロントエンド(ポリシー)と実際のサービスを行うバックエンド(メカニズム)でモジュールを分割する。
3.42 単純性の原則〜UNIX思想5〜
コードが単純になるように設計。複雑化するコードや無駄な機能はできるだけ拒否。
3.43 倹約の原則〜UNIX思想6〜
分量と複雑度が「大きい」コードは書かないようにする。大きくなったら分割する。
3.44 透明性の原則〜UNIX思想7〜
ソフトウェアの動作を外から見えるように設計する。そのために以下の2つの観点がある。
- 透明性:ソフトウェアの動作について、ひと目見てすぐに「何をどのようにしているのか」が理解できること。
- 開示性:ソフトウェアの内部状態について、監視ないし表示できること。
デバッグは開発基幹の大きな部分を締めるので、早い段階でデバッグを簡単にする仕組みは大きな投資になる。
対応
デバッグのための機能を設計の最初の段階から組み込む。例えば、モジュール内変数の内容を書式文字列化するなど。またデバッグを簡単にする仕組みを本番コードに組み込むとよい。
3.45 安定性の原則〜UNIX思想8〜
通常の条件下のみならず予想外の条件下でも適切に動作すること。そのために以下の2つの観点が重要。
- 透明性:コード内で何が起きているのかすぐに分かるコード
- 単純性:複雑でなく、すべての分岐条件を難なく説明できるコード
対応
- コードを書いたプログラマがコードの内部構造について正しく説明できること。
- 手入力だけでなく他のソフトウェアから与えられる入力を使用してストレスを与える。
3.46 表現性の原則〜UNIX思想9〜
コードにおける情報の表現方法は、ロジックでなくデータに寄せて書くようにする。
これはデータ構造より手続きロジックのほうが複雑だからで、コード上の避けられない複雑さはデータ側に寄せておいたほうが良い。
3.47 驚き最小の原則〜UNIX思想10〜
インターフェースはそれを使う人が想像するであろう形に設計する。
これはユーザーの学習コストが低く、思ったとおりに動作するソフトウェアが優れたソフトウェアであるから。
対応
- よく似たソフトウェアのインターフェースをモデルにする。(例:足し算といえば「+」)
- 想定ユーザの特徴を考慮する。(エンドユーザ?プログラマ?システム管理者?)
- UNIX世界の伝統に注意を払う
- 一見似ているが微妙に異なる表現を同じにしない(例:モジュールからの値の取得は「get」、リモートからの取得は「fetch」)
3.48 沈黙の原則〜UNIX思想11〜
ソフトウェアは表示を最小限に抑えるべきということ。
これは出力が多いとユーザは何が重要な情報か分かりづらくなるのと、他のソフトウェアとの接続性が悪くなるから。
対応
表示出力を最小限に抑え、内部状態を混合させない。
- 本当のエラーだけを標準エラー出力に表示して、その他の要求されていないデータはいっさい出力しないようにする。
- デバッグの目的で、進行状況についてメッセージを表示したい場合は冗長モードのスイッチを作り、デフォルトではそれを無効にしておく。
3.49 修復の原則〜UNIX思想12〜
エラーの回復に失敗したときは処理の継続を直ちに止めろということ。
予期せぬ状況が発生したときにうまく対応して修復できればいいが、修復ができなかった場合ユーザのデータなどを破壊してしまう恐れがあるため。
対応
エラーが発生したときはできるだけ早い段階でエラー通知を強く行う。ただし、ユーザの誤った入力やソフトウェア自身の実行エラーはまず穏便に処理するべきで、それができないときは簡単に障害を診断できるエラーを起こす。
ソフトウェアの入力と出力
- 入力:誤った形式でも観葉に受け入れる。
- 出力:厳密にクリーンで正確なデータを送る。
3.50 経済性の原則〜UNIX思想13〜
プログラマの時間を節約するべきということ。また、そのために必要な設備への投資を惜しまないということ。
これは設備を導入するコストよりプログラマの働く時間に対するコストのほうが大きいので設備投資によってプログラマが効率よく仕事できれば簡単に投資は回収できるため。
対応
貧弱なハードウェア、必要なツールや情報への無駄なアクセス制限をやめ、環境開発のためのソフトウェアやハードウェアに投資する。
3.51 生成の原則〜UNIX思想14〜
手作業を避け、コードを書くためのコードを書くということ。
これはコードに対する手動の単純作業は遅れやミスの原因になるため。
対応
コードジェネレータを作り、繰り返しが多く定型的なコードを生成できるようにする。
3.52 最適化の原則〜UNIX思想15〜
コードの最適化を行う前に正しく動作するコードを書くべきだということ。
これは正しく動作する前に最適化を行ってしまうと、
- コードが無理なものになったり、内部構造がわかりにくくなることでデバッグにかかる時間が増えてしまう。
- 半端な最適化をすると全体的な最適化の妨げになる。
といった問題が生じるから。
対応
前提として正しくしてから速くする。動くプロトタイプを作って最適化を行う。
3.53 多様性の原則〜UNIX思想16〜
ソフトウェアの開発において「正しい唯一の方法がある」とは思わず、多様な選択肢を受容するべきということ。
対応
たった1回のリリースで全ての要望に答えることはできないので、オープンで拡張性の高いソフトウェアにして相互運用性の向上やカスタマイズ性の高さを確保する。
3.54 拡張性の原則〜UNIX思想17〜
あらゆる拡張性を考慮した設計を行うということ。「唯一の正しい方法はない。」
対応
拡張のための接続部を柔軟なものにし、明示する。コードの中に「〜が必要になったら」と記述しておく。
3.55 UNIX哲学
UNIXのは以後にある設計の哲学のことで「UNIXという考え方」。9個の定理を紹介していく。
3.56 小は美なり〜UNIX哲学1〜
ソフトウェアは小さく作り、小さく保つと価値が高いということ。
理由
- 理解が容易
- 保守が容易
- マシンリソースに負担をかけない
- 他のソフトウェアと組み合わせやすい
一方で大きなソフトウェアは以下のデメリットがある。
- 複雑で、コードの理解が困難
- 不測の事態に対応できない
対応
小さく設計し、1つの仕事に専念させる。足りなければ他のソフトウェアと連携すれば良い。
そのために解決する問題をきちんと理解することが大切。
3.57 1つ1仕事〜UNIX哲学2〜
1つのソフトウェアには1つのことをうまくやらせる。(例:lsはファイル一覧を表示するだけで、表示整形の機能はいらない)
対応
問題が大きくなったときは、問題を小さく分割し、小さな問題に対応するソフトウェアを作る。
3.58 即行プロトタイプ〜UNIX哲学3〜
できるだけ早くプロトタイプを作れということ。
これはよいソフトウェアが度重なる修正を経てできてくるもので、試作以前の構想は憶測の域を出ず合意が取りづらいため。
メリット
- 前提の誤りを早期に発見できる。
- 設計の方針が正しいのかどうかを早期に判断
- 要件不備による手戻りを減らせる。
- プロトタイプをユーザーに見せて悪い点などを指摘していただくこと早期に修正可能
- 早いうちから誤りを取り除く作業を始められる。
- 「計算不能なアルゴリズム」などの問題をふるいにかけ、試行錯誤しながら品質を高められる。
目指すべきシステム
「性能」と「機能」の最適なバランスが模索され、ほんとに必要な機能だけが残され、適正なリソースで多くのことを達成できるようになるシステム
そして、これは性能が高く機能が欠けているシステム、機能が多くなり性能が犠牲になっているシステムを作っていく過程でできるシステム。
3.59 効率性より移植性〜UNIX哲学4〜
開発の効率性より移植性を優先せよということ。
移植性:他のプラットフォームに合わせてコードを書き換えるときに、コストが少なく済むようなソフトウェアの能力。
理由
まずハードウェアと切り離すことのできないソフトウェアは、ハードウェアの優位が失せるとソフトウェアの価値も下落してしまう。優位性のあるハードウェアに移植しようと思ったときに無駄な時間を取られることはビジネスにとって致命傷になりかねない。だから開発効率を下げてでも、移植性を担保すべき。
対応
ハードウェアに依存する部分とそうでない部分を切り離した設計を行う。また依存しない部分は再利用しやすい単位でモジュール化する。また、コードの最適化に時間を欠けすぎないようにすることも大事。これは最適化を突き詰めると使っているハードウェアに特化したコードになり移植性の妨げになるため。
3.60 データはテキスト〜UNIX哲学5〜
バイナリ形式のデータファイル設計は採用せず、テキストファイルに保存するということ。
これはデータが移動や交換を繰り返すため移植性の高い「テキストファイル」が優れているため。
メリット
- 最も一般的で、移植性が高い形式。
- 人間がデータを直ちに確認できる。
- 扱いやすい形式で、バイナリ形式のように別のバイナリ形式への変換を気にする必要がない。
対応
テキストファイルを入出力するよう設計して、テキストファイルの中でも標準的なCSVやXMLを選択する。
3.61 レバレッジ・ソフトウェア〜UNIX哲学6〜
他のモジュールや設定ファイル、ソフトウェアそのものを自分のソフトウェアで使うことで、ソフトウェアの性能を高めよということ。
自分で書く量が少なければそれだけ開発効率が上がる。
対応
大きなソフトウェアを書くのではなく、それぞれが単機能の小さなソフトウェアを作り、それを組み合わせることで大きな仕事を成し遂げる。
また惰性で手作業でやっていることをコンピュータに任せて自動化する。
3.62 シェルスクリプト活用〜UNIX哲学7〜
小さなソフトウェアをシェルスクリプトを用いてつなげることで大きな仕事を成し遂げよということ。
メリット
- インタプリタでプラットフォーム専用のバイナリにコンパイルする必要がなく、移植性が高い。
- 簡易な制御構造を利用してコマンドやソフトウェア同士を結合し、流れ作業や定型処理を実現できる。
3.63 対話インターフェース回避〜UNIX哲学8〜
ソフトウェア同士の会話は人間ではなくソフトウェア中心で考え、人間との対話を極力減らそうということ。
対話的インターフェースはコマンドインタプリンタの上位に立ち、ユーザと対話するスタイルのインターフェースで以下の問題が発生する。
- ソフトウェアごとの独自の対話方法を覚えなければならない
- ソフトウェア同士が対話できなくなる
- 待ち時間が長くなる
- 入力部分の解析コードが大きく、醜くなる
- 「大は美なり」的なアプローチになる
- 機能選択の「メニュー」
対策
対話型インターフェースは初心者向けのもので、ソフトウェア同士の会話は基本的にはソフトウェアを中心に考える。
3.64 フィルタ化〜UNIX哲学9〜
ソフトウェアはフィルタとして設計するべきだということ。
フィルタとは入力ストリームをデータとして受け取り、何らかの加工を施し、加工したデータを出力ストリームに送り出すこと。
対応
データ入力には標準入力を使用し、出力には標準出力を用いる。またエラー情報には標準エラー出力を用いる。
3.65 UNIX哲学が持つその他の小定理
- 環境カスタマイズ
- ユーザが好みに応じて自分で環境を調整できるようにする。
- 軽薄短小カーネル
- UNIXはOSのカーネルの肥大化を拒絶する。
- OSのカーネルにアプリケーションを組み込むとコンテキストスイッチが減らせる。
- 一方で、カーネルのプログラマしか保守できないようになる問題が生まれる。
- 小文字使用
- 命名は小文字を使い、かつ簡潔にする。
- 森林保護
- 紙のファイルにあるデータは「操作」できない。
- すべてのテキストファイルをコンピュータに保存する。
- 沈黙は金
- 極力表示出力を抑えるようにする。
- 無意味なエラーメッセージ(ファイルが見つかりません)などで必要な情報が埋もれないようにする。(UNIXではlsでファイルがなくても何も表示されない。)
- 並列思考
- CPUをできるだけ忙しく働かせる。
- そのために仕事を小さく分割して、それらを同時に実行する。
- 部品コラボレーション
- 大規模な単体型のソフトウェアより小さな部品を集めたソフトウェアを作るべき。
- 柔軟性に優れ、保守がしやすく、実行時の必要リソースも小さく抑えられ得る。
- 90パーセント解
- 対象ユーザーの90%が満足する解を目指す。
- 残りの10%は自分でどうにかしてもらおうとするスタンス。
- 10%の問題とは問題の中でもコストや時間がかかったり、プログラミングしにくい部分。
- 劣るが優る
- 高品質ではないが効率的なシステムが生き残るという考え方。
- 階層指向
- 物事を階層的に考える。(例:ファイルシステムにおけるディレクトリ構造)