Java | オブジェクト指向:オブジェクト指向的リファクタリング

Java Java
スポンサーリンク

オブジェクト指向的リファクタリングとは何か

リファクタリング自体は
「外から見た動きを変えずに、コードの中身(設計・構造)だけを良くしていく作業」
です。

オブジェクト指向的リファクタリングというときは、単に「きれいに書き直す」ではなく

クラスに“責務”を正しく割り振る
オブジェクト同士の関係を整理する
手続き的な塊を、オブジェクトの振る舞いに分解していく

といった方向にコードを変えていくことを指します。

動作は変えずに「誰が何を責任を持ってやるか」を整理し直す。
これがオブジェクト指向的リファクタリングの核心です。


まずは「手続き的なコード」からスタートしてみる

例題:注文金額を計算してメールを送るダメコード

初心者がよく書きがちな、手続き的なコードを例にします。

public class OrderController {

    public void handle(HttpServletRequest request) {
        String userName = request.getParameter("userName");
        String email = request.getParameter("email");
        int price = Integer.parseInt(request.getParameter("price"));
        int quantity = Integer.parseInt(request.getParameter("quantity"));

        int total = price * quantity;

        if (total >= 10000) {
            total = (int)(total * 0.9); // 1万円以上は1割引
        }

        String body = "こんにちは " + userName + " さん。"
                + "ご注文金額は " + total + " 円です。";

        try {
            // メール送信の詳細
            Properties props = new Properties();
            props.put("mail.smtp.host", "smtp.example.com");
            Session session = Session.getInstance(props);
            MimeMessage msg = new MimeMessage(session);
            msg.setFrom("shop@example.com");
            msg.setRecipients(Message.RecipientType.TO, email);
            msg.setSubject("ご注文ありがとうございます");
            msg.setText(body);
            Transport.send(msg);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
Java

一見「動くコード」です。
でも、OrderController が

リクエストから値を取り出す
注文金額を計算する
割引ロジックを持つ
メール本文を組み立てる
メール送信の詳細も全部知っている

という、何でも屋になってしまっています。

ここから少しずつ、オブジェクト指向的にリファクタリングしていきます。


ステップ1:意味のあるクラスを導入する(データ+振る舞いに寄せる)

DTO とドメインオブジェクトを分ける

まず「ただの入力値の束」と「ドメインとして意味のあるもの」を分けます。

リクエストから取り出した値を、一旦 DTO に詰めます。

public class OrderRequestDto {
    public String userName;
    public String email;
    public int price;
    public int quantity;
}
Java

注文そのものは、ドメインオブジェクトとして表現します。

public class Order {

    private final int unitPrice;
    private final int quantity;

    public Order(int unitPrice, int quantity) {
        if (unitPrice < 0 || quantity <= 0) {
            throw new IllegalArgumentException("価格・数量が不正");
        }
        this.unitPrice = unitPrice;
        this.quantity = quantity;
    }

    public int totalBeforeDiscount() {
        return unitPrice * quantity;
    }

    public int totalAfterDiscount() {
        int total = totalBeforeDiscount();
        if (total >= 10_000) {
            total = (int) (total * 0.9);
        }
        return total;
    }
}
Java

これだけでも、割引ロジックが Controller から Order に移りました。
「金額計算は注文自身が知っているべき」というオブジェクト指向的な形に一歩近づいています。

Controller は「リクエストを DTO に詰める」「サービス呼び出し」だけに寄せていけます。


ステップ2:ロジックを「それっぽいクラス」に移動する

金額計算を専用クラスに出す(Move Method / Extract Class)

割引ルールが増えてきたとき、Order の中に if を増やしていくとすぐに肥大化します。
「金額計算」という責務を専用のクラスに出すのが典型的なリファクタリングです。

public class DiscountCalculator {

    public int calcTotal(int unitPrice, int quantity) {
        if (unitPrice < 0 || quantity <= 0) {
            throw new IllegalArgumentException();
        }
        int total = unitPrice * quantity;
        if (total >= 10_000) {
            total = (int) (total * 0.9);
        }
        return total;
    }
}
Java

Order はこの Calculator を使うだけにしてもいいですし、
「金額計算は全部 DiscountCalculator に任せる」と決めるのもありです。

重要なのは、「金額計算という責務」が
Controller ではなく、金額計算を担当するクラスに集約されたことです。

メソッドを「本当に責任を持つべきクラス」に動かす。
これが Move Method / Extract Class と呼ばれる代表的なリファクタリングです。


ステップ3:メール送信の詳細を外に追い出す

メール送信専用のクラスへ切り出す

Controller の中のメール送信処理は、正直 Controller の責務ではありません。
専用クラスに逃がします。

public class MailSender {

    public void sendOrderMail(String to, String body) {
        try {
            Properties props = new Properties();
            props.put("mail.smtp.host", "smtp.example.com");
            Session session = Session.getInstance(props);
            MimeMessage msg = new MimeMessage(session);
            msg.setFrom("shop@example.com");
            msg.setRecipients(Message.RecipientType.TO, to);
            msg.setSubject("ご注文ありがとうございます");
            msg.setText(body);
            Transport.send(msg);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
Java

メール本文の組み立ても、別クラスにしてしまえます。

public class OrderMailBodyBuilder {

    public String build(String userName, int totalAmount) {
        return "こんにちは " + userName + " さん。"
                + "ご注文金額は " + totalAmount + " 円です。";
    }
}
Java

これで Controller はだいぶスリムになります。

public class OrderController {

    private final DiscountCalculator calculator;
    private final MailSender mailSender;
    private final OrderMailBodyBuilder bodyBuilder;

    public OrderController(DiscountCalculator calculator,
                           MailSender mailSender,
                           OrderMailBodyBuilder bodyBuilder) {
        this.calculator = calculator;
        this.mailSender = mailSender;
        this.bodyBuilder = bodyBuilder;
    }

    public void handle(HttpServletRequest request) {
        String userName = request.getParameter("userName");
        String email = request.getParameter("email");
        int price = Integer.parseInt(request.getParameter("price"));
        int quantity = Integer.parseInt(request.getParameter("quantity"));

        int total = calculator.calcTotal(price, quantity);
        String body = bodyBuilder.build(userName, total);
        mailSender.sendOrderMail(email, body);
    }
}
Java

最初の巨大メソッドに比べて、「誰が何をしているか」がはっきりしました。
Controller は「HTTP の入口」としての責務だけ、
金額計算、本文生成、送信はそれぞれのクラスが担当しています。

これがオブジェクト指向的リファクタリングのイメージです。

1つ目のクラスにべったり張り付いていた責務を、
複数の役者(オブジェクト)に分散させていく。


ステップ4:条件分岐の塊をポリモーフィズムに変える

if / switch をクラスに変換する(Replace Conditional with Polymorphism)

オブジェクト指向的リファクタリングで重要な技術の一つがこれです。

例えば割引ルールが増えて、こんなコードになったとします。

public int calcTotal(int unitPrice, int quantity, String userRank) {
    int total = unitPrice * quantity;

    if ("NORMAL".equals(userRank)) {
        // 割引なし
    } else if ("SILVER".equals(userRank)) {
        total = (int)(total * 0.95);
    } else if ("GOLD".equals(userRank)) {
        total = (int)(total * 0.9);
    }
    return total;
}
Java

ランクごとに条件分岐が増えていきます。
こういう場合、「ユーザランクごとの振る舞い」をクラスにしてしまいます。

public interface DiscountPolicy {
    int apply(int total);
}
Java
public class NormalDiscountPolicy implements DiscountPolicy {
    @Override
    public int apply(int total) {
        return total;
    }
}
Java
public class SilverDiscountPolicy implements DiscountPolicy {
    @Override
    public int apply(int total) {
        return (int)(total * 0.95);
    }
}
Java
public class GoldDiscountPolicy implements DiscountPolicy {
    @Override
    public int apply(int total) {
        return (int)(total * 0.9);
    }
}
Java

DiscountCalculator は「どのポリシーか」を知らなくてもよくなります。

public class DiscountCalculator {

    private final DiscountPolicy policy;

    public DiscountCalculator(DiscountPolicy policy) {
        this.policy = policy;
    }

    public int calcTotal(int unitPrice, int quantity) {
        int total = unitPrice * quantity;
        return policy.apply(total);
    }
}
Java

「if で分岐していたものを、それぞれのクラスのメソッドにする」のが
オブジェクト指向的リファクタリングのかなり重要なパターンです。

条件分岐を増やし続けるのではなく、
“違い”をクラスとして表現する方向に寄せていくイメージです。


ステップ5:値オブジェクトを導入して原始型を追い出す

int や String だけで勝負しない

オブジェクト指向的リファクタリングでは、「ただの値」を表すクラスを導入することも大事です。

例えば金額を int で持っていると、どこでも好きな int を渡せてしまいます。

public int calcTotal(int price, int quantity) { ... }
Java

ここを、値オブジェクトに変えます。

public class Money {

    private final int amount;

    public Money(int amount) {
        if (amount < 0) throw new IllegalArgumentException();
        this.amount = amount;
    }

    public Money add(Money other) {
        return new Money(this.amount + other.amount);
    }

    public Money multiply(int quantity) {
        return new Money(this.amount * quantity);
    }

    public int asInt() {
        return amount;
    }
}
Java

DiscountCalculator のシグネチャもこう変えられます。

public Money calcTotal(Money unitPrice, int quantity) {
    Money total = unitPrice.multiply(quantity);
    // 割引ロジック…
    return total;
}
Java

「金額」という概念を Money クラスに押し込むことで

マイナス禁止などのルールを一箇所で守れる
足し算・掛け算のロジックが散らばらない

というメリットがあります。

これは「プリミティブ執着(Primitive Obsession)の解消」として知られるリファクタリングで、
オブジェクト指向的コードへ近づける強力な一手です。


オブジェクト指向的リファクタリングで常に意識してほしい問い

リファクタリングのテクニック名を全部覚えることより、
一つひとつの変更のときに、次のような問いを自分に投げるのが大事です。

この処理、本来は誰(どのクラス)の責任だろうか
この if / switch は、どこかのオブジェクトの振る舞いとして表現できないか
この int / String は、本当は専用の値オブジェクトにすべきじゃないか
この巨大メソッドは、いくつかの意味のあるメソッドに分けられないか
このクラス一つに、いくつの“役割”が混ざってしまっているか

オブジェクト指向的リファクタリングは、
「クラスとメソッドを散らかしたり並べ替えたりしながら、責務の居場所を探していく」作業です。

タイトルとURLをコピーしました