👋

脳に収まるコードを書くために

に公開

はじめに

最近この本を読んだので、この本に出てくる内容の一部を少し深掘りしてみます。

脳に収まるコードの書き方―複雑さを避け持続可能にするための経験則とテクニック
Code That Fits in Your Head: Heuristics for Software Engineering, 1st edition
脳に収まるコードの書き方 - O'Reilly Japan

機能追加や機能改修、不具合修正などでプログラムを修正している時に、

  • 修正のし易いコード
  • 修正のし難いコード

があるのを感じると思います。
では、この2つの違いはなんでしょうか?

この違いの1つには

脳に収まる複雑さのコードであるかどうか

があげられます。

では、脳に収まるコードはどうすればかけるようになるのか?

脳に収まるコードを定量的に計測する

  • 脳に収まるコードの書き方の書籍で紹介されているのがサイクロマティック複雑度です

    • これはメソッド内のif文、while文、for文などの条件分岐やループを数えた数値 + 1をして計算することができます。
  • 例:条件式2つの場合 +1してサイクロマティック複雑度は3になる

  • 図を見ると最終的に分岐した処理の数も3になっている

  • サイクロマティック複雑度が高ければ把握しておかなければならない処理も多くて、理解しにくくなりやすく、必要なテストコードの量も大きくなるというのがわかります。

実際のコードでサイクロマティック複雑度を計測してみる

  • Androidアプリの開発を普段しているので下記のツールで計測してみました。
  • 他の環境でもサイクロマティック複雑度を計測する機能を持ったツールが何かしらあると思うので探してみて下さい。
  • 静的解析ツールのDeteKtを使用
    • detekt/detekt: Static code analysis for Kotlin
    • CyclomaticComplexMethodのthresholdの設定を変えることで、任意のサイクロマティック複雑度のメソッドを見つけられる
    • ./gradlew detekt を実行すると結果がでる
  • 最も複雑度の高かったメソッドで 146
  • 複雑度が高いのにテストコードが少ない箇所があれば、それは危ないコードかもしれない

サイクロマティック複雑度を下げるには

  • 処理の塊ごとにメソッドの分割ができないか
  • 早期リターンなど条件分岐の単純化ができないか
  • ポリモーフィズムを利用して条件分岐自体を排除できないか
  • テーブル駆動方式(Table-Driven Method)を適用して条件分岐自体を排除できないか
    • Code Complete 第2版 上 ― 完全なプログラミングを目指して
    • 第18章 テーブル駆動方式を参照
  • デザインパターンを適用して条件分岐自体を排除できないか
カテゴリ デザインパターン名 評価 (Generated by Gemini) 考察(どのように貢献するか、あるいは、なぜ関係しないか)(Generated by Gemini)
生成 Abstract Factory どの具象ファクトリ(製品群)を利用するかを決定する if 文を、ファクトリ自体の差し替えに置き換える。
生成 Builder 複雑なオブジェクトの生成手順を隠蔽する。生成ロジック内の if 文は減るが、クライアント側の分岐をなくすのが主目的ではない。
生成 Factory Method どの具象クラスをインスタンス化するかの if/switch 文を、サブクラスのオーバーライド(ポリモーフィズム)に置き換える。
生成 Prototype new するクラスを if 文で選択する代わりに、既存のインスタンスをコピーすることでオブジェクトを生成する。
生成 Singleton × インスタンスが一つであることを保証するパターン。クライアント側でのインスタンス選択の分岐がないため、関連性は低い。
構造 Adapter インターフェースの異なるクラスをラップする。if (obj is ITypeA) のような型チェック分岐をせず、統一的なインターフェースで扱えるようにする。
構造 Bridge 「抽象」と「実装」を分離する。クライアントは実装の違いを意識せず抽象を扱えるため、実装の種類による if 文を排除できる。
構造 Composite 「個」と「全体」を同一視する。if (node is Leaf) のような葉か枝かの分岐をクライアントコードからなくすことができる。
構造 Decorator 機能の有無や組み合わせを if 文で判定する代わりに、オブジェクトを動的にデコレート(装飾)することで機能を追加する。
構造 Facade 複雑なサブシステムへの窓口を提供する。クライアントのコードはシンプルになるが、if 文はFacadeの内部にカプセル化される形になる。
構造 Flyweight × オブジェクトを共有してメモリを節約するのが目的。条件分岐の排除とは直接関係しない。
構造 Proxy アクセス制御(例:権限チェック)の if 文をProxy内にカプセル化できるが、クライアント側の分岐排除が主目的ではない。
振る舞い Chain of Responsibility 誰がリクエストを処理するかの if-else if 文を、オブジェクトの連鎖(チェーン)に置き換える。
振る舞い Command 実行する処理の内容を if/switch 文で分岐させる代わりに、Commandオブジェクトそのものを差し替えることで実現する。
振る舞い Interpreter 構文規則に応じた処理の分岐を、各規則を表すクラスのポリモーフィズムによって実現する。
振る舞い Iterator コンテナ(配列、リスト等)の種類によるループ処理の if 文をなくし、統一的な方法で要素にアクセスできるようにする。
振る舞い Mediator オブジェクト間の複雑な相互作用のルール(誰から誰への通知か等)を if 文で記述する代わりに、Mediatorが一元管理する。
振る舞い Memento × オブジェクトの状態を保存・復元するのが目的。条件分岐の排除とは直接関係しない。
振る舞い Observer 状態変化を通知すべき相手を if 文で管理するのではなく、動的な購読・解除の仕組みに置き換える。
振る舞い State オブジェクトの「状態」に基づく振る舞いの if/switch 文を、状態を表すクラスのポリモーフィズムに置き換える。
振る舞い Strategy どの「アルゴリズム(戦略)」を利用するかを if/switch 文で選択する代わりに、アルゴリズムを表すクラスを差し替える。
振る舞い Template Method アルゴリズム内の一部のステップの違いを if 文で分岐させる代わりに、サブクラスでのオーバーライドに置き換える。
振る舞い Visitor データ構造に対する操作の種類や、要素の型による instanceof の分岐を、ダブルディスパッチを利用したポリモーフィズムに置き換える。

まとめ

サイクロマティック複雑度を通して、脳に収まるコードをどうすればかけるようになるかを説明しました。

オブジェクト指向のポリモーフィズムや、デザインパターンは勉強して知っていてもなかなか いつ使えばいいのか?となって 使い所の判断が難しいものです。
しかし、サイクロマティック複雑度を下げるための道具として考えると使い所がわかりやすくなったのではないでしょうか?

テーブル駆動方式が適用できる場合など、かならずしもサイクロマティクス複雑度が大きいメソッドが理解しにくいメソッドであるとは限りませんが、怪しいコードを探す手がかかりとして利用してみると良さそうです。

Ref

脳に収まるコードの書き方―複雑さを避け持続可能にするための経験則とテクニック
Code That Fits in Your Head: Heuristics for Software Engineering, 1st edition
脳に収まるコードの書き方 - O'Reilly Japan

株式会社ソニックムーブ

Discussion