0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Effective Java 4-19:継承のために設計および文書化する、でなければ継承を禁止する

Posted at

4-19:継承のために設計および文書化する、でなければ継承を禁止する

要点

継承は強力だが脆い。サブクラスがスーパークラスの実装の内部仕様に依存すると、スーパークラスの変更でサブクラスが壊れる。公開 API が増え、保守や互換性のコストが跳ね上がる。だから「拡張を公的にサポートする」か「完全に禁止する」か、どちらかにすべき。

悪い例

public class Parent {
    public Parent() {
        init(); // 危険:オーバーライド可能なメソッドをコンストラクタで呼ぶ
    }

    public void init() { /* 初期化 */ }
}

public class Child extends Parent {
    private final Resource r = new Resource();

    @Override
    public void init() {
        r.doSomething(); // r はまだ初期化されていない可能性がある -> NPE
    }
}

問題点

  • スーパークラスのコンストラクタがオーバーライド可能なメソッドを呼んでいる
    → サブクラスのフィールドが未初期化の状態で呼ばれる危険。
  • スーパークラスがどのメソッドを「安全に」オーバーライドできるか文書化していない
    → サブクラス実装が壊れる。

良い例

設計どおりに拡張ポイント(hooks)を提供し、仕様を文書化する例:

/**
 * ドキュメントに「サブクラスはこの hook を実装してよい。ただしコンストラクタではなく
 * onInit() が呼ばれるタイミングで安全に使える」と明記する。
 */
public abstract class Framework {
    public final void start() {
        // 固定の初期化手順
        initCore();
        // hook を呼ぶ(安全なタイミング)
        onInit();
    }

    private void initCore() { /* 基本初期化 */ }

    /**
     * 拡張ポイント: サブクラスはここを実装できる。
     * implSpec: onInit は start() の中で呼ばれ、すでに基盤が初期化済みであることを保証する。
     */
    protected abstract void onInit();
}

ポイント

  • start() は final にして処理順を固定(サブクラスが勝手に手順を壊せない)。
  • 拡張ポイント onInit() は protected にし、いつ呼ばれるか/どの条件下で安全かを Javadoc(@implSpec など)で明記する。
  • サブクラスはドキュメントに従えば安全に拡張できる。

継承を禁止する/制限する方法

ライブラリ作成者は継承を禁止したいことが多い。
やり方:
1. finalクラスにする(一番簡単)

public final class Utility { ... }

 
2. コンストラクタをprivateにして静的ファクトリを用意(派生禁止 + インスタンス制御)

public class Foo {
    private Foo() { }
    public static Foo of(...) { return new Foo(...); }
}

3. パッケージプライベート(default)にして外部から継承不能にする

class InternalHelper { ... } // same package only

4. Java17+のsealedクラスで制御(明示的に許可されたサブクラスのみ)

public sealed class Base permits AllowedSub1, AllowedSub2 { ... }

5. ドキュメントで「継承はサポートしない」旨を明記し、可能ならコンパイル時に final を使う(宣言的に禁止)。

継承を「設計して文書化」する際の必須項目

ライブラリで継承を許すなら、最低でも次を文書化しておくこと:

  • いつ/どの順序でサブクラスのメソッドが呼ばれるか(コンストラクタ後か、初期化完了後か等)。
  • どのメソッドが拡張ポイント(override して良い)か(そしてその契約)。
  • サブクラスで守るべき不変条件。
  • スレッド安全性の契約(呼び出しはシングルスレッドか?マルチスレッドで呼ばれるか?)。
  • 例外処理の方針(サブクラスが例外を投げてよいか、投げる場合どう扱うか)。
  • super のメソッドは必ず呼べるか/呼ぶべきか(呼ぶ順序やタイミング)。
  • 将来の互換性についての注意(どの部分は安定 API で、どれは将来変更される可能性があるか)。

まとめ

継承を許すなら設計と契約を明確にして文書化し、そうでなければ継承を禁止してユーザーに不確定な拡張をさせないこと。
これにより将来の保守性・互換性・安全性が格段に向上する。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?