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

Web APP Java
スポンサーリンク

2日目のゴール

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

1日目は
「似たクラスの共通部分を親に出す」
という一番シンプルな継承を体験しました。

2日目はそこから一歩進んで、
「どこまで共通にして、どこから別物として扱うか」
を意識して設計できるようになることがゴールです。


「共通にしたい気持ち」と「分けておきたい現実」

なんでもかんでも親に上げると苦しくなる

1日目の Person / Employee / PartTimer を思い出してください。

Person に「名前」「年齢」をまとめて、
Employee / PartTimer は「雇用形態としての違い」だけを持つようにしました。

ここで、よくある“やりすぎ”がこうです。

「社員もアルバイトも給料があるから、
給与計算も親にまとめちゃおう」

と考えて、Person に
「時給」「月給」「残業代」などを
全部押し込めてしまうパターンです。

一瞬「共通化できた!」と感じるのですが、
すぐにこうなります。

社員は月給+ボーナス
アルバイトは時給×時間
契約社員はまた別のルール

「共通にしたい気持ち」と
「実際は違うルールを持っている現実」
がぶつかります。

2日目は、
この「どこまで共通にするか」の線引きを
ちゃんと考えられるようになる日です。


例題1:Person に“共通にしすぎた”パターン

給与計算まで親に入れてしまった場合

まず、あえて悪い例を見てみます。

public class Person {
    protected String name;
    protected int age;
    protected int salary; // 給料

    public Person(String name, int age, int salary) {
        this.name = name;
        this.age = age;
        this.salary = salary;
    }

    public int calculateMonthlyPay() {
        return salary;
    }
}
Java

Employee と PartTimer をこう書いたとします。

public class Employee extends Person {
    public Employee(String name, int age, int salary) {
        super(name, age, salary);
    }
}
Java
public class PartTimer extends Person {
    public PartTimer(String name, int age, int hourlyWage, int hours) {
        super(name, age, hourlyWage * hours);
    }
}
Java

一見、動きます。
でも、すぐに問題が出ます。

アルバイトの勤務時間が月ごとに変わる
残業代や深夜手当をどうするか
社員のボーナスをどう扱うか

全部を salary という一つの数字に押し込めるのは、
「現実を無理やり共通化している」 状態です。

ここで大事なのは、

「共通にできるのは“本当に同じ意味のもの”だけ」
「無理に共通にすると、あとで設計がねじれる」

という感覚です。


例題1の改善:共通なのは「人としての情報」までにする

給与計算は、それぞれのクラスに任せる

Person は、
「人としての共通部分」だけを持つように戻します。

public class Person {
    protected String name;
    protected int age;

    public Person(String name, int age) {
        if (age < 0) {
            throw new IllegalArgumentException("年齢は0以上");
        }
        this.name = name;
        this.age = age;
    }

    public void introduce() {
        System.out.println("名前: " + name + " (" + age + "歳)");
    }
}
Java

Employee は、
「月給制の人」としての情報と振る舞いを持ちます。

public class Employee extends Person {
    private int monthlySalary;

    public Employee(String name, int age, int monthlySalary) {
        super(name, age);
        this.monthlySalary = monthlySalary;
    }

    public int calculateMonthlyPay() {
        return monthlySalary;
    }
}
Java

PartTimer は、
「時給制の人」としての情報と振る舞いを持ちます。

public class PartTimer extends Person {
    private int hourlyWage;
    private int workingHours;

    public PartTimer(String name, int age, int hourlyWage, int workingHours) {
        super(name, age);
        this.hourlyWage = hourlyWage;
        this.workingHours = workingHours;
    }

    public int calculateMonthlyPay() {
        return hourlyWage * workingHours;
    }
}
Java

ここでのポイントは、

Person は「人としての共通部分」だけ
給与計算は、それぞれのクラスに任せる

という線引きです。

「共通にするのは“本当に同じ世界のもの”だけ」
というルールが、ここで効いています。


重要ポイントの深掘り1:フィールドの共通化と“意味の共通化”は別物

名前が同じでも、意味が違えば共通にしない

継承でよくある勘違いが、

「同じフィールド名があるから親にまとめよう」
という発想です。

例えば、

User の id
Order の id
Product の id

どれも id という名前ですが、
意味はまったく違います。

これを「共通だから」と言って
親クラス Entity にまとめてしまうと、
すぐにこうなります。

