品質管理

良いコードの書き方|可読性・保守性を高める12の原則とJavaの実例

良いコードの書き方|可読性・保守性を高める12の原則とJavaの実例

良いコードとは、他人が読んで意図をつかめ、こわがらずに直せて、しかも正しく動くコードです。動くだけのコードとの差は、書いた直後には見えません。半年後に仕様変更が入り、別の人がそのコードを開いたときに、修正にかかる時間とバグの数として表に出ます。この記事では、言語を問わず通用する12の原則を、2026年10月時点の最新LTSであるJava 25でコンパイルとテストが通るコード例とともに整理します。原則の多くはPython・JavaScript・Goでもそのまま使えます。後半では、原則を当てはめすぎてかえってコードが読みにくくなる失敗パターンと、AIが書いたコードを受け入れるときの見方も扱います。

まとめ:良いコードの書き方の要点

迷ったら「半年後の別の人が、このコードを読んで安全に直せるか」で判断します。この記事では、名前、分岐の浅さ、状態の少なさ、テストの有無の順に確認することを勧めます。

  • 読みやすさ:意図が伝わる名前をつけ、ガード節で分岐を浅くし、マジックナンバーは定数にして、コメントには「なぜ」を書く
  • 変更への強さ:変数のスコープを狭め、値はできるだけ不変にし、同じ知識は1か所に置き(DRY)、1つの単位には1つの責務だけを持たせ、継承より部品の組み合わせ(コンポジション)を選ぶ
  • 正しさ:入力はコードの入口で検証し、振る舞いはユニットテストで固定する
  • シンプルさ:今必要なものだけを書き(KISS・YAGNI)、速度の工夫は計測してから行う
  • 仕組み化:書式と典型的なバグはフォーマッタと静的解析に任せ、人のレビューは設計と意図に使う

原則は目的ではありません。DRYのための無理な共通化や、単一責任のための細切れの分割は、かえって読みにくいコードを生みます。どこで止めるかの目安は「原則を使いすぎて壊れる4つの失敗パターン」の章にまとめました。

良いコードの条件:可読性・保守性・正確性

良いコードの条件は、読める(可読性)、直せる(保守性)、正しく動く(正確性)の3つに整理できます。技術評論社の『良いコードを書く技術』の紹介記事も、保守性・効率・正確さ・無駄のなさの4条件を挙げており、どの整理でも中心にあるのは「人が読んで直せること」です。処理速度は要求される応答時間や処理量を基準に評価し、細かな高速化は計測で必要性を確かめてから行います。

可読性と保守性の違い

可読性は「読んで処理の意図と流れをすぐ追えるか」、保守性は「直したいときに少ない手間と小さな影響範囲で直せるか」を指します。可読性は保守性の前提ですが、同じではありません。読みやすくても、1つの修正のために10ファイルを触る必要があるコードは保守性が低いと言えます。

この区別は国際規格でもされています。ソフトウェア製品の品質モデルを定めるISO/IEC 25010:2023は、保守性を「意図した保守担当者が、有効かつ効率的に製品を修正できる能力」と定義し、モジュール性・再利用性・解析性・修正性・試験性の5つの副特性に分けています。可読性に最も近いのは、変更の影響を見積もり、直すべき箇所を特定しやすいかを表す解析性です。修正性は「欠陥を持ち込まず、既存の品質を落とさずに修正できるか」、試験性は「要件を満たすかを確かめるテストを設計して実行できるか」を指します。規格の上でも、保守性は読みやすさに加えて、部品の独立性やテストのしやすさまで含む性質です。なお2023年版は2011年版の改訂で、品質特性に安全性が加わり、使用性と移植性はそれぞれ相互作用能力(interaction capability)と柔軟性(flexibility)に置き換わりました。

読みやすさを決める書き方

コードは書く時間より読む時間のほうが長くなります。マーティン・ファウラーは1999年の著書『Refactoring』で「コンピューターが理解できるコードは誰にでも書ける。優れたプログラマーは人間が理解できるコードを書く」と述べています(原文は出版社の書籍ページに掲載)。読みやすさに効く書き方は、名前・分岐・数値とコメントの3か所に集中しています。

意図が伝わる命名

名前はコードのなかで最も多く読まれる情報です。calcやdata1のような名前は、何を計算し何を持っているかを読み手に推測させ、その推測の誤りがバグになります。

避けたい名前 意図が伝わる名前 理由
d, tmp, data1 elapsedDays, activeUsers 中身と単位が分かる
flag, check() isPaid, hasPermission() 真偽値は is・has・can で始める
manager, util OrderValidator, ShippingNotifier 何をするクラスかが名前で決まる

