5日目のゴール
継承アプリ5日目のテーマも
「共通部分をまとめる」。
でも今日は、
「どこまで継承でまとめて、どこからは“別の手段”を使うか」
という一歩深い話をします。
つまり、
継承の“気持ちよさ”と“危うさ”を両方知ったうえで、
ここは継承でまとめる
ここは継承を使わず、別のクラスに切り出す
という判断ができるようになるのが、5日目のゴールです。
継承は「強い結びつき」を生む
親が変わると、子が全部揺れる
まず、継承の性質をもう一度整理します。
extends で親クラスを決めると、
子クラスはその親の
フィールド
メソッド
設計方針
に強く結びつきます。
親のフィールド構成が変わると、
子のコードも影響を受ける。
親のメソッドの仕様が変わると、
子の振る舞いも変わる。
これは「再利用できて嬉しい」という側面と同時に、
「親の変更が子に波及しやすい」という怖さ も持っています。
5日目では、この「強い結びつき」を意識しながら、
継承を使う場面・使わない場面を考えていきます。
例題1:無理に「共通の親」を作った結果、苦しくなるパターン
User・Order・Product を「全部 Entity にする」世界
よくある“やりすぎ継承”の例を見てみます。
アプリに、こんなクラスがあるとします。
ユーザーを表す User
注文を表す Order
商品を表す Product
どれも「ID」を持っているからといって、
こういう親クラスを作ってしまうパターンがあります。
public class Entity {
protected long id;
public Entity(long id) {
this.id = id;
}
public long getId() {
return id;
}
}
Javaそして、全部を Entity にぶら下げる。
public class User extends Entity {
private String name;
public User(long id, String name) {
super(id);
this.name = name;
}
}
Javapublic class Order extends Entity {
private int totalPrice;
public Order(long id, int totalPrice) {
super(id);
this.totalPrice = totalPrice;
}
}
Javapublic class Product extends Entity {
private String title;
public Product(long id, String title) {
super(id);
this.title = title;
}
}
Java一見、「ID の共通部分をまとめた」ように見えます。
でも、ここで立ち止まってほしい。
User の ID
Order の ID
Product の ID
は、
「たまたま同じ名前」なだけで、
意味もライフサイクルもまったく違います。
それなのに、
全部を同じ親にぶら下げてしまうと、
Entity を前提にしたユーティリティが増える
Entity を継承していないクラスが“仲間外れ”になる
親の設計を変えづらくなる
という 「なんでもぶら下がる謎の親クラス」 が生まれます。
ここでの教訓は、
共通にしたいのは「フィールド名」ではなく「意味」
意味が違うものを、無理に同じ親にまとめない
ということです。
例題1の改善:「共通の処理」は別クラスに切り出す
継承ではなく“ヘルパー”にする選択肢
ID に関する共通処理が欲しいなら、
必ずしも継承でまとめる必要はありません。
例えば、
「ID を採番する仕組み」が欲しいなら、
こういうクラスを作ればいい。
public class IdGenerator {
private long nextId = 1;
public long generate() {
return nextId++;
}
}
JavaUser はこう使う。
public class User {
private final long id;
private String name;
public User(IdGenerator generator, String name) {
this.id = generator.generate();
this.name = name;
}
public long getId() {
return id;
}
}
JavaOrder も同じように使える。
public class Order {
private final long id;
private int totalPrice;
public Order(IdGenerator generator, int totalPrice) {
this.id = generator.generate();
this.totalPrice = totalPrice;
}
public long getId() {
return id;
}
}
Javaここでは、
ID の「採番ロジック」は IdGenerator にまとめる
User / Order / Product は、それぞれ独立したクラスのまま
という構造になっています。
つまり、
「共通部分をまとめる」ときに、
継承ではなく“別クラスに切り出す”という選択肢もある
ということです。
重要ポイントの深掘り1:「is-a」と「has-a」の違い
「〜である」と「〜を持っている」
継承を使うかどうかを考えるときに、
よく出てくるキーワードが
「is-a」
「has-a」
です。
Dog extends Animal は、
「犬は動物である(Dog is an Animal)」
という関係なので、継承が自然です。
Car と Engine の関係は、
「車はエンジンである」ではなく
「車はエンジンを持っている(Car has an Engine)」
なので、継承ではなく「フィールドとして持つ」が自然です。
User と IdGenerator の関係も同じです。
「ユーザーは ID 生成器である」ではなく
「ユーザーは ID を持っている」
だから、
継承ではなく「別クラスを持つ」形がしっくりきます。
5日目で掴んでほしいのは、
共通部分をまとめたいときに、
それが「〜である」関係なのか
「〜を持っている」関係なのか
を一度立ち止まって考えるクセです。
例題2:ログ出力の“共通部分”を継承ではなく別クラスにする
Logger と Formatter の分離
3日目で、
ログ出力の共通部分を継承でまとめました。
親クラス Logger がlog(...) の流れと format(...) を持ち、
子クラスが write(...) を実装する形でした。
別の設計として、
「フォーマットだけを別クラスに切り出す」
というやり方もあります。
まず、フォーマッタを作ります。
public class LogFormatter {
public String format(String level, String message) {
String timestamp = java.time.LocalDateTime.now().toString();
return "[" + level + "] " + timestamp + " - " + message;
}
}
JavaConsoleLogger はこう。
public class ConsoleLogger {
private final LogFormatter formatter;
public ConsoleLogger(LogFormatter formatter) {
this.formatter = formatter;
}
public void log(String level, String message) {
String formatted = formatter.format(level, message);
System.out.println(formatted);
}
}
JavaFileLogger はこう。
public class FileLogger {
private final LogFormatter formatter;
private final String filePath;
public FileLogger(LogFormatter formatter, String filePath) {
this.formatter = formatter;
this.filePath = filePath;
}
public void log(String level, String message) {
String formatted = formatter.format(level, message);
try (java.io.FileWriter writer = new java.io.FileWriter(filePath, true)) {
writer.write(formatted + System.lineSeparator());
} catch (java.io.IOException e) {
e.printStackTrace();
}
}
}
Javaここでは、
「ログのフォーマット」という共通部分は LogFormatter にまとめる
ConsoleLogger / FileLogger は、それぞれ独立したクラスのまま
という構造になっています。
継承を使ったバージョンと比べて、
親子関係の結びつきはない
代わりに「フォーマッタを持つ」という has-a 関係になっている
という違いがあります。
どちらが正解、という話ではなく、
「共通部分をまとめる方法は継承だけじゃない」
ということを体で感じてほしいのが、5日目です。
重要ポイントの深掘り2:継承は“後戻りしづらい”
一度親子関係にすると、外すのが大変
継承の怖さは、
一度親子関係を作ると、
それを外すのがけっこう大変だというところにあります。
親クラスに依存した子クラスが増える
親を前提にしたユーティリティが増える
テストも親子前提で書かれる
結果として、
「やっぱりこの親クラスいらなかったな」と気づいたときに、
大量のコードを直す必要が出てきます。
逆に、
「別クラスとして切り出す」形は、
後から差し替えたり、構造を変えたりしやすい。
LogFormatter を別クラスにしておけば、
フォーマットの仕様を変えるときも
差し替えやすい。
IdGenerator を別クラスにしておけば、
採番方法を変えるときも
他のクラスに影響を与えずに済む。
5日目で覚えておいてほしいのは、
継承は「強い結びつき」を生む
だからこそ、「本当に親子関係か?」をよく考えてから使う
という慎重さです。
5日目の実践:「継承でまとめたくなったら、別クラス案も一度考える」
いきなり extends に飛びつかない
今日やってみてほしいのは、
こういう小さな習慣です。
「共通部分があるから、親クラスを作ろう」と思ったら、
一度こう自分に聞いてみる。
これは「〜である」関係か?
それとも「〜を持っている」関係か?
「〜を持っている」なら、
別クラスに切り出して、フィールドとして持つ形にできないか?
例えば、
ID の共通処理 → IdGenerator に切り出す
フォーマットの共通処理 → Formatter に切り出す
計算ロジックの共通処理 → Calculator に切り出す
というふうに、
「共通部分をまとめる手段として、継承以外も持っておく」。
この一呼吸があるだけで、
継承の設計はかなり健全になります。
5日目で本当に掴んでほしいこと
継承アプリ5日目で伝えたいのは、
「共通部分をまとめる」ときに、
継承と“別クラスに切り出す”を意識的に選べる人になること
です。
継承は「〜である」関係に向いている。
「〜を持っている」関係なら、別クラスとして切り出す方が自然なことが多い。
親子関係は強い結びつきを生むので、
後から外すのが大変。
だからこそ、「本当に親か?」をよく考えてから extends を書く。
この感覚を持てているあなたは、
もう「継承を知っている人」ではなく、
「継承を設計の道具として、他の手段と並べて選べる人」 です。
この軸を持ったまま、
6日目・7日目では
継承とインターフェース、ポリモーフィズムの関係を
さらにクリアにしていけます。


