🐥

コードの複雑さを可視化して可読性を上げる方法

に公開

コードを読んでいるときに「なんかよく分からんが複雑でわかりにくいな...」と感じることはありませんか?
私は既存のコードを読んでいるときはもちろん、自分が書いたコードを読むときもそう感じることがあります。
複雑さの要因を理解していないと、適切な改善ができませんよね。
今回は、「脳に収まるコードの書き方」という書籍を参考に、コードの複雑さの可視化とその複雑度を軽減させる方法を解説していきます。

前提

当たり前ですが、コードは書く回数よりも読まれる回数の方が多いです。
コードの価値には、アプリケーションが動くことだけではなく可読性も大きく関係しています。
目先のリリースを優先して「とりあえず動くコード」を許してしまうと、将来、他の誰かあるいは未来の自分がそのコードを読むときに必ず苦しむことになります。
可読性を二の次にせず、「脳に収まるコード」のための最適化を常に検討しましょう。

コードの複雑さを可視化しよう

書籍によると、人間の短期記憶はわずか7つのことしか記憶できないようです。
複雑な仕様のコードを追っていると、、必要な前提知識がどんどん増えていき、読み終わったときには何も頭に入っていない、あるいは、読んでいる途中に何度も元のファイルを読み返す、といった経験はありませんか?
これは人間の「目に見えるものが8つ以上になると脳に収まらなくなる」という性質によって起きているんです。
裏を返せば、目に見えるものを7つ以内に収めれば、「脳に収まるコード」を実現できるわけです。

では目に見えるものを7つ以内に収めるにはどうすれば良いのでしょうか。
書籍に書かれていた、複雑度を計る指標をいくつか紹介します。

サイクロマティック複雑度

サイクロマティック複雑度の算出方法は、1からスタートし、そこに分岐(if文、else if、else、switch、三項演算子など)とループ(for、while、foreachなど)の数を加算していくものです。
例えば以下のコードだとサイクロマティック複雑度は7になります。
スタートの1からif文の個数5と三項演算子の個数1を足して、7です。

class ReservationController
{
    public function post(?ReservationDto $reservation_dto): void
    {
        if ($reservation_dto === null) {
            throw new Exception('DTOがnullです。');
        }
        if ($reservation_dto->resevation_at === null) {
            throw new Exception('予約日時が指定されていません。');
        }
        if (Carbon::parse($reservation_dto->resevation_at)->gt(Carbon::now())) {
            throw new Exception('予約日時は現在日時より未来でなければなりません。');
        }
        if ($reservation_dto->email === null) {
            throw new Exception('メールアドレスが指定されていません。');
        }
        if ($reservation_dto->quontity <= 0) {
            throw new Exception('人数は1以上でなければなりません。');
        }
    
        $reservation = new Reservation($reservation_dto->email, $dto->name ?? "", $reservation_dto->quontity, $reservation_dto->resevation_at);
        $this->reservation_repository->create($reservation);
    }
}

変数の数

変数の数も複雑さを表現する重要な指標です。
ここでの変数は、ローカル変数、メソッド引数、クラスフィールドすべて含みます。
変数名が7つを超える場合、そのコードは多くのことをやりすぎている可能性が高いと言われています。

その他

コードの行数、幅も複雑さを表現する指標として一般的に扱われています。
コードの幅は80文字以内に抑えることが適切と言われているようです。
コードの行数の明確な閾値は記載ありませんでしたが、多ければ多いほど複雑になるのは明確です。

コードの複雑度を7に抑えよう

ここまで、ひとつのコードの複雑度を7以下に抑えることの重要性を話しました。
上記のpost()メソッドのコード例を7つの要素で表現したものが以下の図です。
7つの六角形が綺麗に埋まっていますね。
下記のような図を書籍では「ヘックスフラワー」と呼んでいました。
ひとつのコード(メソッドやクラス)をヘックスフワラーに置き換えたとき、構成要素が7つの六角形に収まっていないといけないわけです。

では、もしpost()のサイクロマティック複雑度が8以上になった場合はどうするのでしょうか?
ヘックスフラワーの各要素はすでに埋まってしまっています。
その場合は、新しい別のコード(メソッドやクラス)に分割していきます。
そして、その分割したコードもサイクロマティック複雑度が7以下になるように作成します。

ヘックスフラワーのひとつの要素から、またさらにヘックスフラワーを作成するーーこのように、コードのうちのひとつの要素にズームインしても、その中の要素も7つ以下になっている状態が望ましいとされています。
書籍ではこの状態をフラクタルアーキテクチャーと呼んでいます。

ちなみに常に7つの要素が埋まっている必要はなく、7つ以下に収まっていれば問題ありません。

複雑なコードを適切に分割する

複雑さを一定に保つために、闇雲にコードを分割していけば良いわけではありません。
フラクタルアーキテクチャで重要なのは、ヘックスフラワーのひとつの要素にズームインしたときに、外側のヘックスフラワーの内容を意識する必要がないという点です。
つまり「見えているものが全て」なのです。
コード分割を誤ると、外側のヘックスフラワー(コードの呼び出し元)や内側のヘックスフラワー(呼び出した先のコード)の仕様も意識しないと理解できないコード、つまり脳に収まらないコードになってしまいます。

