Java | 1 日 120 分 × 7 日アプリ学習 中級編:オブジェクト指向(OOP) - 継承アプリ

Web APP Java
スポンサーリンク

6日目のゴール

継承アプリ6日目のテーマも
「共通部分をまとめる」。

でも今日は、
「共通部分をまとめた“あと”に壊れない継承とは何か」
を考える日です。

つまり、
継承で親子関係を作ったあとに

親を直したら子は大丈夫か?
子を増やしても設計が破綻しないか?

という “長持ちする継承” を意識していきます。


「共通部分をまとめたつもりが、あとで破綻する」パターン

追加要件が来た瞬間に、親が苦しくなる

まず、よくある失敗パターンからいきます。

「通知」の例をもう一度使いましょう。

最初は、メール通知とプッシュ通知だけでした。

public abstract class Notification {
    protected String body;

    public Notification(String body) {
        this.body = body;
    }

    public abstract void send();
}
Java
public class MailNotification extends Notification {
    private String toAddress;
    private String subject;

    public MailNotification(String toAddress, String subject, String body) {
        super(body);
        this.toAddress = toAddress;
        this.subject = subject;
    }

    @Override
    public void send() {
        System.out.println("メール送信: " + toAddress + " / " + subject);
        System.out.println("本文: " + body);
    }
}
Java
public class PushNotification extends Notification {
    private String userId;
    private String title;

    public PushNotification(String userId, String title, String body) {
        super(body);
        this.userId = userId;
        this.title = title;
    }

    @Override
    public void send() {
        System.out.println("プッシュ送信: " + userId + " / " + title);
        System.out.println("本文: " + body);
    }
}
Java

ここまでは気持ちいい継承です。
共通部分(本文と send という行為)を親にまとめて、
違いだけを子に持たせている。

ところが、ある日こういう要望が来ます。

「SMS 通知も追加したい。
SMS は文字数制限があるから、長い本文は切り詰めて送ってほしい」

ここで、設計の“耐久性”が試されます。


例題1:無理やり親に寄せて、継承が歪むパターン

親の責務が増えすぎて、子が振り回される

要件を聞いて、
ついこうしたくなるかもしれません。

「本文の長さ制限も Notification にまとめよう」

public abstract class Notification {
    protected String body;
    protected int maxLength = Integer.MAX_VALUE;

    public Notification(String body) {
        this.body = body;
    }

    public void setMaxLength(int maxLength) {
        this.maxLength = maxLength;
    }

    protected String truncatedBody() {
        if (body.length() <= maxLength) {
            return body;
        }
        return body.substring(0, maxLength);
    }

    public abstract void send();
}
Java

SMS だけ、maxLength を設定する。

public class SmsNotification extends Notification {
    private String phoneNumber;

    public SmsNotification(String phoneNumber, String body) {
        super(body);
        this.phoneNumber = phoneNumber;
        this.maxLength = 70; // ここで制限
    }

    @Override
    public void send() {
        System.out.println("SMS送信: " + phoneNumber);
        System.out.println("本文: " + truncatedBody());
    }
}
Java

一見、動きます。
でも、じわじわと違和感が出てきます。

メールやプッシュには文字数制限はないのに、
maxLength というフィールドを持たされている。

Notification の責務が
「通知の本文を持つ」から
「文字数制限まで知っている」へと膨らんでいる。

「一部の子だけが必要な事情」を、
親に押し込めてしまっている
状態です。

こうなると、
親を触るたびに「他の子への影響」を気にしないといけなくなり、
継承がどんどん窮屈になります。


例題1の改善:「共通部分」と「特殊事情」を分ける

親は“本当に全員に共通すること”だけを持つ

ここで、継承の原点に戻ります。

Notification に本当に共通しているのは何か?

本文を持つ
送るという行為を持つ

文字数制限は、
SMS だけの事情です。

ならば、
親はシンプルに戻します。

public abstract class Notification {
    protected String body;

    public Notification(String body) {
        this.body = body;
    }

    public abstract void send();
}
Java

SMS 側でだけ、制限を持てばいい。

public class SmsNotification extends Notification {
    private String phoneNumber;
    private static final int MAX_LENGTH = 70;

    public SmsNotification(String phoneNumber, String body) {
        super(body);
        this.phoneNumber = phoneNumber;
    }

    private String truncatedBody() {
        if (body.length() <= MAX_LENGTH) {
            return body;
        }
        return body.substring(0, MAX_LENGTH);
    }

    @Override
    public void send() {
        System.out.println("SMS送信: " + phoneNumber);
        System.out.println("本文: " + truncatedBody());
    }
}
Java

メールやプッシュは、
何も変える必要がありません。

ここでのポイントは、

親に入れるのは「全員に本当に共通すること」だけ
一部の子だけが必要な事情は、その子の中に閉じ込める

という線引きです。

「共通部分をまとめる」ときに、
“全員共通”と“一部だけの事情”を混ぜない。

これが、長持ちする継承の第一条件です。


重要ポイントの深掘り1:「将来の子クラス」を頭の片隅に置く

今いる子だけを見て親を決めると、あとで苦しくなる

6日目で意識してほしいのは、
親クラスを設計するときに

「今いる子クラス」だけでなく
「将来増えそうな子クラス」も
ぼんやり想像しておくことです。

Notification なら、

メール
プッシュ
SMS
LINE
Slack

