🤖

読みやすいコードの実践

2024/03/07に公開

読みやすいコードを書くために

プログラミングにおける「認知負荷(Cognitive Load)」とは、開発者がコードを理解するために必要な精神的労力を指します。昔から「リーダブルコード」は可読性を上げるためのプラクティスが載った名著であると評価され、最近では「プログラマー脳」、「Good Code, Bad Code」といった書籍が話題となり、この概念は広く知られるようになりました。本記事では、特に新人エンジニアや、まだこの概念に触れたことがない方々へ、読みやすく保守しやすいコードを書くためのアプローチをご紹介します。

認知負荷とは何か

認知心理学において、認知負荷は作業記憶(ワーキングメモリ)が使用される量を指します。

In cognitive psychology, cognitive load refers to the amount of working memory resources used.
出典: Wikipedia - Cognitive load

コードで言えば、読み手が理解するために必要な精神的な努力の大きさを意味します。分かりにくいコードは、読み手に無駄な時間を消費させ、バグの発生を引き起こしやすくします。

Java における具体例

JavadocはJavaのドキュメント生成ツールであるため、通常は付けることが推奨されています。しかし、今回は敢えて避け、コードの読みやすさだけで内容を理解してもらうための例を提示します。

次のコードは、初めてのプログラミング授業でよく目にするコードのひとつです。

public class Main {
    static double gou;

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            if (i % 2 == 0) {
                System.out.println("偶数です");
            } else {
                System.out.println("奇数です");
            }
        }
    }
}

このコードは、0から9までの整数について偶数か奇数かを判定し出力する処理です。このコードは単純明快であり、その直感性が読みやすさに繋がっています。

次のサンプルでは少し複雑さが増します。

Main.java
import java.util.HashMap;
import java.util.Map;

public class Main {
    static double gou;

    public static void main(String[] args) {

        Map<Integer, Integer> tempMap = new HashMap<>();
        tempMap.put(100, 2);
        tempMap.put(3000, 1);
        tempMap.put(50000, 2);

        calc(tempMap);

        System.out.printf("合計: %.2f", gou);

    }

    private static void calc(Map<Integer, Integer> arg) {
        for (Map.Entry<Integer, Integer> entry : arg.entrySet()) {
            if (entry.getValue() == 2) {
                gou += entry.getKey() * 1.1;
            } else {
                gou += entry.getKey() * 1.08;
            }
        }
    }
}

このコードは商品価格と税率を元に合計金額を計算しています。

先ほどと同じように値に対して条件によって異なる処理をするコードですが、このコードを見て何の処理をしているのか瞬時に判断できる人は少ないでしょう。

短いこのコードですら他人に読みづらさを感じさせることができます。

もしクオリティのコードが大規模なコードのそこら中に記載されていたらと考えると皆さんはそのコードの意味や障害の対応を素早くできるでしょうか。

答えは否です。

読みづらいポイントをまとめて見ましょう。

  1. tempMapMapのキーと値が何を表しているのかが不明瞭
  2. calcメソッド内のif文では、リテラルで21が何を意味しているのか不明瞭
  3. calcメソッドが何を計算するか不明瞭
  4. calcメソッドの計算結果がどこに保存されるか不明瞭

他にも「税率がリテラルで定義されているため、今後税金が変更した時に対応箇所がわかりづらい」など色々と思うことがあるでしょう。

改善観点

意味のある命名

コード内の各要素に明確で理解しやすい名称をつけることが重要です。これは特に変数やメソッドに当てはまります。

コードの変数名やメソッド名は基本的に英語が望ましいです。

今回の場合、合計を意味するgoutotal、一時的なMapを意味するtempMapは本来は商品のMapなのでitemMapなどが良いでしょう。

また独自の省略(goukeigouと書くこと)はしない方が良いです。特に日本語をローマ字で書いて母音を抜く記法をするエンジニアがたくさんいますが、昨今その手法にメリットはなく、可読性低下というデメリットの方が大きいため辞めましょう。

例外を作る場合でも、チームで統一性のあることが重要です。

適切なオブジェクト構造

データやロジックの関連性が高く、一緒に処理されることが多い場合は、クラスやオブジェクトを使ってデータをまとめると良いでしょう。

このようにすることで、コードの再利用性を高め、処理の内容を呼び出し元で気にする必要が減り、生産性向上につながります。

マジックナンバーを避ける

コード中で特定の数値が何を意味しているのか不明瞭な場合、それを明確な命名が付いた定数に置き換えましょう。

大抵の場合、マジックナンバーは他の個所でも利用するため、適切なオブジェクト構造で宣言しましょう。

改善

それぞれの視点からコードを修正していきましょう。

Javaの誤ったAPIの使い方を正す

まずは、Mapで管理することが不適切なので修正しましょう。
商品(Item)用のクラスを作ります。

Item.java
public class Item {
    private int price;
    private int taxCategory;

    // コンストラクタやGetter、Setterは省略
}

商品がひとつのオブジェクトになったので、利用するように変更しましょう。

Main.java
import java.util.ArrayList;
import java.util.List;

public class Main {
    static double gou;

