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;
}
}
JavaEmployee と PartTimer をこう書いたとします。
public class Employee extends Person {
public Employee(String name, int age, int salary) {
super(name, age, salary);
}
}
Javapublic 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 + "歳)");
}
}
JavaEmployee は、
「月給制の人」としての情報と振る舞いを持ちます。
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;
}
}
JavaPartTimer は、
「時給制の人」としての情報と振る舞いを持ちます。
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);
}
}
Javapublic 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);
}
}
JavaButton はこう。
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);
}
}
JavaLabel はこう。
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, text を protected にしました。
protected int x;
protected int y;
protected String text;
Javaprotected は、
「同じクラス+サブクラスから見える」 という意味です。
Button や Label からは、x, y, text に直接アクセスできます。
これは便利ですが、
同時に注意も必要です。
protected が増えすぎると、
子クラスが親の中身を触り放題になり、
カプセル化が弱くなります。
2日目の感覚としては、
まずは private を基本にする
「子クラスからどうしても触りたい」ものだけ protected にする
くらいのスタンスで十分です。
「継承したからといって、なんでもかんでも protected にしない」
これも、共通化とカプセル化のバランスの話です。
2日目の実践:共通部分を“言葉で”切り分けてからコードにする
先に日本語で「共通」と「違い」を書き出す
今日やってほしい練習は、とてもシンプルです。
似たクラスが2つあったら、
まずコードを書く前に、
日本語でこう書き出してみてください。
この2つに共通しているのは何か?
それは「同じ意味」で共通しているか?
それとも「たまたま同じ名前」なだけか?
共通している“意味”があるなら、
それを親クラスにまとめる。
違っている部分は、
子クラスに残す。
この「言葉で切り分ける」ひと手間を挟むだけで、
継承の設計はかなり安定します。
2日目で本当に掴んでほしいこと
継承アプリ2日目で伝えたいのは、
「共通部分をまとめる」ときに、
“意味”をちゃんと見るクセをつけること です。
同じフィールド名だから、ではなく
同じ世界・同じ役割だから、親にまとめる。
共通にするのは「人としての情報」まで。
給与計算のようにルールが違うものは、それぞれのクラスに任せる。
画面コンポーネントなら、
位置やテキストのような「表示されるものとしての共通部分」を親に出す。
この感覚が育っていると、
3日目以降の
「抽象クラス」「ポリモーフィズム」に
スムーズに入っていけます。