User には「パスワード」がある
Order には「合計金額」がある
Product には「在庫数」がある

共通しているのは「ID を持っている」という一点だけ。
それを親にしてしまうと、
「なんでもかんでもぶら下がる謎の親クラス」 が生まれます。

2日目で覚えておいてほしいのは、

「共通にする」のは
「同じ名前」ではなく「同じ意味」 だということです。


例題2:画面コンポーネントの共通部分をまとめる

ボタンとラベルの共通部分

GUI をイメージしてみましょう。

ボタン(Button)
ラベル(Label)

どちらも「画面に表示されるもの」です。

継承なしで書くと、こうなります。

public class Button {
    private int x;
    private int y;
    private String text;

    public Button(int x, int y, String text) {
        this.x = x;
        this.y = y;
        this.text = text;
    }

    public void draw() {
        System.out.println("ボタン描画: (" + x + ", " + y + ") " + text);
    }
}
Java
public class Label {
    private int x;
    private int y;
    private String text;

    public Label(int x, int y, String text) {
        this.x = x;
        this.y = y;
        this.text = text;
    }

    public void draw() {
        System.out.println("ラベル描画: (" + x + ", " + y + ") " + text);
    }
}
Java

ここで共通部分を探すと、

位置(x, y)
表示テキスト(text)
「描画する」という行為

が見えてきます。

そこで、親クラス Component を作ります。

public class Component {
    protected int x;
    protected int y;
    protected String text;

    public Component(int x, int y, String text) {
        this.x = x;
        this.y = y;
        this.text = text;
    }

    public void draw() {
        System.out.println("コンポーネント描画: (" + x + ", " + y + ") " + text);
    }
}
Java

Button はこう。

public class Button extends Component {

    public Button(int x, int y, String text) {
        super(x, y, text);
    }

    @Override
    public void draw() {
        System.out.println("ボタン描画: (" + x + ", " + y + ") " + text);
    }
}
Java

Label はこう。

public class Label extends Component {

    public Label(int x, int y, String text) {
        super(x, y, text);
    }

    @Override
    public void draw() {
        System.out.println("ラベル描画: (" + x + ", " + y + ") " + text);
    }
}
Java

ここでは、

位置とテキストという「表示されるものとしての共通部分」を親にまとめて、
描画の具体的な内容は子で上書きしています。

「同じ世界(画面コンポーネント)の中の共通部分」
親に出しているので、自然な継承になっています。


重要ポイントの深掘り2:protected の意味と注意点

子クラスからだけ触れる“半公開”のフィールド

さっきの Component では、
x, y, textprotected にしました。

protected int x;
protected int y;
protected String text;
Java

protected は、
「同じクラス+サブクラスから見える」 という意味です。

Button や Label からは、
x, y, text に直接アクセスできます。

これは便利ですが、
同時に注意も必要です。

protected が増えすぎると、
子クラスが親の中身を触り放題になり、
カプセル化が弱くなります。

2日目の感覚としては、

まずは private を基本にする
「子クラスからどうしても触りたい」ものだけ protected にする

くらいのスタンスで十分です。

「継承したからといって、なんでもかんでも protected にしない」
これも、共通化とカプセル化のバランスの話です。


2日目の実践:共通部分を“言葉で”切り分けてからコードにする

先に日本語で「共通」と「違い」を書き出す

今日やってほしい練習は、とてもシンプルです。

似たクラスが2つあったら、
まずコードを書く前に、
日本語でこう書き出してみてください。

この2つに共通しているのは何か?
それは「同じ意味」で共通しているか?
それとも「たまたま同じ名前」なだけか?

共通している“意味”があるなら、
それを親クラスにまとめる。

違っている部分は、
子クラスに残す。

この「言葉で切り分ける」ひと手間を挟むだけで、
継承の設計はかなり安定します。


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

継承アプリ2日目で伝えたいのは、
「共通部分をまとめる」ときに、
“意味”をちゃんと見るクセをつけること
です。

同じフィールド名だから、ではなく
同じ世界・同じ役割だから、親にまとめる。

共通にするのは「人としての情報」まで。
給与計算のようにルールが違うものは、それぞれのクラスに任せる。

画面コンポーネントなら、
位置やテキストのような「表示されるものとしての共通部分」を親に出す。

この感覚が育っていると、
3日目以降の
「抽象クラス」「ポリモーフィズム」に
スムーズに入っていけます。

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