Java | オブジェクト指向:抽象クラスの使いどころ

Java Java
スポンサーリンク

抽象クラスの狙いどころ

抽象クラスは「共通の骨格と不変条件を親で固定し、差し替えたい部分だけ子に実装させる」ための道具です。インターフェースが“契約(メソッドの型)”だけなのに対し、抽象クラスは“状態(フィールド)と共通実装”まで一緒に持てます。これにより、前処理・検証・監査・初期化の順序を親で統一しつつ、具体的なやり方は子に任せる安全な拡張が可能になります。


テンプレートメソッドで「流れ」を固定する(重要)

テンプレートメソッドは、親が処理の流れ(骨格)を定義し、差し替えたいステップだけ抽象メソッドにするパターンです。流れを固定しておくと、監査や検証が抜ける事故を親で防げます。

abstract class Importer {
    public final void run(String path) {
        String raw = load(path);            // 子が実装(抽象)
        String normalized = normalize(raw); // 親の共通ロジック
        save(normalized);                   // 子が実装(抽象)
    }
    protected abstract String load(String path);
    protected String normalize(String s) { return s == null ? "" : s.trim(); }
    protected abstract void save(String data);
}

final class CsvImporter extends Importer {
    @Override protected String load(String path) { return "a,b,c"; }
    @Override protected void save(String data) { System.out.println("saved: " + data); }
}
Java

この形なら、仕様変更(例: normalize を強化)は親の1箇所を直すだけで全実装に反映され、品質と保守性が両立します。


共通の状態と不変条件を親で保証する

抽象クラスはフィールドとコンストラクタを持てるため、子が使う共通状態の検証を親で一元管理できます。親コンストラクタで不変条件(インバリアント)を確立し、子はその前提に乗って処理を組み立てます。

abstract class PaymentGateway {
    private final String endpoint;
    protected PaymentGateway(String endpoint) {
        if (endpoint == null || endpoint.isBlank()) throw new IllegalArgumentException("endpoint");
        this.endpoint = endpoint.trim();
    }
    protected final String endpoint() { return endpoint; }

    public final boolean charge(int amount) {
        if (amount <= 0) return false;   // 共通検証
        return doCharge(amount);         // 差し替えポイント
    }
    protected abstract boolean doCharge(int amount);
}

final class StripeGateway extends PaymentGateway {
    StripeGateway(String endpoint) { super(endpoint); }
    @Override protected boolean doCharge(int amount) {
        System.out.println("stripe -> " + endpoint() + " amt=" + amount);
        return true;
    }
}
Java

共通検証は親に、具体実装は子に——この分離で「抜け漏れ」を機械的に防げます。


Skeletal implementation(骨格実装)で高速に増やす

“骨格実装”とは、抽象クラスに最低限の共通実装と便利メソッドを揃え、子は差分だけ実装すれば完成する形のことです。標準ライブラリの AbstractList などが典型です。自作でも「必須の抽象メソッド+デフォルトの具象メソッド」を用意すると、新しい派生型を短時間で安全に増やせます。

abstract class Formatter {
    public final String format(String raw) {
        String s = preprocess(raw);      // 既定処理
        return postprocess(s);           // 抽象:仕上げだけ差し替え
    }
    protected String preprocess(String s) { return s == null ? "" : s.trim().replaceAll("\\s+", " "); }
    protected abstract String postprocess(String s);
}

final class UpperFormatter extends Formatter {
    @Override protected String postprocess(String s) { return s.toUpperCase(); }
}
Java

骨格があるから、子は「中核の1メソッド」を書くだけで完成します。拡張速度と一貫性が向上します。


インターフェースとの使い分け(重要)

抽象クラスは「状態を持ち、初期化順序や不変条件を親で管理したい」ときに向いています。インターフェースは「複数の役割を自由に組み合わせたい」場面に強いです。両者の併用が実務では最適解になりがちです。

interface Normalizer { String apply(String s); }

abstract class SecureService implements Normalizer {
    @Override public final String apply(String s) {
        String t = s == null ? "" : s.trim();
        audit("normalize");
        return doApply(t);              // 抽象:差し替え点
    }
    protected abstract String doApply(String s);
    private void audit(String tag) { System.out.println("[AUDIT] " + tag); }
}

final class LowerService extends SecureService {
    @Override protected String doApply(String s) { return s.toLowerCase(); }
}
Java

役割(Normalizer)はインターフェースで提供しつつ、流れ・監査・状態は抽象クラスで固定する——組み合わせると強力です。


プラグイン構造・ポリモーフィズムの土台にする

抽象クラスを「共通の起動手順」「エラーハンドリング」「リソース管理」の土台にし、子が“実処理”だけを提供すると、差し替え可能なプラグイン構造が簡潔に作れます。呼び出し側は親型で扱うだけで、実体に応じて動的に切り替わります。

abstract class Task {
    public final void run() {
        try {
            setup();
            execute();     // 抽象:プラグインの本体
        } finally {
            teardown();
        }
    }
    protected void setup() {}
    protected abstract void execute();
    protected void teardown() {}
}

final class EmailTask extends Task {
    @Override protected void execute() { System.out.println("send email"); }
}
final class SmsTask extends Task {
    @Override protected void execute() { System.out.println("send sms"); }
}
Java

安全な枠組み(setup/teardown)を親で保証し、拡張は execute だけに集中できます。


落とし穴と守るべき型(深掘り)

コンストラクタでオーバーライド可能メソッド(抽象や非 final)を呼ぶのは危険です。多段継承で子の未初期化フィールドに触れてしまいます。初期化は「親の完成 → 子の完成」を崩さず、仮想呼び出しを避けてください。また、protected フィールドの露出はカプセル化を壊します。フィールドは private、拡張ポイントは検証付きの protected メソッドに限定し、流れを担うメソッドは final にするのが安全です。継承が深くなるなら、階層を浅く保ち、命名と @Override を徹底して読解コストを抑えます。


仕上げのアドバイス(重要部分のまとめ)

抽象クラスは「流れ・不変条件・共通状態」を親で固め、差し替えたい小さな点だけ子に実装させる武器です。テンプレートメソッドで骨格を固定し、骨格実装で拡張を高速化する。インターフェースと組み合わせて役割を柔軟にしつつ、安全性は親で担保する。コンストラクタの仮想呼び出しを避け、拡張ポイントは最小に——この型を守れば、拡張しやすく壊れにくい設計が自然に手に入ります。

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