3日目のゴール
継承アプリ3日目のテーマも
「共通部分をまとめる」。
1日目:
似たクラスの「明らかな共通部分」を親に出す感覚。
2日目:
「どこまで共通にして、どこから別物として扱うか」の線引き。
3日目はここから一歩進んで、
「共通部分を“コード”ではなく“振る舞い”としてまとめる」
ところまで行きます。
キーワードは
「メソッドの共通化」と「テンプレートメソッド的な考え方」です。
共通にしたいのは「処理の流れ」そのもの
似ているのは“中身”ではなく“流れ”のときがある
まず、こういう状況をイメージしてください。
ファイルにログを書くクラスが2つあるとします。
コンソールログ
ファイルログ
どちらも、やっていることの流れは似ています。
時刻を付ける
メッセージを整形する
どこかに出力する
ただし、「どこに出力するか」だけが違う。
こういうとき、
「処理の流れは共通にしたいけど、一部だけ変えたい」
という欲が出てきます。
これを継承でまとめるのが、
3日目のテーマです。
例題1:ログ出力クラスを“コピペで”書いてみる
まずは継承なしで、似たコードを並べてみる
継承を使う前に、あえてベタに書いてみます。
public class ConsoleLogger {
public void log(String level, String message) {
String timestamp = now();
String formatted = format(level, timestamp, message);
System.out.println(formatted);
}
private String now() {
return java.time.LocalDateTime.now().toString();
}
private String format(String level, String timestamp, String message) {
return "[" + level + "] " + timestamp + " - " + message;
}
}
Javapublic class FileLogger {
private String filePath;
public FileLogger(String filePath) {
this.filePath = filePath;
}
public void log(String level, String message) {
String timestamp = now();
String formatted = format(level, timestamp, message);
try (java.io.FileWriter writer = new java.io.FileWriter(filePath, true)) {
writer.write(formatted + System.lineSeparator());
} catch (java.io.IOException e) {
e.printStackTrace();
}
}
private String now() {
return java.time.LocalDateTime.now().toString();
}
private String format(String level, String timestamp, String message) {
return "[" + level + "] " + timestamp + " - " + message;
}
}
Javaここで感じてほしいのは、
now() が丸かぶりformat(...) も丸かぶりlog(...) の前半(時刻取得〜整形)もほぼ同じ
という 「処理の流れレベルでの共通部分」 です。
違うのは、
最後の「どこに書くか」だけ。
例題1の共通部分を“流れ”として抜き出す
「ログを書く手順」を親クラスにまとめる
ここで、共通している“流れ”を
親クラスにまとめてみます。
public abstract class Logger {
public void log(String level, String message) {
String timestamp = now();
String formatted = format(level, timestamp, message);
write(formatted); // 最後だけ、子クラスに任せる
}
protected String now() {
return java.time.LocalDateTime.now().toString();
}
protected String format(String level, String timestamp, String message) {
return "[" + level + "] " + timestamp + " - " + message;
}
protected abstract void write(String formattedMessage);
}
Javaここでやっていることは、こうです。
log(...) の「流れ」は親が持つ。
「どこに書くか」は write(...) として、子に任せる。now() と format(...) も親で共通化する。
つまり、
「テンプレート(型)となる流れ」を親に置いて、
一部だけ子に差し替えさせる 形です。
子クラス側:差があるところだけ実装する
ConsoleLogger と FileLogger を継承で書き直す
ConsoleLogger はこうなります。
public class ConsoleLogger extends Logger {
@Override
protected void write(String formattedMessage) {
System.out.println(formattedMessage);
}
}
JavaFileLogger はこう。
public class FileLogger extends Logger {
private String filePath;
public FileLogger(String filePath) {
this.filePath = filePath;
}
@Override
protected void write(String formattedMessage) {
try (java.io.FileWriter writer = new java.io.FileWriter(filePath, true)) {
writer.write(formattedMessage + System.lineSeparator());
} catch (java.io.IOException e) {
e.printStackTrace();
}
}
}
Java外からの使い方は、どちらも同じです。
Logger logger1 = new ConsoleLogger();
logger1.log("INFO", "コンソールに出したいログ");
Logger logger2 = new FileLogger("app.log");
logger2.log("ERROR", "ファイルに出したいログ");
Javaここでのポイントは、
log(...) の「手順」は完全に共通
違うのは write(...) の中身だけ
外から見たときの使い方は同じ
という状態になっていることです。
「共通部分をまとめる」が、
「処理の流れレベル」にまで広がった 形です。
重要ポイントの深掘り1:抽象クラスと“穴あきメソッド”
「ここだけは子に書いてもらう」という設計
親クラス Logger は abstract になっています。
public abstract class Logger {
...
protected abstract void write(String formattedMessage);
}
Javaabstract メソッドは、
「ここは親では実装しない。子クラスに必ず書かせる」
という“穴あきメソッド”です。
これによって、
ログの流れ(log)は親が完全に握る
「どこに書くか」だけは、子が責任を持つ
という役割分担ができます。
3日目で掴んでほしいのは、
「共通部分をまとめる」ときに、
「全部を親に書く」か「一部だけ子に任せる」か
を選べる、という感覚です。
重要ポイントの深掘り2:共通化の“粒度”を上げる
「行単位」ではなく「意味のある手順単位」でまとめる
1日目・2日目の共通化は、
主に「フィールド」や「単純なメソッド」でした。
名前と年齢
位置とテキスト
自己紹介メソッド
3日目では、
「意味のある手順」そのものを共通化する
ところまで来ています。
ログ出力なら、
時刻を取る
メッセージを整形する
出力する
という「手順」。
これを log(...) という一つのメソッドにまとめて、
その中の一部だけを子に任せる。
この「粒度の高い共通化」ができると、
コードの重複が減るだけでなく、
「処理の流れが一箇所に集まる」 ので
読みやすさ・変更のしやすさが一気に上がります。
例題2:レポート出力の“テンプレート”を共通化する
PDF レポートと CSV レポート
もう一つ、テンプレート的な共通化の例を出します。
レポートを出力するクラスが2つあるとします。
PDF レポート
CSV レポート
どちらも、やっていることの流れは似ています。
データを集める
ヘッダーを書く
本文を書く
フッターを書く
ファイルとして保存する
違うのは「どういう形式で書くか」だけ。
まず、継承なしで書くとこうなります(イメージ)。
public class PdfReport {
public void export() {
List<String> data = collectData();
writeHeader();
writeBody(data);
writeFooter();
saveAsPdf();
}
// collectData, writeHeader, writeBody, writeFooter, saveAsPdf ...
}
Javapublic class CsvReport {
public void export() {
List<String> data = collectData();
writeHeader();
writeBody(data);
writeFooter();
saveAsCsv();
}
// collectData, writeHeader, writeBody, writeFooter, saveAsCsv ...
}
Javaexport() の流れがほぼ同じです。
これを、親クラスにまとめます。
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();
}
JavaPDF 用の子クラス。
public class PdfReport extends Report {
@Override
protected List<String> collectData() {
// PDF 用のデータ収集
return List.of("PDFデータ1", "PDFデータ2");
}
@Override
protected void writeHeader() {
System.out.println("PDFヘッダーを書く");
}
@Override
protected void writeBody(List<String> data) {
System.out.println("PDF本文を書く: " + data);
}
@Override
protected void writeFooter() {
System.out.println("PDFフッターを書く");
}
@Override
protected void save() {
System.out.println("PDFとして保存");
}
}
JavaCSV 用の子クラス。
public class CsvReport extends Report {
@Override
protected List<String> collectData() {
return List.of("CSVデータ1", "CSVデータ2");
}
@Override
protected void writeHeader() {
System.out.println("CSVヘッダーを書く");
}
@Override
protected void writeBody(List<String> data) {
System.out.println("CSV本文を書く: " + data);
}
@Override
protected void writeFooter() {
System.out.println("CSVフッターを書く");
}
@Override
protected void save() {
System.out.println("CSVとして保存");
}
}
Java外からは、こう使えます。
Report report1 = new PdfReport();
report1.export();
Report report2 = new CsvReport();
report2.export();
Javaここでも、
export() の「流れ」は完全に共通
中身の具体的な書き方だけ、子クラスごとに違う
という構造になっています。
3日目の実践:「流れが似ているメソッド」を探してみる
コピペしている“手順”を見つけたら、テンプレート化を疑う
今日やってみてほしいのは、
自分のコードの中から
「前半と後半は同じで、真ん中だけ違う」
「ほぼ同じ手順を、クラスごとにコピペしている」
というメソッドを探すことです。
見つけたら、こう考えてみてください。
この手順、親クラスに doSomething() としてまとめられないか?
その中の「ここだけ違う」部分を、abstract メソッドにできないか?
つまり、
「流れを親に、差分を子に」
という形にできないかを考えてみる。
これができると、
継承は「フィールドの共通化の道具」から
「処理の流れをデザインする道具」 に変わります。
3日目で本当に掴んでほしいこと
継承アプリ3日目で伝えたいのは、
「共通部分をまとめる」の対象が
“データ”から“振る舞い(手順)”に広がる感覚 です。
ログ出力の流れを親にまとめる。
レポート出力の流れを親にまとめる。
親クラスは「テンプレート(型)」を持つ。
子クラスは「違うところだけ」を埋める。
この「流れレベルの共通化」が見えてくると、
継承は一気に“設計の道具”らしくなります。
次のステップでは、
このテンプレート的な継承と
「ポリモーフィズム(多態性)」がつながっていきます。