メソッド名は動詞で始めます。短さより明確さを優先しますが、数行のループ変数iのように範囲が狭く慣習が決まっているものは短くて構いません。言語ごとのキャメルケース・スネークケースの使い分けはプログラミングの命名規則まとめで確認できます。

ガード節による浅い分岐

条件分岐が入れ子になるほど、いまどの条件の内側にいるのかを読み手が覚えておく必要があります。対処はガード節です。処理しない条件を先にreturnで抜け、本来の処理を浅い位置に置きます。

// ネストが深い書き方
public void notifyShipped(Order order) {
    if (order != null) {
        if (order.isPaid()) {
            if (!order.items().isEmpty()) {
                mailer.send(order.customerEmail(), "ご注文の商品を発送しました");
            }
        }
    }
}

// ガード節で浅くした書き方(動作は同じ)
public void notifyShipped(Order order) {
    if (order == null) return;
    if (!order.isPaid()) return;
    if (order.items().isEmpty()) return;
    mailer.send(order.customerEmail(), "ご注文の商品を発送しました");
}

後者は「対象外を先に除き、残りが本処理」と上から順に読めます。2つの書き方に同じ4種類の注文(null・未入金・商品なし・正常)を渡すと、どちらも正常な注文の1件だけに通知します。ネストが3段を超えたら、ガード節か関数の切り出しを検討する合図です。

マジックナンバーの定数化と「なぜ」を書くコメント

コードに直接書かれた根拠の分からない数値(マジックナンバー)は、意味の推測と、変更漏れの両方を招きます。名前付きの定数にすれば、意味と変更箇所が1つに決まります。コメントには、コードを読めば分かる「何をしているか」ではなく、コードからは読み取れない「なぜそうしたか」を書きます。

public final class Pricing {
    private static final BigDecimal STANDARD_TAX_RATE = new BigDecimal("0.10");

    // 1円未満は切り捨てる。請求システムが切り捨てで計上しており、四捨五入にすると請求書と1円ずれるため
    public static long taxIncluded(long priceYen) {
        return BigDecimal.valueOf(priceYen)
                .multiply(BigDecimal.ONE.add(STANDARD_TAX_RATE))
                .setScale(0, RoundingMode.DOWN)
                .longValueExact();
    }
}

taxIncluded(999)は1,098.9円を切り捨てて1,098を返します。コメントが無ければ、次に読んだ人が「四捨五入の間違いでは」と直してしまうかもしれません。金額にdoubleではなくBigDecimalを使うのも、2進小数の誤差で1円ずれるのを防ぐためです。処理を日本語に訳しただけのコメントは、コードの変更時に直し忘れて嘘になりやすいので書きません。命名とコメントをさらに掘り下げた書籍の要点はリーダブルコードとは?読みやすいコードの4原則と書籍のエッセンスにまとめています。

変更に強くする構造

保守性を決めるのは、1つの変更がどこまで波及するかです。状態を持つ場所を減らし、知識を1か所に集め、部品同士のつながりを緩くすると、修正は狭い範囲に閉じます。

狭いスコープと不変の値

変数は使う直前で宣言し、使い終わったら見えなくなる範囲に置きます。スコープが広いほど、その変数がどこで書き換えられるかを追う範囲が広がるからです。さらに一歩進めて、一度作った値を書き換えられないようにすると、「いつの間にか値が変わっていた」というバグが構造的に起きなくなります。Javaでは、Java 16で正式機能になったrecord(JEP 395)を使うと、次の例のようにプリミティブ型の値を持つ不変オブジェクトを短く書けます。ただし、recordが保証するのはフィールドの再代入禁止であり、参照先のオブジェクトまで自動的に不変になるわけではありません。

public record Money(long yen) {
    public Money {
        if (yen < 0) {
            throw new IllegalArgumentException("金額は0以上: " + yen);
        }
    }

    public Money add(Money other) {
        return new Money(Math.addExact(yen, other.yen));
    }
}

addは自分を書き換えず、新しいMoneyを返します。Math.addExactは桁あふれのときに黙って負の値にならず、ArithmeticExceptionで止まります。リストを受け取る場合は、コンストラクタでList.copyOfを通しておくと、呼び出し元が後から元のリストに要素を足しても、コピー先の要素構成は変わりません。ただし、要素自体が可変オブジェクトなら、その内部状態の変更は防げません。

DRYによる同じ知識の一元化

