1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Javaの良いコード・悪いコード #6】100行メソッドを撃退する「メソッド分割」の3原則

1
Posted at

はじめに

株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。

Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
コーポレートサイト

この記事は、新人〜2年目のJavaエンジニア向けに 「良いコードと悪いコードの違い」 を、現場でよく見る具体例とともに解説していくシリーズの第6回です。

テーマ
#1 命名
#2 コメントの書き方
#3 マジックナンバー・定数化
#4 Null処理
#5 早期リターン
#6(本記事) メソッド分割
#7 ループ処理
#8 例外処理
#9 ログ出力
#10 クラス設計

第6回は メソッド分割 です。現場では「画面1つ動かすメソッドが200行」「1つの if の中身が80行」のようなコードが量産されています。読めない・テストできない・直せない という3拍子を生む元凶です。


この記事のゴール

この記事を読み終わると、以下ができるようになります。

  • 「100行を超えるメソッド」がなぜダメか説明できる
  • 1メソッド1責務を意識して分割できる
  • メソッドの抽象度を揃えられる
  • メソッド名から意図が伝わる粒度に分割できる

「長すぎるメソッド」の本当のコスト

新人〜2年目のコードによく見られるのが、こんな構造です。

public String processOrder(Order order) {
    // 1. バリデーション(10行)
    if (order == null) return "注文がありません";
    if (order.getEmail() == null || order.getEmail().isBlank()) return "メールが不正";
    if (order.getItems() == null || order.getItems().isEmpty()) return "明細が空";

    // 2. 合計金額の計算(10行)
    int totalPrice = 0;
    for (OrderItem item : order.getItems()) {
        totalPrice += item.getUnitPrice() * item.getQuantity();
    }

    // 3. 税込計算(5行)
    int finalPrice = (int) (totalPrice * 1.10);

    // 4. メール送信(30行)
    String body = "ご注文ありがとうございます。\n";
    body += "合計: " + finalPrice + "円\n";
    // ... メール本文の組み立てが延々と続く
    mailService.send(order.getEmail(), "注文確認", body);

    // 5. DB保存(20行)
    Order saved = new Order(...);
    orderRepository.save(saved);
    // ...

    return "注文完了: 合計" + finalPrice + "円";
}

書いた本人は「やることを順番に書いただけ」のつもりです。
ですが、このメソッドには 5つの全く違う責務 が同居しています。

長すぎるメソッドには、3つの大きなコストがあります。

  1. 読解コスト:「いま何の処理を見ているか」を頭の中で把握し続ける必要がある
  2. テストコスト:1つの責務だけテストしたくても、全体のセットアップが必要
  3. 変更コスト:1箇所の修正が、関係ない処理に影響していないか確認する必要

特に厄介なのが テストコスト です。
「税込計算の境界値テストを書きたい」と思っても、注文オブジェクト・メール送信モック・DBモック…と全てをセットアップしないとテストできません。テストを書くこと自体が高コスト になり、結果として テストが書かれない という事態を招きます。


押さえるべきは3原則

新人〜2年目がまず身につけるべきメソッド分割の原則は、以下の3つです。

  1. 1つのことだけをするメソッドにする
  2. 抽象度を揃える
  3. メソッド名で意図を表現する

順番に見ていきます。


原則① 1つのことだけをするメソッドにする

メソッドは 「1つのことだけ」 をするのが基本です。これは「単一責任原則(Single Responsibility Principle)」と呼ばれる、ソフトウェア設計の最も重要なルールの1つです。

「1つのこと」の判断基準

「このメソッドは何をしますか?」と聞かれたとき、答えに 「〜と」「〜および」「〜してから〜する」 が含まれていたら、それは複数のことをやっています。

メソッドの説明 1つのこと?
注文の合計金額を計算する ✅ 1つ
注文をバリデーションする ✅ 1つ
注文を保存する ✅ 1つ
注文の合計を計算 して メール送信する ❌ 2つ
注文をバリデーション 保存 メール送信する ❌ 3つ

「〜して」が含まれる説明になっていたら、そこが分割のポイント です。

悪い例(複数の責務が同居)

