アルパカログ

Webエンジニア兼マネージャーがプログラミングやマネジメント、読んだ本のまとめを中心に書いてます。

継承はできるだけ避ける。名付けと凝集度「CODE COMPLETE」まとめ

「神は細部に宿る」という言葉があるように、洗練されたコードは細部まで無駄がない。

無駄がないから誰にとっても読みやすいし、保守もしやすい。

洗練されたコードを書きたい人にとって、「CODE COMPLETE」は古いが色褪せないエッセンスに溢れている。

コードのある部分で作業しているときに無視しても問題のない部分をできるだけ増やすことが、優秀なプログラマになるための鍵である。

このエントリでは、「CODE COMPLETE」の6章「クラスの作成」と7章「高品質なルーチン」の内容をまとめる。

名付け

クラス

  • 格納媒体に関する名付けをしてはならない
    • HogeFile など
    • ファイルからメモリになるなど格納媒体が変わった場合に整合性が崩れる
  • クラスの名前を動詞にしない

関数

  • 関数が行うことをすべて説明する
    • 副次効果があって名前が長くなることがある
      • CalculateAndPrint() のような
    • その場合は設計を検討する
      • 関連: 凝集度
  • 意味のない動詞、曖昧な動詞、どっちつかずの動詞を使わない
  • 関数名には戻り値の説明を反映させる
    • PrintDocument()
    • オブジェクト指向言語の場合は、オブジェクト自体が呼び出しに含まれるため、オブジェクトの名前を関数名に含める必要はない
      • document.PrintDocument()はかえって冗長
      • document.Print() で良い
  • 正確な反意語を使用する
    • add/remove
    • increment/decrement
    • open/close
    • begin/end
    • insert/delete
    • show/hide
    • create/destroy
    • lock/unlock
    • source/target
    • first/last
    • min/max
    • start/stop
    • next/previous
    • up/down
    • get/set
    • old/new

包含

  • 包含は「has a」関係
    • 社員は名前を持つ、電話番号を持つ、社員番号を持つ
  • 複数のクラスが共通のデータを持つが振る舞いを共有しないという場合は、これらのクラスに包含できるクラスを作成する

継承

  • 継承は「is a」関係にのみ使用する
  • 継承はプログラムの複雑さを増大させる危険なテクニックである
  • リスコフの置換原則(LSP)に従う

リスコフの置換原則

派生クラスは、ユーザーが基底クラスとの違いに気付かずに、基底クラスのインターフェイスを通じて使用できるものでなければならない

  • 派生クラスが1つしかないクラスを疑う
    • 「いつか必要になるかもしれない」層の基底クラスは設計しない方が良い
  • 深い継承ツリーを避ける
    • 深い継承ツリーはエラーを増やす
  • 継承はプログラマの第一の責務である複雑さへの対処にマイナスに働く傾向がある
    • 複雑さを抑えるには、継承に対して大きな偏見を抱くべきである
  • 複数のクラスが共通の振る舞いを持つがデータを共有しないという場合は、共通の関数を定義する共通の基底クラスを継承する

抽象化

  • うまく抽象化されたクラスインターフェイスは、一般に凝集度が強い

何が起きているのかを理解するために基本実装を調べなければならないとしたら、それは抽象化ではない。

  • インスタンスが1つしかないクラスを疑う(Singletonパターンを除く)
    • インスタンスが1つしか存在しないということは、オブジェクトとクラスを混同した設計かもしれない
    • クラスではなくオブジェクトを生成するだけで済ませることができないか考える

クラス

  • クラスを作成する最も重要な理由は、プログラムの複雑さを低減することである
    • 情報を隠蔽して、その情報について考えないで済むようにしよう
    • クラスを作成することで複雑になってしまわないように設計には細心の注意を払うこと
  • クラスが呼び出すルーチンの種類をできるだけ少なくする
  • 他クラスへの間接的なルーチン呼び出しをできるだけ少なくする
    • 基本的にObjectAは自身のルーチンを呼び出す(デメテルの法則)

account.ContactPerson().DaytimeContactInfo().PhoneNumber()

account.ContactPerson() までは良いが account.ContactPerson().DaytimeContactInfo() は良くない

  • 可能であればコンストラクタで全てのメンバデータを初期化する
    • 安価な防御的プログラミング
  • 「is a」関係をモデリングする場合を除き、通常は継承よりも包含の方が望ましい

関数

  • 関数を作る最も重要な理由は、プログラムを頭で理解しやすくことである
  • 関数の名前はその品質を表す
  • 関数の目的は1つに絞る
  • マジックナンバーを使用しない
  • 引数の値を変更すべきではない
    • 引数を作業用変数として使用しない
  • 引数が多すぎない
    • 引数が多くなる場合は名前付き引数を使用する
  • すべての引数を使用する
  • 複数の似たような引数を使用する場合は、それらの順序を統一する
  • 処理順序を隠蔽する
  • 複雑な論理評価を単純にする
    • 評価を関数として独立させると、コードの基本的な流れと評価そのものがすっきりする

凝集度

  • 関数にとっての凝集度とは、関数の処理がどれだけ密に関連しているかを表す
    • Cosine() という関数は凝集度が高い
    • CoseineAndTa() は凝集度が低い

機能的凝集度

  • 一番良いとされる凝集度
  • 関数が1つのことに関する処理を実行する
    • Cosine() GetCustomerName() EraseFile() など
  • これ以外の凝集度は弱く理想的ではない

その他の凝集度

  • 情報的(順次的)凝集度
    • 社員の生年月日から年齢を計算し、その結果から定年までの期間を計算するように、ある処理の入力が前の処理の出力となっている
    • 生年月日から年齢を計算する関数と、生年月日から定年までの期間を計算する関数を別々に作れば良い
  • 連絡的凝集度
    • 同じデータを使うだけの関連性がない処理の集まり
  • 時間的凝集度
    • 同時に実行される複数の処理の集まり
  • 手順的凝集度
    • 順序以外にまとめる理由がなく特定の順序で実行する処理の集まり
  • 論理的凝集度
    • 複数の処理が1つの関数に詰め込まれ、渡された制御フラグによっていずれかの処理が選択される処理の集まり
    • ただし、一連のif文やcase文と他の関数呼び出しだけで構成される場合は問題がない
      • そのような関数はイベントハンドラと呼ばれる
  • 暗号的凝集度
    • 処理同士に関連がない
    • 根本的な設計の見直しや再実装が必要になる

おわりに

このエントリでは、「CODE COMPLETE」の6章「クラスの作成」と7章「高品質なルーチン」の内容をまとめた。

本書では処理の結果、戻り値を返す「関数」と、戻り値を返さず処理だけを行う「プロシージャ」を合わせて「ルーチン」と表現しているが、この記事ではわかりやすさを優先して「関数」とした。

参考になった方は、ぜひ「はてブ」やSNSでシェアしていただければ幸いである。

「CODE COMPLETE」の他の章は下記でまとめている。