    public static void main(String[] args) {
        List<Item> items = new ArrayList<>();
        Item apple = new Item();
        apple.setPrice(100);
        apple.setTaxCategory(2);
        items.add(apple);

        Item detergent = new Item();
        detergent.setPrice(3000);
        detergent.setTaxCategory(1);
        items.add(detergent);

        Item fish = new Item();
        fish.setPrice(50000);
        fish.setTaxCategory(2);
        items.add(fish);

        calc(items);

        System.out.printf("合計: %.2f", result);
    }
}

このコードでは、商品ごとに価格、軽減税率対象かなどがコードのみで掴み取りやすくなりました。
また、商品名や商品コードを追加する場合も容易になります。
このようにJavaの機能を適切な場所で適切に扱うことが、可読性、保守性を飛躍的に向上させます。
LombokBuilderを用いることで、さらに簡素にオブジェクトの宣言ができます。

適切なオブジェクト構造

次はメンバ変数です。
メンバ変数は本来、次の場合に用います。

  • 永続化
  • カプセル化の実現
  • 再利用を高める

つまり今回のような計算結果を返却するようなケースには不適切です。
このような場合はreturnで計算結果を返却するのが適切です。
変数は生存するスコープが狭いほど、認知負荷を低くすることができます。
同じ変数を使い回す(再代入を多用する)、宣言と実際に利用する場所が乖離しているなど、変数の使い方ひとつとっても認知負荷を高めてしまうので、気をつけましょう。

命名

calcメソッドは何が計算されるのか不明です。
このメソッドは合計するだけでなく、税の計算までしています。
そのため、メソッド名はcalculateTotalではなく、calculateTotalWithTaxが適切でしょう。
このように、メソッド名だけで何を行うかが理解できるような名前をつけることが重要です。

それらを踏まえたコードが次のとおりです。

Main.java
import java.util.ArrayList;
import java.util.List;

public class Main {
    private static final double TAX_RATE_NORMAL = 1.1;
    private static final double TAX_RATE_REDUCED = 1.08;

    public static void main(String[] args) {
        List<Item> items = new ArrayList<>();
        Item apple = new Item();
        apple.setPrice(100);
        apple.setTaxCategory(2);
        items.add(apple);

        Item detergent = new Item();
        detergent.setPrice(3000);
        detergent.setTaxCategory(1);
        items.add(detergent);

        Item fish = new Item();
        fish.setPrice(50000);
        fish.setTaxCategory(2);
        items.add(fish);

        double total = calculateTotalWithTax(items);
        System.out.printf("合計: %.2f", total);
    }

    private static double calculateTotalWithTax(List<Item> items) {
        double total = 0;
        for (Item item: items) {
            if (item.getTaxCategory() == 2) {
                total += item.getPrice() * TAX_RATE_REDUCED;
            } else {
                total += item.getPrice() * TAX_RATE_NORMAL;
            }
        }
        return total;
    }
}

初期に比べると、飛躍的に読みやすくなったと思います。
しかしこれでもまだ足りていません。

このコードの書き方は、よくプロジェクトで書いている人がいるレベルです。

  • 定数がprivateで宣言されているが、他のクラスでも利用しているため、別のクラスで定義した方が良い
  • リテラルの意味がわからない(マジックナンバー)

つまり、可読性を上げるというのはなるべく変数やメソッドに意味をしっかりとつけて、ドメインごとにまとめることが重要になります。

それらを踏まえたコードです。

TaxCategory.java

public enum TaxCategory {
    NORMAL(1.1),
    REDUCED(1.08);

    private final double rate;

    // コンストラクタやGetterは省略
}

ItemCalculator.java
import java.util.List;

public class ItemCalculator {

    public static double calculateTotalWithTax(List<Item> items) {
        double total = 0;
        for (Item item: items) {
            total += item.getPrice() * item.getTaxCategory().getRate();
        }
        return total;
    }

}

Main.java
import java.util.ArrayList;
import java.util.List;

public class Main {

    public static void main(String[] args) {
        List<Item> items = new ArrayList<>();
        Item apple = new Item();
        apple.setPrice(100);
        apple.setTaxCategory(TaxCategory.SPECIAL);
        items.add(apple);

        Item detergent = new Item();
        detergent.setPrice(3000);
        detergent.setTaxCategory(TaxCategory.NORMAL);
        items.add(detergent);

        Item fish = new Item();
        fish.setPrice(50000);
        fish.setTaxCategory(TaxCategory.SPECIAL);
        items.add(fish);

        double total = ItemCalculator.calculateTotalWithTax(items);
        System.out.printf("合計: %.2f", total);
    }


}

ItemCalculatorクラスに分けるかItemクラス内にメソッドを持つかはケースバイケースです。
ItemUtilsのようなクラスにする場合もあるでしょう。
このあたりは銀の弾丸がない(万能の解決策は存在しない)部分なので、今後のシステムの拡張性などを鑑みながら実装を行う必要があります。

まとめ

時には非常に独創的で読みにくいコードを見かけることがありますが、良くない例として学ぶことが重要です。

「動けばそれで良い」という考え方も存在しますが、修正が難しいコードは結果的に生産性も低下させるため、そのような習慣は避けるべきです。

コードは書くよりも読むことの方が多いため、可読性を高める努力はチーム全体の生産性を向上させます。他の人でもすぐに理解できるコードを書くことは、あなたのコードが長期間にわたって効果的に使われるための基盤となります。

株式会社ソルクシーズ(事業戦略室)

Discussion