public String processOrder(Order order) {
    // バリデーション
    if (order == null) return "注文がありません";
    if (order.getEmail() == null || order.getEmail().isBlank()) return "メール不正";
    if (order.getItems() == null || order.getItems().isEmpty()) return "明細なし";

    // 合計金額の計算
    int totalPrice = 0;
    for (OrderItem item : order.getItems()) {
        totalPrice += item.getUnitPrice() * item.getQuantity();
    }

    // 税込計算
    int finalPrice = (int) (totalPrice * 1.10);

    // メール送信
    System.out.println("[メール送信] To: " + order.getEmail() + " / 合計: " + finalPrice + "円");

    return "注文完了: 合計" + finalPrice + "円";
}

このメソッドは「バリデーション」「合計計算」「税込計算」「メール送信」の 4つの責務 を持っています。

良い例(責務ごとに分割)

public String processOrder(Order order) {
    if (!isValid(order)) {
        return "注文が不正です";
    }
    int totalPrice = calculateTotalPrice(order.getItems());
    int finalPrice = applyTax(totalPrice);
    sendConfirmationEmail(order.getEmail(), finalPrice);
    return "注文完了: 合計" + finalPrice + "円";
}

private boolean isValid(Order order) {
    if (order == null) return false;
    if (order.getEmail() == null || order.getEmail().isBlank()) return false;
    if (order.getItems() == null || order.getItems().isEmpty()) return false;
    return true;
}

private int calculateTotalPrice(List<OrderItem> items) {
    int total = 0;
    for (OrderItem item : items) {
        total += item.getUnitPrice() * item.getQuantity();
    }
    return total;
}

private int applyTax(int priceExcludingTaxJpy) {
    return (int) (priceExcludingTaxJpy * 1.10);
}

private void sendConfirmationEmail(String email, int totalPrice) {
    System.out.println("[メール送信] To: " + email + " / 合計: " + totalPrice + "円");
}

processOrder 自体は わずか6行 になりました。
そして、各サブメソッドは それぞれ1つのこと だけを担当しています。

分割後のメリット

メリット 説明
読解しやすい processOrder を見るだけで「全体の流れ」が分かる
テストしやすい calculateTotalPrice だけを単体テストできる
再利用しやすい applyTax は他のメソッドからも呼べる
変更しやすい 税率が変わったら applyTax だけ直せばよい

原則② 抽象度を揃える

1つのメソッドの中では、処理の「抽象度(粒度)」を揃える のが鉄則です。

抽象度とは

抽象度とは「どれだけ概念的なレベルで処理を表現しているか」のことです。

  • 高い抽象度:「注文を処理する」「合計を計算する」
  • 低い抽象度:「リストの各要素について unitPrice * quantity を加算する」

悪い例(抽象度がバラバラ)

public String processOrder(Order order) {
    if (!isValid(order)) {                                // 高い抽象度
        return "注文が不正です";
    }
    int totalPrice = 0;                                   // 低い抽象度
    for (OrderItem item : order.getItems()) {             // 低い抽象度
        totalPrice += item.getUnitPrice() * item.getQuantity();  // 低い抽象度
    }
    int finalPrice = applyTax(totalPrice);                // 高い抽象度
    sendConfirmationEmail(order.getEmail(), finalPrice);  // 高い抽象度
    return "注文完了: 合計" + finalPrice + "円";
}

isValid」「applyTax」のような 高い抽象度の呼び出し と、「ループで合計を計算する」低い抽象度の処理 が同じメソッドに混在しています。

これは 「段落の中に箇条書きが紛れている文章」 のようなもので、読み手が混乱します。

良い例(抽象度を揃える)

public String processOrder(Order order) {
    if (!isValid(order)) {
        return "注文が不正です";
    }
    int totalPrice = calculateTotalPrice(order.getItems());  // 低い処理を切り出した
    int finalPrice = applyTax(totalPrice);
    sendConfirmationEmail(order.getEmail(), finalPrice);
    return "注文完了: 合計" + finalPrice + "円";
}

