はじめに
株式会社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つの責務だけテストしたくても、全体のセットアップが必要
- 変更コスト:1箇所の修正が、関係ない処理に影響していないか確認する必要
特に厄介なのが テストコスト です。
「税込計算の境界値テストを書きたい」と思っても、注文オブジェクト・メール送信モック・DBモック…と全てをセットアップしないとテストできません。テストを書くこと自体が高コスト になり、結果として テストが書かれない という事態を招きます。
押さえるべきは3原則
新人〜2年目がまず身につけるべきメソッド分割の原則は、以下の3つです。
- 1つのことだけをするメソッドにする
- 抽象度を揃える
- メソッド名で意図を表現する
順番に見ていきます。
原則① 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つのメソッドの中には、同じ階層の処理だけが並ぶ ようにします。
原則③ メソッド名で意図を表現する
メソッド名は 「このメソッドが何をするか」を一文で表す ように付けます。
メソッド名の付け方ルール
| 種類 | 命名パターン | 例 |
|---|---|---|
| 何かをする処理 | 動詞 + 目的語 |
calculateTotalPrice、sendEmail、validateOrder
|
| Boolean を返す |
is / has / can で始める |
isValid、hasError、canEdit
|
| 値を取得する |
get または find で始める |
getUserName、findUserById
|
| 値を変換する |
to で始める |
toJson、toDto
|
| 値を作る |
build / create / new
|
buildSummary、createOrder
|
悪い例(意図が伝わらない名前)
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は最も曖昧な動詞(何でも当てはまる) -
e、pは意味の伝わらない引数名(#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つの責務だけを担当(合計/平均/最大値/出力)
-
calculateAverageはcalculateTotalを再利用している(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責務 |
s、d、f、i、m
|
subtotal、discountedPrice、finalPrice、item、isMember
|
意図の伝わる命名(#1参照) |
| 計算結果がローカル変数で散在 |
Receipt クラスにまとめる |
データの集約 |
process の中で出力 |
計算と出力を分離 | 計算結果を返り値で受け取れる=テスト可能に |
ポイント:
- 「計算」と「出力」を分けることで、計算ロジックを単体テストできるようになる
- 関連する値(
subtotal、discountedPrice、finalPrice)はReceiptクラスにまとめる - メソッド名を見るだけで全体の流れが分かる(
checkout→calculateSubtotal→applyMemberDiscount→applyTax→printReceipt)
まとめ
新人〜2年目が押さえるべきメソッド分割の3原則は、以下の3つです。
- 1つのことだけをするメソッドにする:「〜して〜する」が含まれたら分割の合図
- 抽象度を揃える:高レベル処理と低レベル処理を同じメソッドに混在させない
-
メソッド名で意図を表現する:動詞+目的語、または
is/has/canのプレフィックス
長さの目安は 10〜30行 ですが、本質は「1つのことだけをしているか」です。
メソッド分割は 「複雑な処理を、小さな部品の組み合わせに変える」 行為です。
小さな部品はテストも修正も再利用も容易です。「コードを書く」より「コードを分ける」ことに時間を使う だけで、半年後の自分とチームが救われます。
次回予告
次回(#7)は 「ループ処理」 を扱います。
- 多重
forループの整理 - Java 8以降の
Stream APIを使うべき場面 - 「ループの中で複数のことをやってしまう」アンチパターン
を、Before / After 形式で解説していきます。
参考
- リーダブルコード 第10章「無関係の下位問題を抽出する」(オライリー・ジャパン)
- Martin Fowler - Refactoring: Extract Function
- Robert C. Martin - Clean Code(単一責任原則)
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!