Java | オブジェクト指向:再利用しやすいクラス設計

Java Java
スポンサーリンク

「再利用しやすいクラス」とは何か

再利用しやすいクラスは
「別の場面・別のプロジェクトでも、ほとんどそのまま使えるクラス」
です。

もう少し噛み砕くと、

特定の画面・特定の DB・特定のフレームワークにベッタリ依存していない
一つのはっきりした役割だけを持っている
入力と出力がシンプルで分かりやすい

こういうクラスは、後から「あ、この処理前にも書いたな」と思ったときに
コピペではなく「クラスごと使い回す」ことができます。

再利用しやすさは、「書くときの気持ちよさ」よりも
「後で読む・直す・使い回すときの楽さ」に直結します。


再利用しづらいクラスの典型例を先に見ておく

何でも詰め込み Service の例

まず、あえて「再利用しづらいクラス」からお見せします。

public class ReportService {

    private final DataSource dataSource;

    public ReportService(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    public void createMonthlyReport(LocalDate from, LocalDate to, HttpServletResponse response) {
        try (Connection con = dataSource.getConnection()) {
            PreparedStatement ps = con.prepareStatement(
                "SELECT * FROM orders WHERE order_date BETWEEN ? AND ?");
            ps.setDate(1, Date.valueOf(from));
            ps.setDate(2, Date.valueOf(to));
            ResultSet rs = ps.executeQuery();

            // 集計処理
            int total = 0;
            while (rs.next()) {
                total += rs.getInt("amount");
            }

            // CSV 生成
            String csv = "from,to,total\n" + from + "," + to + "," + total;

            // HTTP レスポンスに書き込み
            response.setContentType("text/csv");
            response.getWriter().write(csv);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
Java

このクラスは

DB アクセス
集計ロジック
CSV 生成
HTTP レスポンスへの書き込み

を全部一気にやっています。

月次レポートの「集計処理」だけ別の場所でも使いたくなったらどうなるかを想像してみてください。
HttpServletResponse が引数にいる時点で、Web 以外からは呼びづらいです。
DataSource や SQL もベタ書きなので、テストも難しい。

つまり「このメソッドの中身だけ取り出して、別の場面で再利用」がほぼできない構造です。


再利用しやすいクラス設計の基本(重要な考え方)

1つの責務に集中させる

再利用しやすいクラスは、役割がはっきりしています。

集計するだけのクラス
CSV に変換するだけのクラス
HTTP に書き込むだけのクラス

のように、処理を目的ごとに分解します。

先ほどの例を「再利用しやすい形」に分解すると、例えばこうなります。

public final class OrderReportCalculator {

    public int calcTotalAmount(List<Order> orders, LocalDate from, LocalDate to) {
        int total = 0;
        for (Order o : orders) {
            if (!o.date().isBefore(from) && !o.date().isAfter(to)) {
                total += o.amount();
            }
        }
        return total;
    }
}
Java
public final class CsvOrderReportFormatter {

    public String format(LocalDate from, LocalDate to, int totalAmount) {
        return "from,to,total\n" + from + "," + to + "," + totalAmount;
    }
}
Java
public final class HttpResponseWriter {

    public void writeCsv(HttpServletResponse response, String csv) throws IOException {
        response.setContentType("text/csv");
        response.getWriter().write(csv);
    }
}
Java

これらはそれぞれ

集計ロジック
CSV 文字列の生成
HTTP レスポンスへの書き込み

と責務が明確です。

例えば

バッチ処理でファイルに CSV を出したい
コンソールに結果だけ表示したい

といった場面でも、「集計」と「CSV 生成」はそのまま使い回せます。


依存関係を「下方向」に寄せる

再利用しやすいクラスは「下の層」だけを知っている

さっきの悪い例は、いきなり HttpServletResponse に依存していました。
これは「Web 層」のオブジェクトです。

再利用しやすいクラスを作りたいなら、
なるべく「技術に近いもの」より、「業務に近いもの」のほうを見て実装したほうがよいです。

例えば、集計クラスは「注文リスト」と「期間」だけ知っていればよいので、
List<Order>LocalDate にだけ依存します。

Order 自体もドメインクラスとして、余計な技術要素を持たせないようにします。

public final class Order {

    private final LocalDate date;
    private final int amount;

    public Order(LocalDate date, int amount) {
        this.date = date;
        this.amount = amount;
    }

    public LocalDate date() {
        return date;
    }

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

ここには DB のことも HTTP のことも一切出てきません。
だからこそ、「どの層からでも」「どのアプリでも」再利用しやすくなります。


副作用を減らし、「入れたら結果が返る」形にする

引数と戻り値だけで完結するクラスは再利用しやすい

再利用しやすいクラスやメソッドは、
なるべく「何かを返すだけ」「状態を変えない」方向のほうが扱いやすいです。

例えば、金額の合計を計算するメソッドを考えてみます。

public int calcTotal(List<Integer> values) {
    int total = 0;
    for (Integer v : values) {
        total += v;
    }
    return total;
}
Java

これはとても再利用しやすいです。
入力が List、出力が int、それだけ。

逆に、外部の変数を書き換える形にすると、再利用性は落ちます。

public void calcAndStore(List<Integer> values) {
    int total = 0;
    for (Integer v : values) {
        total += v;
    }
    GlobalContext.setTotal(total);   // グローバルに書き込む
}
Java

呼び出し側は GlobalContext を知っていないといけないし、
テストでもグローバル状態をリセットしないといけません。

「引数を渡すと、決まった型の結果が返ってくる」
この形は、別の場所でもほぼそのまま使い回せます。

もちろん、状態を持つ必要があるクラスもあります。
その場合でも、

データをセットする
処理する
結果を取り出す

という流れがシンプルで、外から分かりやすいように意識すると再利用しやすくなります。


名前とインターフェースを「用途で」つける

名前で「何をしてくれるのか」が分かるクラスは再利用される

再利用したくなるクラスは、クラス名やメソッド名を見ただけで
「こいつは何をしてくれるのか」が浮かびます。

例えば、

DiscountCalculator
PasswordHasher
EmailAddressValidator
MoneyFormatter

こういう名前のクラスは、「あ、あの処理に使えそう」とすぐに頭に浮かびます。

逆に、CommonUtilHelperManager のような名前は
「何でもあり」に見えてしまい、再利用のきっかけになりにくいです。

インターフェースにしておくと差し替えも再利用も楽になる

処理の「型」がはっきりしているものは、インターフェースで表現しておくと、
実装を差し替えたりテスト用のダミーに入れ替えたりがしやすくなります。

public interface PasswordEncoder {
    String encode(String raw);
    boolean matches(String raw, String encoded);
}
Java

これに対して、実装を複数用意できます。

public final class Sha256PasswordEncoder implements PasswordEncoder {
    // 実装…
}

public final class PlainPasswordEncoder implements PasswordEncoder {
    // テスト用に「そのまま返すだけ」の実装…
}
Java

どのクラスも PasswordEncoder にだけ依存していれば、
本番/テスト/別アプリで簡単に使い回せます。


実際の「再利用しやすいクラス」の小さな例

割引計算クラス

例えば「割引額」を計算するクラスを考えてみます。

public final class RateDiscountPolicy {

    private final int percent;   // 0〜100

    public RateDiscountPolicy(int percent) {
        if (percent < 0 || percent > 100) {
            throw new IllegalArgumentException("0〜100で指定してください");
        }
        this.percent = percent;
    }

    public int apply(int originalPrice) {
        if (originalPrice < 0) {
            throw new IllegalArgumentException("金額は0以上");
        }
        return originalPrice - originalPrice * percent / 100;
    }
}
Java

このクラスは、

外部技術に依存しない(DB も HTTP も知らない)
一つの責務(割合割引)だけ
入出力がシンプル(int → int)

なので、どこからでも使い回せます。

Web アプリの注文画面でも、
バッチ処理の請求書作成でも、
テストコード内でも同じように使えます。

呼び出し側は、こんな感じでシンプルです。

RateDiscountPolicy policy = new RateDiscountPolicy(10);
int discounted = policy.apply(1000);    // 900
Java

この大きさ・この単純さを意識してクラスを切ると、再利用しやすくなります。


まとめ:再利用しやすいクラスかどうかを見抜く質問

クラスを書いたあとに、こんな風に自問してみてください。

このクラスは一言で「何をするクラス」と説明できるか
その説明に、特定の画面名やフレームワーク名が混ざっていないか
引数と戻り値だけで、ある程度閉じている処理になっているか
他のプロジェクトやバッチ処理でも、ほぼそのまま使えそうか
テストを書くとき、余計な依存をほとんど用意せずに済みそうか

ここで「うーん」と詰まるなら、そのクラスは
何かを抱え込みすぎているか、技術に寄りすぎているかもしれません。

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