processOrder の中身は すべて「高い抽象度の処理(業務的な操作)」 で揃いました。
具体的な計算ロジック(低い抽象度)は、calculateTotalPrice の中だけに閉じ込められています。

抽象度の階層

メソッドは 階層構造 で考えると整理しやすくなります。

processOrder(最も抽象度が高い:業務フロー全体)
  ├─ isValid(中レベル:注文の検証)
  ├─ calculateTotalPrice(中レベル:合計計算)
  │   └─ (ループで unitPrice * quantity を加算する:最も低い)
  ├─ applyTax(中レベル:税込変換)
  └─ sendConfirmationEmail(中レベル:メール送信)

1つのメソッドの中には、同じ階層の処理だけが並ぶ ようにします。


原則③ メソッド名で意図を表現する

メソッド名は 「このメソッドが何をするか」を一文で表す ように付けます。

メソッド名の付け方ルール

種類 命名パターン
何かをする処理 動詞 + 目的語 calculateTotalPricesendEmailvalidateOrder
Boolean を返す is / has / can で始める isValidhasErrorcanEdit
値を取得する get または find で始める getUserNamefindUserById
値を変換する to で始める toJsontoDto
値を作る build / create / new buildSummarycreateOrder

悪い例(意図が伝わらない名前)

private int calc(List<OrderItem> items) {
    int t = 0;
    for (OrderItem i : items) {
        t += i.getUnitPrice() * i.getQuantity();
    }
    return t;
}