など、増えそうですよね。

そのときに、
Notification に入れていいのは

どの通知でも絶対に必要なもの
どの通知でも意味が変わらないもの

だけです。

本文
送るという行為

くらいなら、
どの通知にも自然に当てはまります。

逆に、
「文字数制限」「添付ファイル」「既読管理」などは、
通知の種類によって事情が違いすぎるので、
親に入れるとすぐに苦しくなります。

「今の子」ではなく「増えたときの子」も想像して、
親に何を入れるか決める。

これが、継承を長持ちさせる感覚です。


例題2:レポート出力の“共通部分”を見直す

export の流れは共通。でも、どこまで親に持たせる?

3日目でやったレポートの例を思い出します。

public abstract class Report {

    public void export() {
        List<String> data = collectData();
        writeHeader();
        writeBody(data);
        writeFooter();
        save();
    }

    protected abstract List<String> collectData();
    protected abstract void writeHeader();
    protected abstract void writeBody(List<String> data);
    protected abstract void writeFooter();
    protected abstract void save();
}
Java

PDF と CSV がありました。

ここまではいいのですが、
ある日こう言われます。

「Excel レポートも追加したい。
Excel のときだけ、ヘッダーの書き方がちょっと特殊で…」

ここで、
親の export() をいじりたくなるかもしれません。

「Excel のときだけ、ヘッダーの前に何かする」
みたいな分岐を入れたくなる。

でも、それをやり始めると、
export() の中が

もし PDF ならこう
もし CSV ならこう
もし Excel ならこう

と、
「子クラスの事情を知りすぎた親」
になってしまいます。


例題2の改善:親は“流れ”だけ、分岐は子の中に閉じ込める

親は「テンプレート」、子は「中身の自由度」を持つ

ここで守りたいのは、

親は「流れ」だけを知っている
各ステップの中身は、子が自由に決める

という構造です。

Excel だけ特殊なヘッダーが必要なら、
Excel の writeHeader() の中で完結させればいい。

public class ExcelReport extends Report {

    @Override
    protected List<String> collectData() {
        return List.of("Excelデータ1", "Excelデータ2");
    }

    @Override
    protected void writeHeader() {
        System.out.println("Excel用の特殊なヘッダーを書く");
    }

    @Override
    protected void writeBody(List<String> data) {
        System.out.println("Excel本文を書く: " + data);
    }

    @Override
    protected void writeFooter() {
        System.out.println("Excelフッターを書く");
    }

    @Override
    protected void save() {
        System.out.println("Excelファイルとして保存");
    }
}
Java

親の export() は、一切変えません。

public void export() {
    List<String> data = collectData();
    writeHeader();
    writeBody(data);
    writeFooter();
    save();
}
Java

ここでのポイントは、

親は「順番」だけを握る
各ステップの中身は、どれだけ特殊でも子の中に閉じ込める

という役割分担です。

「共通部分をまとめる」ときに、
親が子の事情を知りすぎないようにする。

これが、テンプレート的な継承を長持ちさせるコツです。


重要ポイントの深掘り2:「親を変えずに子を増やせるか?」をチェックする

いい継承かどうかを見抜くシンプルな質問

6日目で覚えておいてほしい、
とてもシンプルなチェックがあります。

「新しい子クラスを追加するとき、
親クラスを一切触らずに済むか?」

Notification に SMS を追加するとき、
Notification を触らずに済んだか?

Report に Excel を追加するとき、
Report を触らずに済んだか?

もし「親を触らないと新しい子が作れない」なら、
親が子の事情を抱え込みすぎているサインです。

逆に、
親を一切触らずに子を増やせるなら、
「共通部分のまとめ方がうまくいっている」
可能性が高いです。

継承を設計したあとに、
この質問を自分に投げてみてください。


6日目の実践:「親を変えずに子を増やす」練習をしてみる

あえて“新しい子”を妄想してみる

今日やってみてほしいのは、
自分が作った継承に対して、
あえて「新しい子クラス」を妄想してみることです。

Notification なら、新しい通知の種類。
Report なら、新しいレポート形式。
Person なら、新しい雇用形態。

そのときに、

親クラスを触らずに、
子クラスだけで完結できるか?

もし「親にフィールドを足したくなる」
「親のメソッドに分岐を入れたくなる」
と感じたら、

親が「全員共通」と「一部だけの事情」を
混ぜて抱え込んでいるかもしれません。

そのときは、

本当に親にあるべきか?
その子の中に閉じ込められないか?
別クラスに切り出せないか?

を、もう一度見直してみてください。


6日目で本当に掴んでほしいこと

継承アプリ6日目で伝えたいのは、
「共通部分をまとめる」ときに、
“長持ちするかどうか”までセットで考えること
です。

親に入れるのは「全員に本当に共通すること」だけ。
一部の子だけの事情は、その子の中に閉じ込める。

親は「流れ」や「インターフェース」だけを握る。
中身の特殊さは、子の中で完結させる。

そして何より、
「新しい子を追加するとき、親を触らずに済むか?」
という問いで、自分の継承をチェックする。

この感覚を持てているあなたは、
もう「継承を使える人」ではなく、
「継承が時間に耐えられるかまで見通して設計できる人」 です。

7日目では、
ここまでの継承の感覚を
インターフェースやポリモーフィズムと結びつけて、
自分の言葉で整理していくところまで行けます。

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