Code Complete
基礎
ソフトウェアコンストラクションとは
- コンストラクションとはプログラミングのこと
- コーディング+デバッグ+ α(詳細設計やユニットテストなど)からなる
- 「コーディング」という言葉単体は、単なる機械的な作業を連想させるため、コンストラクションの説明としては適切ではない
- コンストラクション以外の作業は?
- プロジェクトマネジメント
- 要求開発(要件定義)
- 概要設計(アーキテクチャ)
- UI 設計
- システムテスト
- 保守
なぜコンストラクションが重要か
- 開発時間の大部分を占めるため
- 開発の中心であるため
- コンストラクションの改善は驚くべき効果をもたらすため
- コードが唯一のドキュメントになってしまうことがよくあるため
- どれだけ急いでいても絶対に省略できない工程であるため
メタファにより開発への理解を深める
メタファの重要性
- メタファを使った理解を「モデリング」という
- よく理解できないものを、既に理解しているものと照らし合わせることで、理解が深まること
- 概念全体を把握しやすくなる
メタファの使用方法
メタファはヒューリスティクスの意味合いが強い。どうすれば物事がうまくいくか、考えるために使うものである。
- アルゴリズム
- 厳密に定義された一連の命令
- ヒューリスティクス(発見的)
- 答えを見つけるために役立つテクニック
システム構築のメタファ
- BAD) コードを書く
- 手紙を書くイメージ
- 書いて終わりじゃねーのよ
- BAD) システムを育てる
- 作物を育てるイメージ
- コードはすくすく育たないし収穫もできないのよ
- SOSO) システムをインクリメンタルに開発する
- 真珠養殖のイメージ
- GOOD) システムを構築する
- 建築のイメージ
- 現代のシステム開発における用語の多くは建築由来。e.g. アーキテクチャ、ビルド、etc
話は逸れるけど、知識を常に収集してストックしておく「知的道具箱」というメタファもいいね
上流工程
準備の重要性
- 品質向上したいタイミングが
- プロジェクトの終わりの場合
- システムテストを強化する
- ただし、どれだけテストをしたところで、もとがクソなら意味がない
- プロジェクトの途中の場合
- コンストラクションのプラクティスを強化する
- プロジェクトの最初の場合
- 高品質な計画、要求、設計を行う
- 安い車として設計したものは、どれだけ後段で努力してもロールスロイスにはならないからね
- プロジェクトの終わりの場合
準備の最大の目標
リスクを減らすこと。
準備不足の原因
- 上流担当開発者が仕事をこなすだけの知識を持っていないから
- すぐにコードを書きたい衝動を押さえられない開発者がいるから
- 準備にかける時間を上司がよく思わないから。対処法:
- はっきり断る
- コーディングしているふりをする
- 準備を怠る危険を上司に叩き込む
- 転職する
準備に文句を言わせないためには
- 論理で訴える
- ユーザが求めていないものを作ると目も当てられないよ
- 変な構築をすると必要のないものに膨大な時間と資金が消えていくよ
- 例えで訴える
- プランクトンがぴょう期ならいずれクジラも死ぬ、みたいな食物連鎖の例とか
- データで訴える
- 問題の修正時期があとになるほど、コストは指数的に増えるよ
- これはウォーターフォール・アジャイルを問わない
ソフトウェアの種類
- ゆるいシステム
- アジャイル開発
- 反復型 の開発手法が適している(以下を交互に繰返し行う)
- 計画、要件定義、アーキテクチャの策定
- コンストラクション、システムテスト、品質保証
- ざっくりとした要求仕様の策定
- 設計とコーディングを同時にやってしまう
- 別チームでのテストや QA はなし
- かたいシステム
- ウォーターフォール開発
- 逐次的 な開発手法が適している
- ちゃんとした要求仕様の策定
- アーキテクチャの設計とレビュー
- 詳細設計とレビュー
- 別チームでのテスト
- QA
反復型手法
コストは以下の順に安くなる
- 準備なしの逐次型(高い)
- 準備なしの反復型
- 準備ありの逐次型
- 準備ありの反復型(安い)
どんなプロジェクトでも「最も重要な要求とアーキテクチャの要素を早期に洗い出すこと」が大事。目安として:
- 逐次型なら事前に 80%の要求を明らかにしておく
- 反復型なら事前に 20%の要求を明らかにしておく
反復型 or 逐次型
- 逐次型に向いているもの
- 要求が安定している
- 設計が単純で理解しやすい
- メンバーが分野に精通している
- プロジェクトのリスクが低い
- 長期的な予測が重要である
- 下流での変更が高く付く
- 反復型に向いているもの
- 要求が不透明
- 設計が複雑で理解しにくい、手間がかかる
- メンバーが分野に明るくない
- プロジェクトのリスクが高い
- 長期的な予測が重要でない
- 下流での変更は安価である
以下、プロジェクトに適したコンストラクションの準備とは何かを考えてみる
準備:課題定義
Product vision statement / Mission statement / 課題定義
- システムが解決する課題が何であるかを定義するもの
- ソリューションについては一切言及しない
- ユーザの言葉で書く。コンピュータ用語は使わない。
- 以下を防ぐためのもの
- 誤った課題を解決しようとして時間を無駄にすること
- 本来の課題が解決されないこと
準備:要求
要求 / 要求開発 / 要求分析 / 要求定義 / 仕様 / 機能仕様 / スペック とは:
- システムが何をすべきかを定義するもの
要求が必要な理由
- システムの機能をユーザ主導(not プログラマ)で決定するのに役立つ
- プログラマ同士の議論を減らせる
- 手戻りを抑制できる
要求は変わる
- 顧客は、開発の過程を通じて自分自身のニーズを理解していくものだから
- 平均して当初の仕様の 25% は変更される
要求変更への対処方法
- 要求がきちんと定義されているかチェックリストで確認する
- コストを関係者全員に認識させる
- 変更の管理手順を予め定めておく
- 変更に対応できる開発手法をとる
- プロジェクトを中止する
- その変更が本当にビジネスに価値をもたらすのか考えさせる
アーキテクチャ
- 最上位レベルの設計、概略設計のこと
- 最終的なシステムの品質を決定する
- 後で変更すると膨大なコストがかかる
アーキテクチャの構成要素
- 概要
- 全体構成をざっと説明するもの
- どんなパーツがあるのか列挙
- 各パーツの役割は1つに絞る
- パーツ間のやり取り規則は明確にする
- 主要なクラス
- システムの8割を司る2割のクラスを明記する感じ
- データ設計
- DB の大まかな構造と内容
- 業務ルール
- 例えば顧客情報は 30 日より古くなってはならない、などあれば
- UI 設計
- リソース管理
- セキュリティ
- パフォーマンス
- スケーラビリティ
- i18n, l10n
- エラー処理
- メッセージ規約など
- フォールトトレランス
- オーバーエンジニアリング
- どれくらい堅牢にすべきか
よいアーキテクチャとは
- 採用した理由、採用しなかった理由が明記されている
- 途中変更する場合は全体と調和するようにする
- 目的が明確である(パフォーマンス重視、柔軟な変更可能性重視、など)
- マシンや言語に依存していない
- 過不足がなく丁度いい
- リスクが明記されている
- 複数の角度からの見解が盛り込まれている
- 不安要素がない
上流工程にかける時間
スケジュール全体の2割から3割
コンストラクションの重要な決断
言語
- 使い慣れた言語だと生産性が高まる
- 高級言語の生産性は低級言語の 5 から 15 倍
- 考える能力は言葉(≒ プログラミング言語)を知っているかどうかで決まる
設計
設計の難しさ
設計の難しさは、設計が:
- wicked problem であるため
- wicked problem = やっかいな問題
- やってみて初めて気がつく問題が内包されていること
- ルーズなプロセスであるため
- 良い方法と悪い方法の違いがわずかであり、間違えやすい
- どれだけやれば十分かわからない、よくあるのは「時間がなくなったら」
- 妥協と優先順位付けの産物であるため
- 制限がつきものであるため
- 非決定論的であるため
- 無限にやり方がある
- ヒューリスティックなプロセスであるため
- 発見的、試行錯誤的
- 試してみないとわからない
- 創発的であるため
- 常に動き続け、レビュー、話し合い、経験を通じて改善されていくもの
重要な設計概念
鉄則:複雑さへの対応
- essential(本質的)問題と accidental(偶発的・付随的)問題
- なぜ複雑さへの対応が必要か?
- 人間の頭はプログラム全体を理解することはできないから
- 複雑な問題を 単純な問題に分割する ことが必要
- 方針
- 一度に対処しなければならない本質的な複雑さを最小限に抑える
- 偶発的な複雑さを必要以上に増やさない
高品質な設計とは
- 最小限の複雑さ 凝った設計にするな
- 保守性
- 疎結合
- 拡張性
- 再利用性
- 高いファンイン
- あるクラスが、他のたくさんのクラスで使われている
- 低いファンアウト
- 1 つのクラスが使用する他のクラスが少ない
- 移植性
- 無駄がない 足すものがなく削るものもない
- 階層化
- 汚いコードを覆うインターフェース層を追加する、など
- 標準化
- 独自のフレームワークをつくるより、一般的なデザインパターンを採用する、など
設計のレベル
- Lv1 ソフトウェア
- Lv2 サブシステム・パッケージ
- Lv3 クラス
- Lv4 ルーチン
- Lv5 ルーチン内部
Lv1 ソフトウェア
システム全体のこと。
Lv2 サブシステム・パッケージに分割
- 大きな単位でシステムを分割する よくあるサブシステムの分け方:
- 業務ルール 源泉徴収額の計算、など
- ユーザーインターフェース 画面描写
- DB アクセス
- システムへの依存部分 windows 用コード、Mac 用コードなど
- サブシステム間のやり取りを可能な限り少なくする => 複雑さへの対処
- ベスト サブシステムが別のサブシステムのルーチンを呼ぶ
- まあまあ サブシステムが別のサブシステムのクラスを含む
- だめ サブシステムが別のサブシステムのクラスを継承する
Lv3 クラスに分割
- パッケージ内で、機能をクラスに分割し、クラスを設計する
- クラスのインターフェース(パブリックなルーチン)も検討する
Lv4 ルーチンに分割
- 各クラスをルーチン(プライベートなルーチンを含む)に分割し、ルーチンを設計する
- このレベルで検討した結果、Lv3 に戻ってインターフェースを変更するのもアリ
- 担当エンジニアの頭の中で行われることが多い
Lv5 ルーチンの内部設計
- ルーチンの設計を行う
- 担当エンジニアの頭の中で行われることが多い
構成要素の設計:ヒューリスティクス
設計には、必ず正しい答えがあるわけではなく、常に発見的・試行錯誤的である。 このため「完璧ではないものの、概ね良い答えが出るであろう方法(≒ 経験則)」で設計は行われる。 この方法のことをヒューリスティクスと呼ぶ。
以下、いくつかのヒューリスティクスを説明する。
現実の世界をオブジェクトにする
- 属性を決める
- メソッドを決める
- 包含や継承の関係を決める
- 属性・メソッドのパブリック・プライベートの別を決める
一貫した抽象化を行う
- 抽象化とは、詳細にこだわらず概念だけを表すこと
- 複雑な部分を無視して簡単にすることで、頭の中で整理できるようにする
- 人間は、集合を扱う場合は必ず抽象化を行う
- 「ガラス・木・釘の組み合わせ」=「家」など
- 抽象化のレベルを揃えること
- ルーチンのインターフェース = ドアノブ
- クラスのインターフェース = ドア
- パッケージのインターフェース = 家
- 極端に細かすぎる or 大きすぎる単位で抽象化しない。頭の中で一度に理解できないから。
- 木の繊維、鉄の分子レベル
実装の詳細をカプセル化する
- カプセル化=複雑な詳細を、隠す
- 抽象化=複雑な詳細を、単純に見せる
継承する
可能かつ最適な場合は、継承を使うと良い。 派遣社員と正社員は、どちらも「社員」の属性と振る舞いを継承できる。 (派遣社員 'is-a' 社員、正社員 'is-a' 社員)
秘密を隠蔽する
隠蔽は、価値が明白に実証され、長きに渡ってその価値を失わない、数少ない理論的手法の一つ。 以下のような情報は隠蔽し、外部にはインターフェースのみを提供すること。 何を隠蔽すべきかを常に意識し、プロジェクト全体に情報をばらまかないこと。
- 変更される可能性の高い領域のコード
- マシン固有のコード
- 型の実装の詳細(int ではなくユーザ定義型を使う、など)
そうすることで以下のメリットが有る
- 変更時に 1 箇所変えればいい=コストを抑えられる
- 頭の中で 1 度に整理すべき事項が少なくなることで、「複雑さへの対処」を行うことができる
変更の可能性が高い領域を特定する
変更されそうな部分は、独立したクラスにしておく。一般的に変更されやすい領域は以下の通り。
- 業務ルール
- これはテーブル駆動型にしておくとなおよい
- ハードウェアに依存する部分
- 入出力インターフェイス(ファイルのフォーマットなど)
- 設計や実装の難度が高い部分
- プログラムの状態変数
- ブール型ではなく列挙型にする
- アクセスルーチンを介して利用する
- データサイズの制約
疎結合にする
疎結合の判断基準
- 数
- パブリックメソッドの数は少なく
- 引数の数は少なく
- 可視性
- データは引数として明示的に渡す
- こそこそグローバルデータを見に行ったりするのは ×
- 柔軟性
- どんな場面でも使いやすいこと
結合の種類
- 単純データパラメータ結合
- モジュール間を、プリミティブな引数のみでやりとり
- オブジェクトパラメータ結合
- モジュール間を、オブジェクト(というよりクラスインスタンスなど)を引数としてやりとり
- 1 よりは密結合
- 単純オブジェクト結合
- モジュールと、その中でインスタンス化されたオブジェクトの関係のこと
- セマンティック結合
- 相手モジュールの内部の仕組みを暗に推察して、何かを行うこと
- 極めて危険で、やっかいな問題を起こすので、使うな
デザインパターンを使う
pattern | desc |
---|---|
Abstract Factory | ? |
Adapter | クラスのインターフェースを別のインターフェースに変換する |
Bridge | インターフェースと実装を別個に変更できるようにする |
Composite | オブジェクトにオブジェクトを包含させる |
Decorator | 動的に機能を追加する。機能ごとにサブクラスを作らない。 |
Facade | 一貫したインターフェースを提供する |
Factory Method | サブクラスでインスタンスを作る |
Iterator | 要素に順次アクセスする方法を提供する |
Observer | 複数のオブジェクトに変更を知らせる |
Singleton | インスタンスを 1 つしか持たない |
Strategy | アルゴリズムをいくつか用意する |
Template Method | ? |
カスタムなやり方をせず、可能な限りデザインパターンをつかうこと。
- デザインパターンを使えば簡単に意図を他者と共有できる
- より上位レベルの話に専念できる
- 車輪の再発明を防げる
- 問題には、問題を複数回解決してみないとわからない部分がある
- 既存のソリューション・ライブらいはそれらを経験し、克服している
注意点
- パターンは絶対ではない。標準パターンに 100%無理に合わせなくてもいい。
- あくまで手段である。パターンを試してみたいという理由だけで使うな。
その他のヒューリスティクス
- 凝集度を強くする
- 階層化する
- クラス規約(事前条件・事後条件など)を決めておく
- 責任を割り当てる
- テストしやすい設計にする
- バインディングタイムを意識的に選択する
- 制御を一元化する(可能な限り 1 箇所に集める)
- 総当たり法をバカにするな、場合によっては最適である
- 図を書いてみる
- ルーチンやクラスを「ブラックボックス」に仕立て、設計をモジュール化する、簡単にする
設計のプラクティス
設計で心がけたほうが良い手順
反復
- 何度も設計してみる。ほぼ必ず、2 回目以降のほうがうまくいく。
- 上流・下流を行ったり来たりすることで、大きな改善が得られる(頭を切り替えるのが辛いけど頑張れ)
分割攻略
- 人間の頭脳は大きくない。複雑な問題は、様々な分野に分け、1 つずつ解決すること。
- 行き詰まったら、その分野で「反復」すること
トップダウン
- 汎用的な問題(大きなクラス)から始めて、それを扱いやすい大きさに分解していく、分割戦術である
- 頭脳が処理できる情報には限りがあるという前提にたっている。
- 分解するよりもコーディングしたほうが簡単に思えるまで、分解し続ける
- 長所
- 人は大きなものを小さく分解するのが得意なので、簡単
- コンストラクションの詳細を先送りにできる、隠蔽できる
- 短所
- 下位レベルに移るにつれて複雑さが増大して行き詰まることがある
ボトムアップ
- 扱いやすい大きさから初めて、全体的なソリューションを組み立てていく、組み立て戦術である
- 全体が漠然としていて、どこから手を付けていいのかわからない時に最適
- 方法
- システムが何をするのかを考え、
- 具象(concrete)クラスと機能を洗い出し、
- グループ化(サブシステム・パッケージにする、オブジェクトにする、継承するなど)して、
- 1 つ上のレベルに進む
- 長所
- 早い段階で必要な機能が分かるので、コンパクトで堅牢なコードになる
- 短所
- 小さいものから大きなものを組み立てていくのは、難しい
- 上位レベルに移ったときにしか気づかない間違いがある
実験的プロトタイプ
「やってみないとわからない」問題を解消するために行う。 下記の条件のもとで行う。
- エンジニアが、必要最小限のコードを書く訓練を受けている
- 設計の問題が十分に明確である
- コードは必ず使い捨てにする。製品コードに流用しない。
コラボする
- 2 人で考える
- 1 人しかいない場合は、一週間放置してから考える
- 第三者に意見を聞いてみる
どれだけ設計すれば十分か
場合による。迷ったら、少し多めに設計に時間を使う。
- 難しいと感じていたので設計を頑張った分野で、問題は起こらない事が多い。
- 簡単だと感じていたので設計を軽視した分野で、問題が見つかる事が多い。
設計作業の文書化
公式な文書にする以外の方法
- コードに直接挿入する
- wiki にする
- メールする
- デジカメでホワイトボードを撮る
- フリップチャートにする
- CRC カードを使う
- UML 図を作る
まとめ
下記は明らかに間違いである
- 全ての詳細をひとつ残らず設計すること(やりすぎ)
- 何も設計しないこと(やらなさすぎ)
以下の気持ちを忘れずに
- 設計を規律ある行為だと思うな(純粋主義者は無視しておけ)
- 設計は、やっかいで、ルーズで、ヒューリスティックなものであると認識しろ
- 最初に思いついた設計で満足するな
- 第三者と協力しろ
- 単純さにこだわれ
- 必要があればプロトタイプを作れ
- 反復、反復、反復
クラスの作成
Abstract Data Types: ADT
- ADT=データとそのデータに作用する操作をまとめたもの。
- クラス=ADT+継承+ポリモーフィズム
ADT を使うメリット
- 複雑な秘密(メンバデータ、実装の詳細)を持ち、それらを抽象化されたインターフェースからのみ操作する
- 抽象化されているのでわかりやすい、ひと目でわかる
currentFont.attribute = 0x02
よりもcurrentFont.setBoldOn()
- 変更により影響が及ぶ範囲が小さくなる
ADT を使う時のガイドライン
- 下位レベルのデータ型を、上位レベルに抽象化する(現実世界の問題として扱う)
- × スタック、リスト、キュー
- ○ 社員、請求書控え
- 単純な項目でも ADT として扱えば読みやすくなる
light.on()
,light.off()
良いインターフェース
良いインターフェースは、良い抽象化と、良いカプセル化から成る。
良い抽象化
- 抽象化のレベルを揃える
- 例)社員というハイレベルな抽象化と、リストという下位レベルの抽象化が混在
addEmployee()
RemoveEmployee()
firstItem()
->firstEmployee()
であるべきlastItem()
->lastEmployee()
であるべき
- インターフェースは対で提供することが多い
- on - off
- add - remove など
- 関係のない情報は別のクラスへ分離する
- インターフェースの変更・追加時にルールを逸脱しないよう注意する
良いカプセル化
- カプセル化
- 「実装の詳細を隠してしまうこと」
- 具体的には、外部からのメンバルーチンへのアクセスを最小限にする。もちろんメンバデータは非公開にする。
- カプセル化の「意味的な違反」に注意する
- プライベートな実装を意識してはならない。あくまでパブリックなインターフェースのみに依存すること。
- クラスを使う時に内部実装を調べなければならないとしたら、それは抽象化に失敗したダメクラスである
設計と実装の問題
包含(has a)
- クラスがメンバデータを保持している、ということ。
- 社員(class) has a:
- 名前
- 電話番号
- 社会保険番号
- 社員(class) has a:
- クラスのメンバデータが 7 個を超えたあたりから、クラスを分解することを検討するとよい
- プライベート継承を使って'has a'を実現するな。親と子の結合度が高くなりすぎ、カプセル化に違反するから。
- プライベート継承とは、親の protected なメンバデータを子に継承することで、
has a
を実現すること。 is a
の関係でモデリングしたい場合を除き、通常は継承よりも包含のほうが望ましい。
継承(is a)
- 子が、親の「特化」したバージョンではあるものの、基本的に同一である、ということ。
- 子は、親のインターフェースに 完全に 従う。従わないとしたら、実装が正しくない。
- 継承は危険なテクニックである。継承するなら、きちんと設計とドキュメンティングを行い、できないならそのクラスの継承自体を禁止すること。
- Liskov substitution principle(LSP)に従え
- 親で定義される全てのルーチンは、子でも全く同じ意味を持つこと
- 子の種類によって意味が異なるとしたら、複雑さが増すだけで害しか無いから
- 親のオーバーライド不可能(private)なルーチンと同じ名前を子で使うな
- 共通のインターフェース、データ、振る舞いは、可能な限り継承の上位に移動する
- インスタンスが 1 つしかないクラスは消せ(るかも)
- 派生クラスが 1 つしか無い基底クラスは消せ
- ルーチンをオーバーライドしているのに中身が空のルーチンを見たら、親の設計を見直せ
- 子の種類は 7 つまで、階層は 3 つまでにとどめよ
- たくさんの条件分岐が出現したら、ポリモーフィズムの活用を検討せよ
- 多重継承は、mixin のために使うという意識で。例えば、
Displayable
とSortable
というインターフェースを実装するなど。
包含と継承の使い分け
- 複数のクラスでデータのみ共通 → クラスを作成し、複数のクラスで包含する
- 複数のクラスでルーチンのみ共通 → 共通のルーチンを持つ基底クラスを作成し、複数のクラスで継承する
- 複数のクラスでデータ・ルーチンとも共通 → 共通のデータ・ルーチンを持つ基底クラスを作成し、複数のクラスで継承する
メンバルーチンとメンバデータ
- ルーチンの数は最小限に
コンストラクタ
- 全てのメンバデータを初期化せよ
- シャローコピーよりはディープコピーを優先して使え
クラスを作成する理由
- 現実オブジェクト(車など)をモデリングする
- 抽象オブジェクト(四角形など)をモデリングする
- 抽象化の力により複雑さを緩和する
- 複雑さを分離する。一箇所直せばいい。
- 実装の詳細を隠蔽する
- 変更による影響を限定する
- 引数の受け渡しを減らす
- 複数のルーチン間で引数をやり取りしている場合、その引数をメンバデータにもつクラスを作ることを検討する
- 制御を一元化する(DB 操作、ファイル操作、プリンタ操作など)
- コードの再利用を促進する
- 関連する操作をパッケージにまとめる(三角関数、文字列操作、ビット演算など)
望ましくないクラス
- ゴッドクラス
- メンバデータしか持たないクラス
- 他のクラスに移譲できないか検討する
- メンバルーチンしか持たないクラス
- クラス名が動詞になったら要注意
- DatabaseInitialization や StringBuilder といったクラスは、他のクラスのメンバールーチンであるべき
高品質なルーチン
ルーチン:関数、メソッドなど
ルーチンを作成する理由
- 詳細について考える必要をなくすことで、複雑さを低減する
- 中間部分をわかりやすく抽象化する(
getConvertedName()
みたいな) - 重複を排除する
- ルーチン(メソッド)を十分に分解し短く保つことで、サブクラスの作成を楽にする
- 複雑になりがちな、ポインタ関連処理を囲いだし、隠蔽する
- 移植性のない機能を囲だし、移植性を向上させる
- 複雑な論理評価(true or false)を囲いだし、単純にする
- 囲いだしたコードを 1 箇所改善することで、全体に効果が波及する
とても短いルーチンの扱い
1 行だけの単純なコードでも、場合によってはルーチンにしたほうが良い場合もある。
ルーチンレベルでの設計
cohesion: 凝集度、強度。ルーチン内の処理がどれだけ密に関連しているかを表す。
下記の凝集度がある。先に記述したものほど理想的である。名前は覚えなくていいので内容を覚えること。
機能的凝集度
最も高く、理想的な凝集度。ルーチンが一つの機能だけを提供する場合。id(入力データ)から名前を取得するgetCustomerName()
など。
情報的(順序的)凝集度
ルーチンが決まった順序で実行する処理で構成される場合。生年月日(入力データ)から年齢を計算し、その年齢をもとに定年までの期間を計算する、など。
この凝集度を見つけたときは、2 つのルーチンに分離すること。
連絡的凝集度
同じ入力データを使用するものの、全く別の処理を行う場合。報告書(入力データ)を印刷し、その後入力データを初期化する、など。
この凝集度を見つけたときは、2 つのルーチンに分離すること。
時間的凝集度
ルーチンが、同じ時期に実行されるべきという点でのみ共通する機能を提供する場合。startup()
,shutdown()
など。
あくまで、他のルーチンを呼び出す指揮役として使う場合のみ使用が容認される。
手順的凝集度(使うな)
画面に入力する順番と一致するから、というような理由で、一連のまとめられた機能を提供する場合。
論理的凝集度(使うな)
引数のフラグにより処理を分岐させる場合。分岐させるという目的のために、特に関連の無いコードが凝集している状態。
フラグに応じて 3 種類の処理のいずれかを実行するのではなく、1 つの処理を実行する 3 種類のルーチンを作れ。
暗号的凝集度(使うな)
もはやカオス。内部の機能に関係性がまるでないルーチンのこと。
良いルーチン名
名前をつけ辛いと感じた場合は、ルーチンの凝集度が低くないか疑うこと。
- 全ての 出力と副次効果 を名前に含める
- perform, output, process, deal など、意味がない or あいまいな動詞を使わない。
- 単なる数字を使わない
outputUser1
outputUser2
など
- 多少長くても意味を理解するのに必要な長さにする。変数名よりは長くなりがち。
- 戻り値を表す
printer.isReady()
customerId.Next()
pen.CurrentColor()
など
- 機能的凝集度であれば、オブジェクトを操作することが多いので、 動詞+オブジェクト名 の形にする
PrintDocument()
checkOrderInfo()
など- ただしオブジェクト指向の場合はオブジェクト名は含めず、
document.Print()
,orderInfo.Check()
などにする
- 正確な反意語を使う
- add/remove
- increment/decrement
- open/close
- begin/end
- insert/delete
- show/hide
- create/destroy
- lock/unlock
- source/target
- first/last
- min/max
- start/stop
- get/put
- next/prev
- up/down
- get/set
- old/new
ルーチンの長さ
長いとエラーが多くなるわけではないものの、目安は 200 行まで
ルーチンの引数の使用
ルーチン間の値のやり取りのエラーは、エラー全体の 4 割を占める
- 引数の順序は、「入力するもの、変更するもの、出力するもの」の順に並べる
- それらが区別できるような名前をつけるとなお良い
- 特に、状態を呼び出し元に伝えたり、呼び出し元にエラーを通知するような引数は、一番最後にすること
- 複数のルーチンで似たような引数を使うときは、なるべく順序を統一する
- 使用しない引数は削除する
- 引数を変更しないこと。ローカル変数を使え。
- 引数に要件がある場合は明記する
- 引数が「入力するもの、変更するもの、出力するもの」のどれに該当するか
- 数値の単位
- 期待される値の範囲
- 期待していない値 など
- 引数は最大 7 個まで
関数とプロシージャ
使い分け
関数とは、値を返すルーチンのこと
id := customerID()
プロシージャとは、値を返さないルーチンのこと
var report MyReport
var success boolean
formatReport(&report, &success)
if success == true { doSomething() }
- ルーチンの主な目的が、関数名が示す値を返すことであるなら、関数を使うこと
- それ以外の場合は、プロシージャを使うこと
戻り値の設定
- 考えられる全ての
return
のパスを意識しておくこと。戻り値を関数の先頭で規定値に初期化しておくとよい。
防御的プログラミング
- Defensive Programming = 問題が起こることを前提とし、予め対策しておくこと
- 多すぎてもダメだし、少なすぎてもダメ
無効な入力への防御
- 外部ソースからのデータの値を確認する
- 入力引数の値を確認する
- 不正な入力を処理する方針を決定する
アサーション
不正な値がないかを確認し、エラーがあれば大声でアサート(主張)すること。
- 入力引数の値が期待範囲内か
- ファイルがきちんと開けたか、ファイルの位置が先頭にあるか
- ポインタが null でないか など
アサーション使用のガイドライン
- エラーとアサーションの使い分け
- 発生しうる間違いにはエラーを使う
- 発生してはならない状況にはアサーションを使う
- 事前条件と事後条件の文書化と検証に使う
エラー処理テクニック
発生しうる間違い(エラー)が発生した場合に、どのように対処して続行するか
- 値を修正して処理を続行する
- 当たり障りのない値を使う。ゼロ値やデフォルト値など。
- 次の有効なデータで代用する。もう一回データを取得してみるなど。
- 前回と同じ値を使う
- 最も近い有効な値を使う
- ログに警告を残して、処理を継続する(必要に応じて他の案と組み合わせる)
- エラーをスローして、親が対処してくれるのを期待する
- グローバルなエラー処理の仕組みを作り、それに任せる
- エラーが発生した場所でエラーを表示する
- ローカルな範囲で、一番良いと思われる方法で処理してしまう
- 処理を中止する
正当性と堅牢性
- 正当性:データが正確であること
- 堅牢性:ソフトウェアの実行が止まらないこと
- 正当性と堅牢性は背反する。コンシューマアプリでは堅牢性が重視される事が多い。
例外
例外のスロー(送出)は、「どう対処したらいいかわからない。誰か対処方法を知らない?」と叫ぶようなもの。上位のルーチンが例外を補足して対処するのが基本の流れである。
- 例外は、無視すべきでないエラーに使う。
- 例外は、他の(多くの場合、上位の)プログラムに伝達するためにつかう。
- 絶対に発生してはならないイベントで使用する(アサーションと同じ)
- ローカルで処理できるエラーを例外にするな
- コンストラクタ・デストラクタ内に例外を書くな(リソースリークの原因になる)
- 抽象化レベルを揃える(Employee クラスなら、EOFException ではなく、EnployeeDataNotFound にする、など)
- 例外メッセージには必要な情報を全て盛り込む
- 空のキャッチブロックは書かない
- 使用するライブラリがスローする例外を知っておく
- ログの記録、例外の報告を一元管理する仕組みを作るのもよい
バリケードによるエラー被害の囲い込み
- 外部データは汚れているので、バリケードで消毒し、内部データを安全に保つ、という考え方
- 入力データは、可能な限り早い段階で正しい型に変換する
- バリケードの外側には、エラー処理を使用する(データのエラーが起こりうる)
- バリケードの内側には、アサーションを使用する(プログラムのエラーしかありえない)
デバッグエイド
デバックを補助するコードや仕組みのこと。
プロダクション環境の制約を開発時には無視する
開発をスムーズに進めるために、開発環境に限って、リソースを大量に使う、実行が遅くなる処理を入れる、セキュリティを無視をする、などを行うことを検討する。
早期導入
デバッグエイドの導入は早ければ早いほどよい
攻撃的プログラミングの使用
開発段階で積極的に失敗を引き起こし、プログラムを中断してしまうことで、問題を洗い出しと修正を促すこと。
- アサーションでプログラムを中止して苦痛を伴わせ、問題を修正させる
- case 文の default 句で盛大に警告を出して失敗する など
デバッグエイドの削除方法
make
など、バージョン管理ツールで行うdefine
など、組み込みのプリプロセッサを利用する- 独自のプリプロセッサを作成する
- デバッグエイド(ルーチン)を、製品版ではスタブに差し替える など
製品コードに防御的プログラミングをどれくらい残すか
- 重要なエラーを検査するコードは残す
- 重要なエラーが発生したときは、プログラムを上品にクラッシュさせる
- 些細なエラーを検査するコードは削除する。または、エラーを記録するに留めるなどし、目立たなくする。
- 可能な限り、プログラムを中断するようなコードは控える(ユーザデータを失わないように)
- エラーメッセージを出す場合は、ユーザにわかりやすい言葉にすること
変数の使用
変数宣言・初期化のベストプラクティス
- 暗黙の宣言は無効にし、すべての変数を明示的に宣言する
- 宣言は使用場所の近くで行う
- 宣言時に初期化も行い、難しい場合はなるべく使用場所の近くで行う
- できるだけ final / const を使う
- カウンタ等、再初期化の必要がないか確認する
スコープ
- スコープ = 変数の知名度
- 持続間隔 = 変数を利用する箇所の間隔
- 寿命 = 変数を宣言した場所から、最後に利用した場所までの距離
スコープを最小限にすることで下記の効果がある
- 一度に覚える必要のある情報が減り、読みやすくなる
- エラーが入りこむ余地が減る
- リファクタリングしやすくなる
スコープを小さくする方法
- 関連するステートメントをまとめる、あるいは別ルーチンに切り出す
// Bad
show(OldData);
show(newData);
delete oldData;
delete newData;
// Good
show(OldData);
delete oldData;
show(newData);
delete newData;
- はじめは最も狭いスコープ(private など)にしておく
永続性
変数の永続性(賞味期限)を勘違いすると事故が起こる。対策は次の通り。
- 重要な変数に正しい値がセットされているか定期的に確認して、不正なら警告を出す
- 使い終えた変数に意味のない値を設定しておく
- データが永続的でないことを前提にコードを書く(変数はすべて使用直前に宣言する、そうでない変数には警戒する、など)
バインディングタイム
変数に値を設定する時期のこと。数字が大きいほど柔軟性が高いが、複雑でエラーが起こりやすくなる相反関係にあるため、適当なところで折り合いをつける。
- ハードコーディング
- 定数でコーディング(ハードコーディングよりは常にマシ)
- プログラムのロード時に環境変数などから読み込む
- インスタンス生成時(ウィンドウ作成時など)
- ジャストインタイム(ウィンドウ移動時など)
1 つの目的に 1 つの変数
- 変数を再利用しない
- 例えば
temp
を同じスコープで、違う目的で回使わないこと。そうなった場合は、より具体的な 2 つの名前につけ直すこと。
- 例えば
- 変数に 2 つの意味や隠れた意味を持たせない(ハイブリッド結合しない)
- 例えば、通常は人口(Integer)を表すが、-1 の場合はエラー(Boolean)を示す変数など。
変数名の力
変数名以外にも、クラス、パッケージ、ファイルなどにも適用可能。
良い名前にするための Tips
名前はなるべく具体的にする
なにを表す変数なのか考える必要がない程度の具体的な名前にする。
- Good
- runningTotal
- trainVelocity
- currentDate
- linesPerPage
- Bad
- ct
- velt
- x
- lpp
- lines
- date
問題を表す名前にする
- Good
- employeeData
- printerReady
- Bad
- inputRecord
- bitFlag
最適な長さにする
8 文字~ 20 文字くらいが最もデバッグしやすいという研究がある
- 長すぎ =>
numberOfPeopleOnTheUsOlympicTeam
- 短すぎ =>
n
- ちょうどいい =>
numTeamMembers
合計・平均・最大などを表す名前は変数名の最後につける
Total
, Sum
, Average
, Max
, Min
, Record
, String
, Pointer
など、計算した値を保持する変数には、その修飾子を最後につける。
例)
- revenueTotal
- expenceAverage
- expenceMax
ただし、num
は例外なので要注意
numCustomers
=> 顧客総数('s'に注目)customerNum
=> 顧客番号
可能であればnum
は使わずに下記のようにしたほうが良い。
customerTotal
=> 顧客総数customerIndex
=> 顧客番号
わかりやすい反意語を使う
- begin / end
- first / last
- locked / unlocked
- min / max
- next / previous
- old / new
- opened / closed
- visible / invisible
- source / target
- source / destination
- up / down
特殊なデータの命名
ループ変数
ごく単純で、ネストされず、ループが 3 行以内で、ループの内部でのみ使用されるインデックスには、i
,j
,k
といった名前を使っても良い。それ以外の場合は、通常と同じく、より具体的な名前をつける。
状態変数
いわゆる「フラグ」のこと。意味のある名前をつける。必要に応じて定数も使う。
# BAD
flag = 0x1;
statusFlag = 0x80;
pringFlag = 16;
# GOOD
dataReady = true;
reportType = REPORT_TYPE_ANNUAL;
recalcNeeded = false;
一時変数
temp
やx
などのこと。そもそも全ての変数は一時的なものである。temp
という名前をつけたくなったときは、プログラマが問題を理解できていない可能性もある。より具体的な名前がつけられないか、よく検討すること。
ブール変数
- 有名どころを使う
- done
- error
- found
- success / ok
- true or false になる名前をつける
- status => statusOK
- sourceFile => sourceFileAvailable / sourceFileFound
- 頭に
is
をつけると正しい名前を矯正されるが、やや読みにくい - 肯定的な名前を使う
- notFound => found
- notDone => done
- notSuccessful => successful
列挙型
Color_Red
, Color_Blue
のように、カテゴリを表すプレフィックスを付ける
命名規則の力
命名規則を作る理由
どのような規約でも無いよりはまし
- 考えなくて済む
- 覚えたルールを他で活かせる
- 早く理解できる
- 名前の増殖を防ぐ
- プログラミング言語の弱点を補う
いつ命名規則が必要か
- 複数のプログラマがいる
- 誰かに引き継ぐことがある
- プロジェクトが大きい
- プロジェクトが長い などの場合
どれくらい正式にするか
短小プロジェクトではゆるく、長大プロジェクトではきつく
短くて読みやすい名前
無理に省略するのは昔のなごりである。それでも省略したいなら、ガイドラインをまとめてプロジェクト内に周知しておくこと。
ガイドライン
変数が 8 文字~ 20 文字程度になるまで下記の作業を繰り返す
- 標準的な略記を使う
- 母音を削除する
- computer => cmpter
- screen => scrn
- and, or, the などを削除する
- '-ing', '-ed'などを削除する
- 名前の中で重要な単語を最大で 3 つ使用する
- 省略するなら 2 文字以上省略する
- 変数の意味を変えないように注意する
省略するときの注意
- 省略法は一貫する
- 発音できる名前にする(xPos -> good, xPstn -> bad)
- 読み間違えなどを招く名前を避ける(bEnd -> good, bend -> bad)
- 書き手よりも読み手を大事にする。読み手に優しくない省略法は使うな。
ダメな名前
- 意味が似た名前をいくつも使うな
input
/inputValue
recordNum
/numRecord
- 見分けにくい名前を使うな
- bad =>
clientRecs
/clientReps
- good =>
clientRecords
/clientReports
- bad =>
- 名前に数字を使うな
- 綴りを勝手に変えない
- bad =>
hilite
- good =>
highlight
- bad =>
- 綴りを間違えやすい単語を使わない
absence
accumulate
receipt
基本的なデータ型
数値全般
- 0 と 1 だけは必要に応じてハードコーディングしてよい。0 はカウンタの初期値、1 はインクリメントなどに使う。
- それ以外の数値(マジックナンバー)は使うな、名前付き定数を使え
- 0 除算が起きないよう注意する
- 型変換は明示的に行う
- 異なる型を(暗黙的変換で)比較しないこと
整数
- 除算に注意する 結果は言語により異なる(7/10===0 など)
- 桁あふれに注意する(中間結果、最終結果どちらも)
浮動小数点
- 大きさが極端に異なる数の加減算はするな
- 桁が足りなくなり、結果が不正確になるから
- もし行う場合は、数をソートしてから絶対値の小さい順に足していくと、最も誤差は小さくなる
- 等価を比較しない
- 同じ値になるはずの 2 つの計算結果が、違う値になることはしばしば発生するため
- もし比較したい場合は、ある程度の誤差を許容する、比較のためのルーチンを作成すること
- 丸め誤差に対処するには
- 精度の高い方に変換する(単精度 → 倍精度)
- BCD(Binary Coded Decimal)に変換する
- 整数に変換する(ドルなら、105 を 1 ドル 5 セントとして管理するなど)
- 丸め誤差に敏感な、専用の型が使っている言語に用意されていないか確認する
文字と文字列
- マジックキャラクタ(
'a'
など)・マジックストリング("Great Title"
など)を使うな、名前付き定数を使え - off-by-one エラーに注意する(文字列数を超えた読み取りなど)
- Unicode を使う
- 開発当初から i18n の戦略を練る
- C 言語における諸注意は省略、本書参照
ブール変数
- 説明変数として使うことで、評価を単純にする。また、プログラムを コードで文書化 する
reachedToLastLevel = level === maxLevel
列挙型
- コードを読みやすくするために使う
// bad
result = getData(data, true, false, false);
// good
result = getData(
data,
EmploymentStatus_CurrentEmployee,
PayrollType_Salaried,
SavingsPlan_NoDeduction,
);
- 信頼性を高めるために使う(ありえない値をコンパイル時にチェック)
- 保守性を高めるために使う(実際の値が変更するときは 1 箇所を編集すれば OK)
- ブール値の代わりに使う(true + 2 種類の false など)
- if/case で使う場合は、最後に無効な値を検査するのを忘れずに
- 実際に使用する要素以外の、制御用要素を使う
- 列挙の最初と最後の要素をループ時に使用する
- 最初の要素に無効な値を設定することで、未初期化の値を検出する
enum Color {
Color_InvalidFirst = 0, // 未初期化を検出
Color_First = 1, // ループの最初の要素として使用
Color_Red = 1,
Color_Green = 2,
Color_Blue = 3,
Color_Last = 3, // ループの最後の要素として使用
}
- 言語に enum が存在しない場合は、自分で作ること
名前付き定数
- 固定的な値を一元管理して、変数宣言時やループ時などに使うことで、保守性・可読性を高める
- たとえ安全そうな場合でも、リテラルは利用しないこと。例えば
12
ではなくMONTH_IN_YEAR
にするなど。 - 言語に名前付き定数が存在しない場合は、自分でつくること。
- 必ず名前付き定数を使い、リテラルと混在させないこと。まざると危険。
配列
- 配列の範囲外にアクセスしないよう注意する
- なるべく、配列よりも「セット、スタック、キュー」を使うこと
- 配列の端っこ(先頭、末端)では off-by-one エラーに注意
- インデックスには意味のある名前をつけることで、以下の問題への対処になる。
- 多次元配列ではインデックス順に注意する
arr[i][j]
とarr[j][i]
など - ネストしたループではクロストークに注意
arr[i]
のつもりでarr[j]
など
- 多次元配列ではインデックス順に注意する
ユーザー定義型(型のエイリアス)の作成
型を一元管理して、保守性・可読性を高める(名前付き定数の、型バージョン)
type Coordinate float64 // ここを変えれば全体を変えられる
var coord1 Coordinate
var coord2 Coordinate
- 現実世界の問題を表す名前にする
TinyInt
ではなくAge
など - 型が変更される可能性がある場合は必ずユーザ定義型を使うこと
- 組み込み型を再定義しない。混乱のもと。
特殊なデータ型
構造体
他の型を基にして作成されたデータのこと。 一般的には構造体よりもクラスを使ったほうがよいが、以下のような場合で使われる。
データの関係を明確にする
// bad
age := 18
name := "John"
sarary := 500
height := 180
// good
husband.age := 18
husband.name := "John"
wife.sarary := 500
wife.height := 180
データの処理を単純化にする
man.name = "John"
man.age = 18
man.sarary = 180
man2 := man // プロパティを一括して複製できる
引数リストを単純化する
たくさんの引数を一つの構造体にまとめることができる。ただし、乱用に注意する。
保守作業を軽減する
一箇所を変更するだけで全体を変更できる。
ポインタ
ポインタを構成するもの
- メモリアドレス
- 内容を解釈するための情報
- 解釈の方法はポインタの基底型で決まる
- 整数ポインタなら整数として、文字列ポインタなら文字列として解釈する
- メモリアドレスをスタート地点とし、基底型が必要とする長さだけデータを読み込む
ポインタに関する注意点
- 通常のエラーは、原因箇所を特定するのは簡単で、修正が難しい
- ポインタのエラーは、原因箇所を特定すること自体が難しい
このため、下記の点に注意する
- ポインタ操作(以下のすべての作業)はルーチンやクラスに分離する
- ポインタを宣言したら、必ず初期値を設定する
- ポインタの割当てと削除は同じスコープで行う
- クラスのコンストラクタで割当て、クラスのデストラクタで削除
- ルーチンで割当て、兄弟ルーチンで削除
- 使用前にアドレスを検査する アドレスが想定範囲を逸脱していないか
- 使用前に値を検査する 値が想定範囲を逸脱していないか
- ドッグタグを使う(詳細省略)
- ポインタはケチらず使って読みやすいコードを書け
- 図を書くとわかりやすくなる
- 後始末に気をつける(詳細省略)
- なるべくポインタを使わない、他の技術を使う
C のポインタ、C++のポインタ
省略
グローバルデータ
問題点
- 意図に反して変更されている事がある
- コード再利用の妨げになる
- 全てのグローバルデータを頭に入れて置かなければ、コードを理解できない
- 人間は大きなプログラムを理解できない
- 部分に分けて、それぞれごとに考えれば済むようにするしかない
使うべき場所
- グローバルな値の保存(コンフィグなど)
- 名前付き定数の代わりに使う
- 列挙型の代わりに使う
- トランプデータの削除(データをルーチンに渡す目的が、更に別のルーチンに渡すことに過ぎないこと)
あくまで最後の手段
もしどうしても使いたい場合は、アクセスルーチンを使用すること(詳細省略)
ストレートなコードの構成
順序が重要なステートメント
順序(依存性)がわかる書き方をすること。
- コード構成、ルーチン名で表す
- 例えば、初期化を行うルーチンなら、
ComputeMarketingExpense
ではなくInitializeExpenceData
など
- 例えば、初期化を行うルーチンなら、
- ルーチンの引数を使って表す
# good(順序が大事であることがわかる)
data = initialize(data)
data = compute(data)
data = finalize(data)
# bad(順序が大事であることがわからない)
computeMarketingExpence()
computeSales()
computeExpence()
- コメントで説明する(最後の手段)
順序が重要でないステートメント
コードが実行順序に依存しない場合は、関連する作業をできるだけ近くに配置すること。
上から下へ読めるコード
読むべき場所が散在しているコードは悪いコードである。なるべく関連するものを一箇所にまとめること。変数の寿命を短くするのは効果的。
関連するステートメントのグループ化
関連するステートメントを四角で囲ってみて、四角形が交錯するなら、うまくまとまっていないといえる。
条件文の使用
if
if-then を書く場合の注意
- 正常系の実行パスを、読みやすいように最初に書く。異常系の処理によって読みにくくなることがないようにする。
- 異常系は原則として else 文に書く。
- else 句は不要な場合が多いので疑ってかかること
- off-by-one エラーに注意する。
<
と<=
の書き間違いなど。
if-then-else の連鎖を書く場合の注意
- 複雑な条件式はルーチンに切り出してカプセル化する(
isAlphabet()
など) - もっとも一般的なケースをより上位に書く
- case 文で代用できないか検討する
case
順番
下記の中から一番適切なものを選択する
- アルファベット順・数値順
- 正常系と異常系の 2 つしかない場合は、正常系を先頭にする
- 出現頻度順
その他
- 各ケースの処理は短く書く。長くなるならルーチンに切り出す。
- 簡単に分類できる単純なデータにのみ使用する。分類作業が複雑になるなら if 文を使う。
- default 句では「その他」扱いのものだけを扱う、または「エラー処理」に使う
- fallthrough は使うな。使うなら、必ずコメントを残せ。
ループ
- ループは複雑である。単純に保つよう努力せよ。
- 変わったループを作らない
- ネストをできるだけ少なくする
- 入口・出口を明確にする
- 前処理・後処理を一箇所にまとめる
- ループ変数には良い名前をつけ、1 つの目的で使う
- 全てのケースで正常に実行され、どんな条件でも終了することを検証する
ループの種類
- カウント:決められた数だけ
- 連続評価:ループ毎にどうするか判定
- エンドレス:永遠に
- イテレータ:イテレーションが終わるまで
while-break
while-break
を使うと、ループの最初や最後ではなく 途中 に出口があるループを作ることができる。- ただし、ループ内部のコードを見ないと終了条件がわからないというデメリットがある。
- 下記のように、重複処理を避けるために使うとよい。
// bad
doSomething1();
doSomething2();
while (score < 10) {
other();
doSomething1();
doSomething2();
}
// good
while (true) {
doSomething1();
doSomething2();
if (score < 10) break;
other();
}
for
- 単純な処理にのみ使用する
- ループを途中で抜けたいなど、制御が複雑なループには
while
を使用する - ループを途中で抜けたいがために、ループ変数を変更してはならない
foreach
- ループを繰り返すための計算が不要であるため、エラーの原因を減らせる
ループの制御
ループに関する問題を防ぐためのベスト・プラクティス
- ループに影響する要因を最小限にする(単純にする)
- ループの内部をブラックボックスにする
- ループの内部をルーチンとして考える
- 制御に関わる変数などを、なるべくループの外に出す
ループの開始
- 入り口は 1 箇所にする
- 初期化コードをはループの直前に書く(近接の法則)
- for のヘッダにループ制御に関係ないコードを詰め込まない
ループ本体
- 本体が空のループは作るな、書き直せ
- 前処理・後処理(
i++
など)は、ループの先頭か末尾にまとめる - ループ内の処理は 1 つの機能に絞る
- 単一責任の法則。ルーチンと同じと考えよ。
- とりあえず分けて作成し、パフォーマンスの問題が出てからまとめる、で OK
ループの終了
- どんな場合でもループが終了することを確認する
- 終了条件を明確に記載する
- for において、ループ変数を書き換えない
- ループ変数の最終値を使用するな。必要ならループ外の変数に明示的に値を保存しておけ。
- 安全カウンタ(上限)を適宜使用する
- while ループでは、フラグよりも break を使うときれいになりやすい。ただし、複数の break には要注意。
- continue は先頭で使う。中盤以降で使う場合は替わりに if 文を使うこと。
- continue, break は注意して使う(終了条件を知るには内部を見る必要があり、ブラックボックスではなくなってしまうから)
ループ変数の使用
- ごく単純なループを除き、ループ変数には
i
などの意味のない名前ではなく、carNumber
など意味のある名前をつける。特にネストする場合は。 - ループ変数は、ループ内のみをスコープにする(コンパイラに頼らないこと)
ループの適切な長さ
- 最長でも 1 画面で確認できる程度の短さにする
- ネストは最大でも 3 段階まで
- 長くなりすぎる場合はルーチンに切り出す
- 長いループでは、出口や終了条件を特にシンプルにすること
ループの作成
ループ内部から、外側に向けて作成していくと良い
ループと配列
配列をループ処理する場合は、foreach
やmap
を積極的に使用する。それにより、ループにまつわる様々な問題を減らすことができる。
特殊な制御構造
ルーチンからの複数の return
読みやすくなる場合を除き、return の使用は最小限に抑える。
- コードを読みやすくするために使う
- 答えがわかった時点で制御を呼び出し元に戻すことで、読みやすくなる場合がある
- ガード句を使って複雑なエラー処理を単純化するために使う
- 前提条件を満たしていない場合などは、ルーチンの頭で return することで深いネストを避けることができる
再帰
- 問題の範囲が狭い時に使うのが最適である。
- 多くの場合、単純にスタックと繰り返し構造を使ったほうが理解しやすい。
ヒント
- Base Case を必ず作る
- 安全カウンタを作って無限再帰を防ぐ
- 再帰の中から別の種類の再帰を呼ばない(ルーチンは 1 つに限定する)
- 再帰を使う必要のないものに使わない(階乗やフィボナッチ数列など)
goto
ほぼすべての goto は他の制御構造に書き換えられる。よほどの理由がない限り使わないこと。
テーブル駆動方式
- 複雑なロジック(if|case)や、複雑な継承構造を劇的にシンプルにできる
- もしテーブルデータを外部に保存すれば、コードを変更せずにデータを修正できる
例えばinputChar
という変数の種類を判定したい場合、テーブルを使うと、複雑なif
文を使わずにすむ。
charType = charTypeTable[inputChar];
検討すべき事項
- 参照方法をどうするか
- 直接アクセス
- インデックスアクセス
- 段階型アクセス
- 何を格納するか
- データ
- ルーチン
直接アクセス方式
特定の値をキーとしてテーブルにアクセスする方法。目的の要素に一発でアクセスできる。
// 指定した月の日数を求める
daysOfMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
days = getDaysOfMonth(someMonth - 1); // 関数ではなく単純にArrayで実装してもOK
// 保険料率を予め用意した表から取得する
rate = rateTable(smokingStatus, gender, maritalStatus, age);
参照キーの補正
例えば「年齢」をキーにするものの、18 歳以下は全て同じデータを返したい場合など、 参照に使うデータをそのままキーとして使えない。このような場合は、以下の方法でキーの補正が必要になる。
- テーブルを複製する方法
- 1 から 18 歳までのテーブルに全て同じデータを複製する。
- キーを変換する方法
- ルーチンを使う
keyFromAge(age)
で 18 歳以下のキーを 18 に変換 - ハッシュを使う
keyFromAge[age]
で 18 歳以下のキーを 18 に変換
- ルーチンを使う
インデックスアクセス方式
単純な数値変換では、テーブルのキーを取得できない場合に使う。 (多くの空要素を容認した)インデックスを生成することで、散在したデータを上手く扱うことができる。
例)100 種類の商品を 0 から 9999 までのランダムな番号で管理している場合:
- インデックステーブル
- 10000 個の配列
- ほとんどは空要素
- 詳細データへの参照を持つ
- 詳細データを含むテーブル
- 実在する商品のデータのみ保持
下記のメリットが有る
- メモリ消費を減らせる
- 低コストに検索が可能、好きなだけインデックスを作れる
- 保守性が高い
段階型アクセステーブル
インデックスアクセス方式では対応できない、不規則なデータや、きりの悪いデータに適している。 特定の値ではなく、範囲をキーとしてテーブルにアクセスする。
// スコアをキーに変換する
func getLevelByScore(score float64) int {
rangeLimits := []float64{50.0, 65.0, 75.0, 90.0, 100.0}
maxLevel := len(rangeLimits) - 1
for level, limit := range rangeLimits {
scoreInRange := score <= limit
reachedToLastLevel := level == maxLevel
if scoreInRange || reachedToLastLevel {
return level
}
}
panic("this can't be happen")
}
func main() {
gradeNamesByLevel := []string{"E", "D", "C", "B", "A"}
fmt.Println(gradeNamesByLevel[getLevelByScore(85.1)]) // => "B"
}
注意点
- 終端・境界の処理が正しいか確認する
- 必要に応じて、リニアサーチではなくバイナリサーチを使う
- インデックスアクセス方式の利用を検討する(特にスピードが重要な場合)
- キーの計算はルーチンとして独立させること
制御構造の問題
論理式 (boolean expression)
全ての制御は論理式を使う。
true or false を使って読みやすく
- 論理式には
true
orfalse
を使う。0 や 1 は使うな。 - 論理式(ブール値)の比較には、暗黙の照合を積極的に使え。
done === false
よりもnot done
(a>b) === true
よりもa>b
複雑な式は単純化する
複雑な式は単純化する。ポイントは、コードで文書化すること。
- 中間値を、良い名前の説明変数に代入することで読みやすくする
- 良い名前をつけたブール関数として独立させる
- if や case ではなく決定表(テーブル駆動方式)を使う
肯定的な論理式にする
否定文の繰り返しは非常に理解しにくい。
- if の条件が否定文(
!statusOK
)の場合は、if 句と else 句を交換する - ド・モルガンの定理を利用して、複数の否定を単一の否定にまとめる。
not A or not B
=>not (A and B)
not A and not B
=>not (A or B)
(これは微妙かも)
カッコを使って明確化する
計算の優先順が曖昧な場合は、カッコを使って読みやすくする
式が評価される方法を知っておく
評価の方法は言語によって異なる
A or B
において、A
が真ならB
は評価しないという言語が多い。- ただ、そうでない言語もあり、場合によってはエラーを引き起こす原因になる。
- 読み手を混乱させる可能性がある場合は、ネストさせることで意図を明確にしておくこと。(
if(A){ if(B){} }
)
数値を含む式は数直線の順に並べる
i > MIN and i < MAX
=> badMIN < i and i < MAX
=> good
0 との比較
0 は複数の目的で使用されるため、目的を強調するようにコードを書くこと。
- 論理式(ブール値)は暗黙に比較する
if (!done)
- 数値は 0 と比較する
count != 0
- ポインタは null と比較する
if(bufferPtr)
=> badif(bufferPter == null)
=> good
深いネストの回避
例えば、3 レベル以上の if 文を理解できる人はほぼいない。
以下、if 文の深いネストを回避する方法。
- 早めに return 又は break する(関数内などに限る)
- if-then-else に置き換える(効率的な順番で評価し、評価を無駄に繰り返さないこと)
if (i > 100) {
} else if (i > 10) {
} else {
}
- case に置き換える
switch (true) {
case i > 100:
break;
case i > 10:
break;
default:
}
- ネストしたコードをルーチンに切り出す
- 設計を見直す。多くの場合、単純に理解が足りていないだけの場合が多い。
構造化プログラミング
- 入口が一つ、出口が一つの制御構造を使用すべき、という考え方。
- 構造化されたプログラムは、規則的な方法でプログラムが進む。上から下に読んでいける。
- コードの最も詳細なレベルの話である。構造化されたトップダウン形式の設計のことではない。
3 つの要素
構造化プログラミングでは下記の 3 つの制御構造のみを使用する。
これ以外の制御構造(break
,continue
,return
,throw-catch
,goto
など)が使われていたら批判的に見ること。
- 連続 順番に実行されるステートメントの集合
- 選択 if や case など
- 反復 for や while など
制御構造と複雑さ
- 制御構造の使い方は、プログラム全体の複雑さを大きく左右する。
- 複雑さとは
- コードを理解するのに必要な労力
- 頭の中に一度に整理しなければいけない事柄の数
ガイドライン
- 複雑さを「判定ポイント」で定量化する
if
,while
,for
,and
,or
ごとに 1 と数えるcase
の選択肢ごとに、1 と数える- 6 を超えたら批判的に見る