private void process(String e, int p) {
    // メール送信
}
  • calc だけでは「何を計算するか」が分からない
  • process は最も曖昧な動詞(何でも当てはまる)
  • ep は意味の伝わらない引数名(#1参照)

良い例(意図が伝わる名前)

private int calculateTotalPrice(List<OrderItem> items) {
    int total = 0;
    for (OrderItem item : items) {
        total += item.getUnitPrice() * item.getQuantity();
    }
    return total;
}

private void sendConfirmationEmail(String email, int totalPrice) {
    // メール送信
}

メソッド名から 「何をするか」「何が引数として必要か」 が一目で分かります。

「process」「handle」「manage」は避ける

新人がよく使う名前ですが、何をするかが全く伝わらない のでNGです。

NG 改善案
processOrder submitOrder / placeOrder / confirmOrder
handleUser registerUser / deleteUser / notifyUser
manageData saveUserProfile / archiveOldRecords

メソッド名を見て 「これは何をしているのか分からない」 と感じたら、それは命名が悪いか、メソッドの責務が広すぎる兆候です。


動作確認:3原則を全部適用したサンプル

3つの原則をすべて適用したコード例です。コピペでそのまま動かせます。

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

public class MethodSplitDemo {
    public static void main(String[] args) {
        Order order = new Order(1L, "tanaka@example.com", List.of(
                new OrderItem("Java本", 2500, 2),
                new OrderItem("SQL本", 3000, 1)
        ));

        String result = processOrder(order);
        System.out.println(result);
    }

    static String processOrder(Order order) {
        if (!isValid(order)) {
            return "注文が不正です";
        }
        int totalPrice = calculateTotalPrice(order.getItems());
        int finalPrice = applyTax(totalPrice);
        sendConfirmationEmail(order.getEmail(), finalPrice);
        return "注文完了: 合計" + finalPrice + "円";
    }

    static boolean isValid(Order order) {
        if (order == null) {
            return false;
        }
        if (order.getEmail() == null || order.getEmail().isBlank()) {
            return false;
        }
        if (order.getItems() == null || order.getItems().isEmpty()) {
            return false;
        }
        return true;
    }

    static int calculateTotalPrice(List<OrderItem> items) {
        int total = 0;
        for (OrderItem item : items) {
            total += item.getUnitPrice() * item.getQuantity();
        }
        return total;
    }

    static int applyTax(int priceExcludingTaxJpy) {
        return (int) (priceExcludingTaxJpy * 1.10);
    }

    static void sendConfirmationEmail(String email, int totalPrice) {
        System.out.println("[メール送信] To: " + email + " / 合計: " + totalPrice + "円");
    }
}

class Order {
    private final long id;
    private final String email;
    private final List<OrderItem> items;
    Order(long id, String email, List<OrderItem> items) { this.id = id; this.email = email; this.items = items; }
    long getId() { return id; }
    String getEmail() { return email; }
    List<OrderItem> getItems() { return items; }
}

class OrderItem {
    private final String name;
    private final int unitPrice;
    private final int quantity;
    OrderItem(String name, int unitPrice, int quantity) { this.name = name; this.unitPrice = unitPrice; this.quantity = quantity; }
    String getName() { return name; }
    int getUnitPrice() { return unitPrice; }
    int getQuantity() { return quantity; }
}

期待する出力

[メール送信] To: tanaka@example.com / 合計: 8800円
注文完了: 合計8800円

メソッドの長さの目安

「具体的に何行までならOK?」とよく聞かれます。

新人〜2年目の目安としては、以下を覚えておくと判断しやすくなります。

メソッドの長さ 状態 対応
10行以内 理想的 そのまま
10〜30行 適切 内容を見て分割の必要性を判断
30〜60行 やや長い 分割できる箇所がないか検討
60行以上 長すぎる 分割を強く検討
100行以上 問題あり 必ず分割する

ただし、「行数」は目安にすぎません
本質は「1つのことだけをしているか」です。10行でも複数の責務が混じっていれば分割すべきですし、50行でも1つの責務に集中していれば許容範囲です。


半日溶かした実話:300行のメソッド

画面1つを動かすために300行を超えるメソッドが書かれた現場の話です。

public ResponseEntity<?> handleOrder(HttpServletRequest request) {
    // 認証チェック(30行)
    // パラメータの取り出し(20行)
    // バリデーション(40行)
    // DBから関連データ取得(30行)
    // 在庫チェック(25行)
    // 価格計算(35行)
    // 割引適用(30行)
    // 税計算(15行)
    // 注文保存(25行)
    // メール送信(30行)
    // ログ出力(10行)
    // レスポンス組み立て(10行)
    // ...
}

全体で300行を超える メソッドでした。
このメソッドのあるバグ(「特定の条件で合計金額が間違って計算される」)を直す依頼が来たのですが、調査だけで 半日以上 かかりました。

問題はこんな感じでした。

  • どこで合計が計算されているのか、メソッドの中盤まで読まないと分からない
  • 変数 total がメソッドの異なる箇所で 3回再代入 されていた
  • 修正したいロジックを変更すると、後続の30行で参照される値も変わる
  • ユニットテストを書こうとしても、HTTPリクエスト全体をモックする必要があり、テストの準備だけで1日かかる

最終的に提案したのは、メソッドを業務単位で分割する ことでした。

public ResponseEntity<?> handleOrder(HttpServletRequest request) {
    OrderRequest orderRequest = parseOrderRequest(request);
    if (!isAuthenticated(orderRequest)) {
        return unauthorizedResponse();
    }
    ValidationResult validation = validate(orderRequest);
    if (!validation.isValid()) {
        return badRequestResponse(validation);
    }
    Order order = buildOrder(orderRequest);
    int finalPrice = calculateFinalPrice(order);
    saveOrder(order, finalPrice);
    sendConfirmationEmail(order, finalPrice);
    return successResponse(order, finalPrice);
}

handleOrder 自体が 10行 になり、業務フローが上から読むだけで分かるようになりました。
各サブメソッドは 個別にユニットテストが書けます

メソッドを分割するだけで、保守可能性は別物になります。


演習問題

難易度の見方

マーク 難易度 目安
基本 原則を覚えれば解ける
⭐⭐ 応用 複数の原則を組み合わせる

まずは自分で考えてから、模範解答を見てください!


問題1:長いメソッドを役割で分割する ⭐

次のメソッドは、点数リストから「合計」「平均」「最高点」を計算し、レポートとして出力します。
役割ごとにメソッドを分割してください。

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

public class Sample {
    public static void main(String[] args) {
        List<Score> scores = new ArrayList<>();
        scores.add(new Score("田中", 80));
        scores.add(new Score("佐藤", 60));
        scores.add(new Score("鈴木", 90));

        printScoreReport(scores);
    }

    static void printScoreReport(List<Score> scores) {
        // 合計を計算する
        int total = 0;
        for (Score score : scores) {
            total += score.getValue();
        }

        // 平均を計算する
        double average = 0.0;
        if (!scores.isEmpty()) {
            average = (double) total / scores.size();
        }

        // 最高点を見つける
        int max = Integer.MIN_VALUE;
        for (Score score : scores) {
            if (score.getValue() > max) {
                max = score.getValue();
            }
        }

        // レポートを出力する
        System.out.println("合計: " + total);
        System.out.println("平均: " + average);
        System.out.println("最高点: " + max);
    }
}

class Score {
    private final String name;
    private final int value;
    Score(String name, int value) { this.name = name; this.value = value; }
    String getName() { return name; }
    int getValue() { return value; }
}
模範解答
import java.util.List;
import java.util.ArrayList;

public class Exercise01 {
    public static void main(String[] args) {
        List<Score> scores = new ArrayList<>();
        scores.add(new Score("田中", 80));
        scores.add(new Score("佐藤", 60));
        scores.add(new Score("鈴木", 90));

        printScoreReport(scores);
    }

    static void printScoreReport(List<Score> scores) {
        int total = calculateTotal(scores);
        double average = calculateAverage(scores);
        int max = findMax(scores);
        printReport(total, average, max);
    }

    static int calculateTotal(List<Score> scores) {
        int total = 0;
        for (Score score : scores) {
            total += score.getValue();
        }
        return total;
    }

    static double calculateAverage(List<Score> scores) {
        if (scores.isEmpty()) {
            return 0.0;
        }
        return (double) calculateTotal(scores) / scores.size();
    }

    static int findMax(List<Score> scores) {
        int max = Integer.MIN_VALUE;
        for (Score score : scores) {
            if (score.getValue() > max) {
                max = score.getValue();
            }
        }
        return max;
    }

    static void printReport(int total, double average, int max) {
        System.out.println("合計: " + total);
        System.out.println("平均: " + average);
        System.out.println("最高点: " + max);
    }
}

class Score {
    private final String name;
    private final int value;
    Score(String name, int value) { this.name = name; this.value = value; }
    String getName() { return name; }
    int getValue() { return value; }
}

期待する出力

合計: 230
平均: 76.66666666666667
最高点: 90

ポイント

  • printScoreReport は「業務フローを書くメソッド」になり、抽象度が揃った
  • 各サブメソッドはそれぞれ1つの責務だけを担当(合計/平均/最大値/出力)
  • calculateAveragecalculateTotal を再利用している(DRY原則)

問題2:抽象度を揃える ⭐

次のメソッドは、抽象度がバラバラです。
低レベルな処理(文字列の組み立て)をサブメソッドに切り出して、buildOrderSummary の抽象度を揃えてください。

public class Sample {
    public static void main(String[] args) {
        System.out.println(buildOrderSummary(new Order("田中", 1000, 3)));
    }

    static String buildOrderSummary(Order order) {
        String customerLine = "お客様: " + order.getCustomerName() + "様";
        int total = order.getUnitPrice() * order.getQuantity();
        String priceLine = "金額: " + order.getUnitPrice() + "円 × " + order.getQuantity() + "個 = " + total + "円";
        return customerLine + "\n" + priceLine;
    }
}

class Order {
    private final String customerName;
    private final int unitPrice;
    private final int quantity;
    Order(String customerName, int unitPrice, int quantity) {
        this.customerName = customerName; this.unitPrice = unitPrice; this.quantity = quantity;
    }
    String getCustomerName() { return customerName; }
    int getUnitPrice() { return unitPrice; }
    int getQuantity() { return quantity; }
}
模範解答
public class Exercise02 {
    public static void main(String[] args) {
        System.out.println(buildOrderSummary(new Order("田中", 1000, 3)));
    }

    static String buildOrderSummary(Order order) {
        String customerLine = formatCustomer(order.getCustomerName());
        String priceLine = formatPrice(order.getUnitPrice(), order.getQuantity());
        return customerLine + "\n" + priceLine;
    }

    static String formatCustomer(String name) {
        return "お客様: " + name + "様";
    }

    static String formatPrice(int unitPrice, int quantity) {
        int total = unitPrice * quantity;
        return "金額: " + unitPrice + "円 × " + quantity + "個 = " + total + "円";
    }
}

class Order {
    private final String customerName;
    private final int unitPrice;
    private final int quantity;
    Order(String customerName, int unitPrice, int quantity) {
        this.customerName = customerName; this.unitPrice = unitPrice; this.quantity = quantity;
    }
    String getCustomerName() { return customerName; }
    int getUnitPrice() { return unitPrice; }
    int getQuantity() { return quantity; }
}

期待する出力

お客様: 田中様
金額: 1000円 × 3個 = 3000円

ポイント

  • buildOrderSummary の中は「formatCustomer」「formatPrice」という同じ抽象度の処理だけが並ぶ
  • 文字列の組み立てや計算(低レベル処理)は、各サブメソッドの中だけに閉じ込められた
  • 「サマリを組み立てる」という意図がそのままコードの構造に表れる

問題3:意図が分かるメソッド名に直す ⭐

次のメソッドは、メールアドレスのバリデーションを行います。
メソッド名・変数名がすべて曖昧なので、意図が伝わる名前に直してください。

public class Sample {
    public static void main(String[] args) {
        System.out.println(check("tanaka@example.com"));
        System.out.println(check("invalid"));
        System.out.println(check(""));
        System.out.println(check(null));
    }

    static boolean check(String s) {
        if (s == null) {
            return false;
        }
        if (s.isBlank()) {
            return false;
        }
        if (!s.contains("@")) {
            return false;
        }
        return true;
    }
}
模範解答
public class Exercise03 {
    public static void main(String[] args) {
        System.out.println(isValidEmailAddress("tanaka@example.com"));
        System.out.println(isValidEmailAddress("invalid"));
        System.out.println(isValidEmailAddress(""));
        System.out.println(isValidEmailAddress(null));
    }

    static boolean isValidEmailAddress(String email) {
        if (email == null) {
            return false;
        }
        if (email.isBlank()) {
            return false;
        }
        if (!email.contains("@")) {
            return false;
        }
        return true;
    }
}

期待する出力

true
false
false
false

改善ポイント

元の名前 改善後 理由
check isValidEmailAddress 「何をチェックするか」が明確(Bool返却なので is で始める)
s email 引数の役割が明確に

ポイント

  • メソッド名は「動詞 or 状態」を表す
  • Booleanを返すメソッドは is / has / can で始める(#1で扱った命名原則)

問題4:複合リファクタリング ⭐⭐

次のメソッドは「複数の責務」「抽象度の不揃い」「曖昧な命名」がすべて含まれています。
3原則を踏まえて、リファクタリングしてください。

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

public class Sample {
    public static void main(String[] args) {
        List<CartItem> cart = new ArrayList<>();
        cart.add(new CartItem("Java本", 2500, 2));
        cart.add(new CartItem("SQL本", 3000, 1));

        process(cart, true);
    }

    static void process(List<CartItem> cart, boolean m) {
        // 小計を計算する
        int s = 0;
        for (CartItem i : cart) {
            s += i.getUnitPrice() * i.getQuantity();
        }

        // 会員割引を適用する
        int d = s;
        if (m) {
            d = (int) (s * 0.95);
        }

        // 税込にする
        int f = (int) (d * 1.10);

        // 出力する
        System.out.println("小計: " + s + "円");
        System.out.println("割引後: " + d + "円");
        System.out.println("税込合計: " + f + "円");
    }
}

class CartItem {
    private final String name;
    private final int unitPrice;
    private final int quantity;
    CartItem(String name, int unitPrice, int quantity) { this.name = name; this.unitPrice = unitPrice; this.quantity = quantity; }
    String getName() { return name; }
    int getUnitPrice() { return unitPrice; }
    int getQuantity() { return quantity; }
}
模範解答
import java.util.List;
import java.util.ArrayList;

public class Exercise04 {
    public static void main(String[] args) {
        List<CartItem> cart = new ArrayList<>();
        cart.add(new CartItem("Java本", 2500, 2));
        cart.add(new CartItem("SQL本", 3000, 1));

        Receipt receipt = checkout(cart, true);
        printReceipt(receipt);
    }

    static Receipt checkout(List<CartItem> cart, boolean isMember) {
        int subtotal = calculateSubtotal(cart);
        int discountedPrice = applyMemberDiscount(subtotal, isMember);
        int finalPrice = applyTax(discountedPrice);
        return new Receipt(subtotal, discountedPrice, finalPrice);
    }

    static int calculateSubtotal(List<CartItem> cart) {
        int subtotal = 0;
        for (CartItem item : cart) {
            subtotal += item.getUnitPrice() * item.getQuantity();
        }
        return subtotal;
    }

    static int applyMemberDiscount(int subtotal, boolean isMember) {
        if (!isMember) {
            return subtotal;
        }
        return (int) (subtotal * 0.95);
    }

    static int applyTax(int priceExcludingTaxJpy) {
        return (int) (priceExcludingTaxJpy * 1.10);
    }

    static void printReceipt(Receipt receipt) {
        System.out.println("小計: " + receipt.getSubtotal() + "円");
        System.out.println("割引後: " + receipt.getDiscountedPrice() + "円");
        System.out.println("税込合計: " + receipt.getFinalPrice() + "円");
    }
}

class CartItem {
    private final String name;
    private final int unitPrice;
    private final int quantity;
    CartItem(String name, int unitPrice, int quantity) { this.name = name; this.unitPrice = unitPrice; this.quantity = quantity; }
    String getName() { return name; }
    int getUnitPrice() { return unitPrice; }
    int getQuantity() { return quantity; }
}

class Receipt {
    private final int subtotal;
    private final int discountedPrice;
    private final int finalPrice;
    Receipt(int subtotal, int discountedPrice, int finalPrice) {
        this.subtotal = subtotal; this.discountedPrice = discountedPrice; this.finalPrice = finalPrice;
    }
    int getSubtotal() { return subtotal; }
    int getDiscountedPrice() { return discountedPrice; }
    int getFinalPrice() { return finalPrice; }
}

期待する出力

小計: 8000円
割引後: 7600円
税込合計: 8360円

改善ポイント

元のコード 改善後 理由
process() checkout() + 出力分離 「精算する」と「出力する」を分離
1メソッドに4責務 5メソッドに分割 1メソッド1責務
sdfim subtotaldiscountedPricefinalPriceitemisMember 意図の伝わる命名(#1参照)
計算結果がローカル変数で散在 Receipt クラスにまとめる データの集約
process の中で出力 計算と出力を分離 計算結果を返り値で受け取れる=テスト可能に

ポイント

  • 「計算」と「出力」を分けることで、計算ロジックを単体テストできるようになる
  • 関連する値(subtotaldiscountedPricefinalPrice)は Receipt クラスにまとめる
  • メソッド名を見るだけで全体の流れが分かる(checkoutcalculateSubtotalapplyMemberDiscountapplyTaxprintReceipt

まとめ

新人〜2年目が押さえるべきメソッド分割の3原則は、以下の3つです。

  1. 1つのことだけをするメソッドにする:「〜して〜する」が含まれたら分割の合図
  2. 抽象度を揃える:高レベル処理と低レベル処理を同じメソッドに混在させない
  3. メソッド名で意図を表現する:動詞+目的語、または is / has / can のプレフィックス

長さの目安は 10〜30行 ですが、本質は「1つのことだけをしているか」です。

メソッド分割は 「複雑な処理を、小さな部品の組み合わせに変える」 行為です。
小さな部品はテストも修正も再利用も容易です。「コードを書く」より「コードを分ける」ことに時間を使う だけで、半年後の自分とチームが救われます。


次回予告

次回(#7)は 「ループ処理」 を扱います。

  • 多重 for ループの整理
  • Java 8以降の Stream API を使うべき場面
  • 「ループの中で複数のことをやってしまう」アンチパターン

を、Before / After 形式で解説していきます。


参考


@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?