DRY(Don't Repeat Yourself)は、同じ知識をコードの複数箇所に書かない原則です。税率を3か所に直書きすると、税率が変わったときに1か所だけ直し忘れます。前の例のように税率をSTANDARD_TAX_RATEに集めておけば、変更は1行で済みます。

注意したいのは、DRYの対象が「同じ形のコード」ではなく「同じ知識」である点です。会員登録と問い合わせフォームで、メールアドレスの検証がたまたま同じ3行だったとしても、片方だけ要件が変わる可能性があるなら別々のままにしておきます。共通化するか迷ったら、同じ処理が3回目に現れた時点で検討する、という目安(Rule of Three)が実務ではよく使われます。2回目の時点では、本当に同じ知識なのか、偶然似ているだけなのかがまだ判断できないことが多いからです。

単一責任の原則による変更理由の分離

単一責任の原則(SRP)は、モジュールを変更する理由を1つに絞る考え方です。同じ立場からの要求で一緒に変わる処理をまとめ、異なる理由で変わる処理を分けます。データベースへの保存、金額の計算、メールの送信を1つのクラスに入れると、メール文面の変更のために計算処理まで触ることになり、関係ない機能を壊す危険が生まれます。メソッドが画面に収まらない長さになったら、意味のまとまりごとに小さなメソッドへ切り出し(Extract Method)、名前でそのまとまりの意図を語らせます。分け方と、分けすぎの見極めはSRP(単一責任の原則)とは?意味と具体例、SRP違反・やりすぎ(過剰分割)の見極め方で詳しく扱っています。

継承とコンポジションの使い分け

継承は親クラスの変更が子クラスへそのまま波及し、クラス同士が強く結びつきます。多くの場面では、必要な部品をフィールドとして持たせるコンポジションのほうが変更に強くなります。部品をインターフェースで受け取ると、差し替えも簡単です。

public interface Mailer {
    void send(String to, String body);
}

public final class ShippingNotifier {
    private final Mailer mailer;

    public ShippingNotifier(Mailer mailer) {
        this.mailer = mailer;
    }
    // notifyShipped は前述のガード節版
}

ShippingNotifierは、実際のメール送信の仕組みを知りません。本番ではSMTPで送る実装を、テストでは送信先を記録するだけの実装を渡せます。「AはBの一種」と言い切れるときだけ継承を使い、「AはBを持つ」ならコンポジションを選ぶ、という基準で迷いが減ります。

正しく動かすための書き方

読みやすく直しやすいコードでも、間違った値を黙って受け入れたり、直した拍子に別の箇所が壊れたりするなら良いコードとは言えません。正しさを支えるのは、入口での検証と、振る舞いを固定するテストです。

防御的プログラミングによる入口での検証

「正しい値しか来ないはず」と決めつけず、不正な値は受け付けた時点で止めます。先のMoneyは、コンストラクタで負の金額を拒否しています。こうしておくと、マイナスの金額がシステムの奥まで流れて、請求処理の段階で初めておかしな値に気づく、という事態を防げます。検証は、外部から値が入ってくる境界(画面の入力、APIのリクエスト、ファイルの読み込み)と、値オブジェクトの生成時に置きます。内部のすべてのメソッドで同じ検証を繰り返すと、今度はコードが検証だらけで読めなくなります。

ユニットテストによる振る舞いの検証

テストは、コードの振る舞いを「こう動くはず」という形で書き残したものです。テストを実行すると、リファクタリングや仕様変更のあとも、テストで確認している振る舞いが保たれているかを確かめられます。以下はJUnit 6.1.3を使ったテストの抜粋です(import文は省略)。テスト対象のOrderは次のrecordで、ShippingNotifierには前述のガード節版のnotifyShippedを組み込んでいます。

public record Order(String customerEmail, boolean isPaid, List<String> items) {
    public Order {
        items = List.copyOf(items);
    }
}
class MoneyTest {
    @Test
    void 負の金額は作れない() {
        assertThrows(IllegalArgumentException.class, () -> new Money(-1));
    }

    @Test
    void 加算しても元の値は変わらない() {
        Money price = new Money(100);
        Money total = price.add(new Money(50));
        assertEquals(150, total.yen());
        assertEquals(100, price.yen());
    }
}

class ShippingNotifierTest {
    @Test
    void 未入金の注文には通知しない() {
        List<String> sent = new ArrayList<>();
        ShippingNotifier notifier = new ShippingNotifier((to, body) -> sent.add(to));

        notifier.notifyShipped(new Order("[email protected]", false, List.of("book")));
        notifier.notifyShipped(new Order("[email protected]", true, List.of("book")));

        assertEquals(List.of("[email protected]"), sent);
    }
}

ソースをsrcに置き、JUnitのコンソールランチャー(junit-platform-console-standalone-6.1.3.jar)を同じ場所に用意すれば、次の2行でコンパイルとテストを実行でき、3件とも成功します。

javac -d out -cp junit-platform-console-standalone-6.1.3.jar src/*.java
java -jar junit-platform-console-standalone-6.1.3.jar execute -cp out --select-class MoneyTest --select-class ShippingNotifierTest

ShippingNotifierTestでは、メール送信の部品をラムダ式で差し替えています。コンポジションで部品を外から渡す設計にしておいたので、実際にメールを送らずに通知の条件だけを確かめられます。テストが書きにくいと感じたら、それは責務が混ざっているか、部品が直接newされていて差し替えられないサインです。テストメソッド名は、何を保証しているかが一読で分かる文にします(Javaでは日本語の識別子も使えます)。

シンプルさを保つ:KISS・YAGNIと最適化の順番

KISS(Keep It Simple, Stupid)は「動く範囲で最も単純な形を選ぶ」、YAGNI(You Aren't Gonna Need It)は「今必要でない機能は作らない」という原則です。「将来ほかの決済手段も増えるかもしれない」と抽象クラスと設定ファイルを先に用意しても、その将来が来なければ、読み手が理解すべき部品が増えただけになります。必要になった時点で足すほうが、そのときの要件に合った形で作れます。先回り実装が高くつく理由はYAGNI(ヤグニ)原則とは?意味・読み方と先回り実装の4つのコストで整理しています。

処理速度も同じ考え方で扱います。ドナルド・クヌースは1974年の論文「Structured Programming with go to Statements」で、小さな効率改善は97%ほどの場合は忘れるべきで、早すぎる最適化は諸悪の根源だと書き、同時に残りの重要な3%の機会は逃すべきでないとも述べています。効率を気にすること自体は正しく、ループの中で毎回データベースを呼ぶような、明らかに計算量が大きい書き方は最初から避けます。一方で、読みやすさを削ってまで行う細かな高速化は、プロファイラで遅い箇所を測ってからにします。原典の文脈はPremature optimization is the root of all evil(早すぎる最適化は諸悪の根源)の意味と原典で解説しています。

静的解析による規約違反と潜在バグの検出

インデント、命名規則の違反、未使用の変数、null参照の見落としのような問題の多くは、静的解析で自動検出できます。ただし検出漏れや誤検出もあるため、人のレビューやテストと併用します。Javaでよく使われるツールと、2026年10月時点の最新版(Maven Centralの登録値)は次のとおりです。

ツール 主に見るもの 最新版
Checkstyle 命名・書式など規約への準拠 14.3.0
PMD 未使用コード・複雑すぎるメソッド 7.28.0
SpotBugs バイトコードから潜在バグを検出 4.10.5
SonarQube 独自解析による問題検出・品質の可視化 製品版(別途)

以前はFindBugsが定番でしたが、最終版は2015年3月の3.0.1で、FindBugsの公式リポジトリは開発の継続先としてSpotBugsを案内しています。古い記事や社内の手順書でFindBugsを見かけたら、SpotBugsに置き換えてください。

これらをCI(継続的インテグレーション)に組み込み、プルリクエストごとに自動で走らせると、規約違反の指摘が人によってぶれず、レビューで書式の話をする時間が消えます。整形はフォーマッタに任せれば、空白やかっこの位置で議論することもなくなります。SonarQubeで品質の基準(品質ゲート)を決める手順はSonarQube(ソナーキューブ)とは?で扱っています。人のレビューは、名前が意図を表しているか、責務の分け方が妥当か、といった機械に判断できない点に使います。

原則を使いすぎて壊れる4つの失敗パターン

ここまでの原則は、どれも当てはめすぎると逆効果になります。原則に従った結果として読みにくくなったなら、原則より読みやすさを優先して構いません。原則を当てはめすぎた失敗例を、次の4つに整理します。

  • DRYの過剰共通化:形が似ているだけの処理を1つの関数にまとめ、呼び出し元ごとの違いを引数のフラグで切り替えるようになったら失敗です。process(data, true, false, null)のような呼び出しは、中を読まないと何が起きるか分かりません。フラグの組み合わせを追わないと処理の目的が分からなくなったら、共通化を見直して別々の関数に分けます。
  • 単一責任の細切れ:3行ずつのクラスが20個並び、1つの処理を追うのにファイルを10回移動するなら分けすぎです。一緒に変更されるものは一緒に置きます。
  • 先回りの抽象化:実装が1つしかないのにインターフェース、ファクトリ、設定クラスをそろえる書き方です。部品の差し替えが必要になる、またはテストで差し替えたい、という具体的な理由ができてから抽象化します。前章のMailerは、テストで差し替える理由があるので切り出しています。
  • コメントの全面禁止:「良いコードにコメントは不要」を文字どおりに受け取り、業務上の制約や、あえて選んだ実装の理由まで消してしまう失敗です。「何を」はコードで、「なぜ」はコメントで書く、という分担を崩さないでください。

判断に迷ったときの基準は、この記事の冒頭と同じです。半年後の別の人が読んで、安全に直せるか。原則はその問いに答えるための道具で、満たすべきチェックリストではありません。既存のコードを段階的に整えていく進め方はリファクタリングとは?24の観点とタイミング・進め方で解説しています。

AIが書いたコードを受け入れるときの見方

生成AIがコードを書く場面が増えても、そのコードを読んで正しさを判断し、後から直すのは人です。むしろ、書く速さが上がった分だけ、読む量と直す量が増えています。

傾向は数字にも出ています。GitClearの2025年版の調査は、2020〜2024年に変更された2億1,100万行を分類し、既存のコードを別の場所へ移す「移動」の割合が24.1%(2020年)から9.5%(2024年)に減り、同じ行の「コピー&ペースト」が8.3%から12.3%に増えたと報告しています。コピペが移動を上回ったのは、同社の計測で2024年が初めてです。移動はリファクタリングの痕跡なので、既存のコードを整理して使い回すより、新しく書き足す変更が増えたことを示します。GoogleのDORA(DevOps Research and Assessment)が2024年の調査をまとめた報告書でも、AIの導入度が25%高いことと、デリバリーの安定性が推定7.2%、スループットが1.5%低いこととの関連が報告されています。この関連だけで因果関係は断定できません。

AIの生成物をレビューするときは、この記事の原則をそのまま点検項目に使えます。既存の関数で済む処理を新しく書いていないか(DRY)、1つの関数に複数の責務を詰め込んでいないか、入力の検証が抜けていないか、テストがあるか。とくに重複は、AIが周辺のコードを読まずにその場で書き下ろすと起きやすく、静的解析の重複検出(PMDのCPDなど)で機械的に拾えます。読みにくいコードはAIが書いても人が検証できず、責務が混ざったコードはAI自身も次の修正で文脈を取り違えます。AIに任せる範囲が広がるほど、良いコードの基準を持っている人の価値は上がります。

よくある質問

良いコードとは一言で言うと何ですか?

他人が読んで意図をつかめ、安全に直せて、正しく動くコードです。正しく動くことは出発点にすぎず、読みやすさ(可読性)と直しやすさ(保守性)を備えて初めて良いコードと呼べます。

可読性と保守性はどう違いますか?

可読性は読んで理解できるか、保守性は少ない手間と小さな影響範囲で変更できるかです。保守性は可読性を含むより広い性質で、ISO/IEC 25010では解析性・修正性・試験性などの副特性に分けて定義されています。

Javaで良いコードを書くための最初の一歩は?

名前を丁寧につけることと、CheckstyleやSpotBugsを導入して規約違反と潜在バグを自動で拾える環境を作ることです。不変の値を手軽に作れるrecordも、Java 16以降なら使えます。文字列比較やループなど、Java固有の書き方はJavaのコード例で学ぶ書き方の基本にまとめています。

保守性の高いコードを書くには何から始めればよいですか?

変更されやすい箇所にユニットテストを書くことから始めます。テストがあれば、名前の変更や関数の切り出しを、壊していないと確かめながら進められます。テストの無いまま大きく書き換えると、読みやすくなった代わりにバグが入る、という結果になりがちです。

良いコードが書けるようになるには?

他人のコードを読む量を増やし、自分のコードをレビューしてもらう機会を作るのが近道です。原則を覚えるより、指摘を受けて直す往復のなかで身につきます。静的解析ツールの指摘も、毎回無料で受けられるレビューとして使えます。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.27 テックブログ Brotli 脆弱性:CVE-2026-111497の影響確認とNode.js・Python・.NETの更新手順
  2. 2026.10.28 テックブログ リンツ不正アクセス:314万件の対象確認とパスワード強制リセット後の対処
  3. 2026.10.27 テックブログ Joshin(ジョーシン)不正アクセス:最大600万件の対象とサブスク初期パスワードの対処
  4. 2025.02.27 コラム トラッキングビーコン(Webビーコン)とは?仕組み・用途・確認と対策を徹底解説
  5. 2026.10.23 テックブログ Renta!はいつ再開?不正アクセスでメールアドレス等が流出したおそれ・再開時のパスワード再設定と運営側の対応

RELATED POSTS 関連記事

目次