読みやすいコードのガイドライン
コメント
ドキュメンテーション
JavaDoc/KDoc などのこと
ドキュメンテーションは、APIリファレンスの生成やコード参照時に利用されるので、コードの中身を読まなくとも「そのコードが何をするのか・何であるのか」について理解可能にすること
非形式的なコメントを書く場合と比較した注意点を理解するため、アンチパターン6つ
- 自動生成されたドキュメンテーションを放置する
- 宣言と同じ内容を繰りかえす
- コードを自然言語に翻訳する
- 概要を書かない
- 実装の詳細に言及する
- コードを使う側に言及する
アンチパターンを鑑みて、ドキュメンテーションに必要なこと
- コードが何であるのか・何をするのかを最初に簡潔に説明する
- 抽象度や粒度をコードよりも高く保つ
- 実装の詳細やコードを使う側に言及しない
まず最初に要約を書き、その後に詳細を補足すると良い。
コードをトップダウンに理解できるドキュメンテーションを各。
依存関係
4つの観点
- 依存の強さ
- 依存の方向
- 依存の重複
- 依存の明示性
端的に言うと、弱く、巡回や重複が無く、明示的な依存関係が好ましい。
依存関係の強さを示す指標に結合度がある。
これから結合度を種類別に説明するが、説明順に強い→弱いとなっていく。
基本的には結合度はできるだけ弱く保つべきである。
内容結合(content coupling)
依存関係の中で最も強い。
隠蔽されるべきコードの詳細に依存すると発生する。
- 依存先のコードを変更する
- 依存先に隠蔽された変数を、外部から参照する
- 依存先の内部のコードに直接ジャンプする
共通結合と外部結合
この2つの依存関係は、誰もいが読み書きできる場所を使って、値の受け渡しをすると発生する。
- グローバル変数を使って値の受け渡しを行う
- 可変なシングルトンを使う
- IO(ファイル・ネットワーク・外部デバイスなど)や共有メモリなど、コードから単一のものとして見えるリソースを利用する*1
*1: OSや処理系によって、何が単一のものとして見えるかは異なる(視点のレイヤーの違い)
共通結合と外部結合の違いは、受け渡しをする値の種類によって決まる。
- 共通結合:受け渡しする値が、何らかのデータ構造を持つ
- 外部結合:受け渡しする値が、Intといった基本的な単一のデータ型
制御結合
何をするかを決定する値(フラグなど)を渡すことで、呼び出し先の動作を変える場合に発生する。
分岐はプログラミングの本質の1つのため避けられない場面もありますが、以下のような状況では、制御結合を緩和するべきでしょう。
- 不必要に条件分岐の粒度が大きい場合
- 条件分岐間で動作の関連性が薄い場合
スタンプ結合とデータ結合
どちらも弱い結合。
値の受け渡しに関数の引数や戻り値を用い、かつ、制御結合出ない場合に発生する。
(違いは受け渡しする値の種類によって決まる)
gRPC/XML/JSONなどのデータ構造はデバイス入出力やNW通信といったコードの外部の要素とデータを受け渡しする際に必要となるが、これを 外部定義データ形式 という。
また、外部定義データ形式を「コードの内部で定義し直されたモデルクラス」に変換したものを 内部定義データ形式 という。
外部定義データ形式を取り扱う範囲を限定することで、データ形式が更新された場合でも、その影響範囲を限定することが可能となる。
メッセージ結合
引数や戻り値などで情報の受け渡しをせず、単に関数呼び出しをする場合に発生する。
依存の方向性
依存関係に巡回は設けない方が良い。
ただし、プログラム開発において、依存の巡回を完全に取り除くのは不可能。
依存の方向を決める基準
- 呼出元 → 呼出先
- 具体 → 抽象
- 複雑 → 単純
- 可変 → 不変
- アルゴリズム → データモデル
- 仕様の変更が多い → 仕様の変更が少ない
呼出元 → 呼出先
巡回を発生させてしまう典型例
- Callee のメソッドの引数として、Callerの関数をコールバックとして渡す
- Caller 自身のインスタンスを渡す
- Caller のインスタンスを持つ別のオブジェクトをCalleeに渡す
具体 → 抽象
複雑・可変 → 単純・不変
単純なオブジェクト/不変のオブジェクト:
広い範囲で長い期間使われることもあれば、狭い範囲で使われ、すぐに参照が放棄されることもある。
つまり、使われるスコープ・生存期間・被参照の数が小さい時もあれば、大きいときもあるという特徴を持つ。
複雑なオブジェクト/可変なオブジェクト:
スコープ・生存期間・被参照を厳密に管理する必要がある。
単純・不変なオブジェクトは多くの箇所で利用される可能性が高く、もしそのオブジェクトが複雑・可変なオブジェクトの参照を保持する場合、それの挙動変更により多くの箇所で調査が必要になる可能性がある。
これは可用性の依存の話にも似ている。
マイクロサービスアーキテクチャにおいて、可用性の高いMS → 可用性の低いMS に依存してしまうと、可用性の高いMSは、可用性の低いMSに合わせて、可用性をコントロールしなければならなくなる。
つまり強い方が弱い方に合わせなくてはいけなくなる。
依存の重複
- 数珠つなぎの依存
- 依存の集合の重複
数珠つなぎの依存
ある参照を受け取るためだけに、本来関係ないクラスに依存すること
間接的な依存を直接的な依存へ改修することによって解決することができる
デメテルの法則
「直接的な依存を使う」ことを、より一般的かつ形式的に定義したものとして デメテルの法則 がある。
あるメソッドで発生するメンバアクセスについて、レシーバは次のうちいずれかに限られるべきであるという主張
- this 自身
- this のプロパティ
- method の引数
- method 内で生成されたオブジェクト
- グローバル変数やシングルトン
デメテルの法則に従っている例と違反している例を以下に示す。
val GLOBAL_VALUE: GlobalValue = GlobalValue()
class SomeClass() {
// デメテルの法則を満たしている例
fun functionFollowingLod(parameter: Parameter) {
privateMethod() // レシーバーが`this`となるメソッド
property.anotherProperty // プロパティのメンバ
parameter.method() // 引数のメンバ
AnotherClass().method() // メソッド内で作られたオブジェクトのメンバ
GLOBAL_VALUE.method() // グローバル(トップレベル変数)のメンバ
}
// デメテルの法則に違反している例
fun functionViolatingLod(parameter: Parameter) {
parameter.method().anotherMethod // 引数のメソッドの戻り値のメンバ
val propertyOfProperty = property.antoherProperty
propertyOfProperty.method() // プロパティのプロパティのメンバ
}
private val property: Property = Property()
private fun privateMethod() = ...
}
デメテルの法則に従うことで、本来不要である依存関係を解消できるほか、依存元のコードを依存先の内部構造から独立させられる。
表面的な回避策を取るのではなく、それぞれのクラスが知っているべき・知るべきではない情報は何かを意識して設計することが重要。