コードを分割する上で大事なポイントを以下にあげます。

凝集

コードを分割する際、同じ関心ごとを持つコードをまとめるようにしましょう。
ケント・ベックは凝集度の原則を「同じ速度で変化するものは一緒にする。違う速度で変化するものは分ける。」と説明しています。
これは、将来の変更を予測し、その変更が影響する範囲を限定するために非常に重要な考え方です。

コードの凝集度を考える際に、クラスフィールドの使われ方に目を向けると良いです。

  • 凝集度が高い状態:すべてのメソッドにおいてすべてのクラスフィールドが使用されている場合
  • 凝集度が低い状態:それぞれのメソッドで別々のクラスフィールドが使用されている場合

コード分割後にクラスフィールドを一つも使わないメソッドができた場合、そのメソッドは別のクラスに移した方が良い可能性が高くなります。
staticメソッドを作成した場合も、本当にstaticであるべきかを再度考え直しましょう。

カプセル化

カプセル化とは、「クラスのプロパティをprivateに閉じて、ゲッターセッターを通して値を取得更新できるようにすること」、がすべてではありません。
カプセル化することによって「オブジェクトが常に有効であることを保証する」ということが重要です。

コード例の$reservation_dtoには、プロパティがNULLでないか?日付は未来日か?などのルールがあります。
このオブジェクトを使用するメソッドでは、毎回このルールに反していないかをチェックをしなければならない状態になっています。
この場合、ひとつでもif文を書き忘れるとデータ不整合が発生してしまいます。
そうならないように、オブジェクトを呼び出したときに必ずバリデーションチェックを通った状態にしておく(バリデーションチェックされているか?を意識させない)のが、カプセル化の目的です。
オブジェクトが有効であるかどうかは、呼び出し側の責任ではなくインスタンス自身で保証する必要があるのです。

実践

上記のポイントをおさえつつ、例のコードを分割していきます。
$reservation_dtoのプロパティに関するif文は、切り出せそうです。

class ReservationController
{
    public function post(?ReservationDto $reservation_dto): void
    {
        if ($reservation_dto === null) {
            throw new Exception('DTOがnullです。');
        }
        
+       $this->checkReservationDto($reservation_dto);
    
        $reservation = new Reservation($reservation_dto->email, $dto->name ?? "", $reservation_dto->quontity, $reservation_dto->resevation_at);
        $this->reservation_repository->create($reservation);
    }
        
    private function checkReservationDto(ReservationDto $reservation_dto): void
    {
        if ($reservation_dto->resevation_at === null) {
            throw new Exception('予約日時が指定されていません。');
        }
        if (Carbon::parse($reservation_dto->resevation_at)->gt(Carbon::now())) {
            throw new Exception('予約日時は現在日時より未来でなければなりません。');
        }
        if ($reservation_dto->email === null) {
            throw new Exception('メールアドレスが指定されていません。');
        }
        if ($reservation_dto->quontity <= 0) {
            throw new Exception('人数は1以上でなければなりません。');
        }
    }
}

上記のようにメソッドを切り出してみました。
post()のサイクロマティック複雑度は3に減らすことができました!
ただ、checkReservationDto()を見てみると、ひとつもクラスフィールドを使用していないことがわかります。
$reservation_dtoのバリデーションチェックを担っているので、ReservationDtoクラスに移動させます。

class ReservationController
{
    public function post(?ReservationDto $reservation_dto)
    {
        if ($reservation_dto === null) {
            throw new Exception('DTOがnullです。');
        }

        $reservation = new Reservation($reservation_dto->email, $dto->name ?? "", $reservation_dto->quontity, $reservation_dto->resevation_at);
        $this->reservation_repository->create($reservation);
    }
}

class ReservationDto
{
    public function __construct(
        private Carbon $resevation_at,
        private string $email,
        private int $quontity,
        private string $name
    )
    {
        if (Carbon::parse($this->resevation_at)->gt(Carbon::now())) {
            throw new Exception('予約日時は現在日時より未来でなければなりません。');
        }
        if ($this->quontity <= 0) {
            throw new Exception('人数は1以上でなければなりません。');
        }
    }
}

これで、呼び出し元であるpost()でReservationDtoのバリデーションチェックを意識する必要がなくなりました!

まとめ

コードは脳に収まるように書きましょう。
脳に収めるには、コードに含まれる要素を7つまでに抑えることが重要です。
目に見えているコードだけで理解できるよう、適切に分割していきましょう。

感想

私は「脳に収まるコード」という書籍を読んで、コードの複雑さに対する解像度が上がったように思えます。
書籍には他にも実践に役立つノウハウが書かれているのでぜひ読んでみてください。

コードレビューに出す前に、自分のコードが脳に収まるコードになっているか確認してみてね!

参考

  • Seemann, M., 吉羽 龍太郎, 原田 騎郎, Martin, R. C. (2024). 『脳に収まるコードの書き方 ―複雑さを避け持続可能にするための経験則とテクニック』. オライリー・ジャパン.
レバテック開発部

Discussion