抽象化の基本と実践
📋 この記事の目次
前回は「Ticketクラス自身が持つ処理の中にも、種類ごとに実装を強制したい部分が残っている」という宿題を残しました。今回はまず、TicketCraftの「チケットが更新されたら、担当者に通知する」機能を題材に抽象化の考え方を学び、後半でその宿題(Ticket自身の設計)にも触れます。今のコードは、こうなっています。
public void notifyOnUpdate(Ticket ticket) {
// メール送信のロジックがそのままベタ書きされている
SmtpClient smtp = new SmtpClient("mail.example.com");
smtp.send(ticket.getAssignee() + "@example.com", "チケット更新: " + ticket.getTitle());
}
先週、「Slackにも通知してほしい」という要望が来ました。ところが、このnotifyOnUpdateを呼び出している箇所は社内に7つあり、Slack通知を追加するには7箇所すべてに手を入れる必要があります。今回は抽象化で、この「呼び出し側は通知の中身を気にしなくていい」状態を作ります。
抽象化は「リモコンのボタンだけ見せる」ことだと考える
テレビのリモコンを使うとき、私たちは「電源ボタンを押す」だけで済みます。ボタンの先で赤外線がどう送信され、テレビの基板がどう反応しているかを知る必要はありません。
抽象化とは、この 「本質的な操作(ボタン)だけを見せて、中の仕組みは隠す」 という考え方です。TicketCraftの通知処理も、呼び出し側からは「通知する」というボタンだけが見えていて、その先がメールなのかSlackなのかは知らなくてよい状態を目指します。
実践:通知処理を抽象化する
Step 1: インターフェースで「本質的な操作」だけを定義する
public interface NotificationChannel {
void notify(Ticket ticket);
}
「通知する」というボタンだけを定義し、中身の実装はまだ何も書きません。
Step 2: 具体的な実装クラスを用意する
public class EmailNotificationChannel implements NotificationChannel {
@Override
public void notify(Ticket ticket) {
SmtpClient smtp = new SmtpClient("mail.example.com");
smtp.send(ticket.getAssignee() + "@example.com", "チケット更新: " + ticket.getTitle());
}
}
public class SlackNotificationChannel implements NotificationChannel {
@Override
public void notify(Ticket ticket) {
SlackClient slack = new SlackClient("#ticketcraft-notify");
slack.postMessage(ticket.getAssignee() + "さん、チケットが更新されました: " + ticket.getTitle());
}
}
Step 3: 呼び出し側は「本質的な操作」だけを使う
public void notifyOnUpdate(Ticket ticket, NotificationChannel channel) {
channel.notify(ticket);
}
// 呼び出し例
notifyOnUpdate(ticket, new EmailNotificationChannel());
notifyOnUpdate(ticket, new SlackNotificationChannel());
notifyOnUpdateのコードは、通知手段がメールかSlackか、あるいは将来増えるかもしれない別の手段かを一切知りません。新しい通知手段(例: 社内チャットツール)を追加したくなっても、NotificationChannelを実装した新しいクラスを1つ追加するだけで済み、7箇所の呼び出し元は無修正のままです。
抽象クラスとインターフェース、どちらを使うか
ここまでインターフェースを使ってきましたが、Ticketのような継承階層では抽象クラスも使えます。使い分けの目安は次の通りです。
| 観点 | 抽象クラス | インターフェース |
|---|---|---|
| 共通の実装(コード)を持たせたいか | 持たせられる | Java8以降は一部持てるが基本は持たせない |
| 複数同時に実装させたいか | 単一継承のみ(1つしか継承できない) | 複数のインターフェースを同時に実装できる |
| 「is-a関係」か「can-do関係」か | is-a(BugTicket is a Ticket) |
can-do(BugTicket can notify) |
public abstract class Ticket {
private String title;
public Ticket(String title) {
this.title = title;
}
// 共通の具象メソッド(すべての子クラスで共通の実装)
public String getTitle() {
return title;
}
// 抽象メソッド(子クラスに実装を強制する)
public abstract boolean isUrgent();
}
isUrgent()を抽象メソッドにすると、BugTicketやFeatureTicketは必ずこのメソッドを実装しなければならず、実装を忘れた場合はコンパイル時にエラーとして検出されます。「実装漏れを実行前に機械的に見つけられる」のが、抽象化の地味に強力な効能です。
🧓 ベテランの現場コラム 「とりあえず全部インターフェースにしておけば柔軟性が高まる」と考えて、なんでもインターフェース化する人がいますが、実装が1つしかない・将来も増える予定がないものまで抽象化すると、コードを読む際に「実体がどこにあるか」を探す手間が増えるだけになります。TicketCraftの通知先のように、「実装が複数存在し、今後も増えそうな箇所」 にこそ抽象化を使うのが効果的です。
🧓 ベテランの現場コラム OOP基礎編もこれで最終回です。振り返ると、
Ticketクラスは「public フィールドで誰でも書き換え放題」から、「カプセル化で守られ」「継承で種類ごとに整理され」「ポリモーフィズムで呼び出し側の分岐が消え」「抽象化で拡張ポイントが明確になった」という道のりを辿りました。この4つはバラバラの知識ではなく、1つのコードを段階的に良くしていくための一連の手順だと捉えると記憶に定着しやすいです。
挫折ポイントTOP3と対処法
1. 抽象クラスとインターフェースのどちらを使うか迷い続ける
「共通の実装コードを持たせたいか」「複数同時に実装させたいか」の2点で機械的に判断しましょう。
2. 抽象メソッドを増やしすぎる
すべての子クラスに本当に実装させたいメソッドだけを抽象化しましょう。共通の実装で済むものは具象メソッドのままにしておいて構いません。
3. 「とりあえず抽象化」で実装が1つしかないインターフェースを量産する
実装が1つしかなく、将来も増える見込みがないものは、無理に抽象化しなくても構いません。
この先、何を学べばいいか
OOP基礎編はこれで完結です。次回からはJava8モダン文法編に進み、ラムダ式やStream APIを使って、TicketCraftのコードをより簡潔に書き直していきます。
まとめ
- 抽象化は「本質的な操作だけを見せて、中の実装を隠す」考え方
- 抽象クラスは共通実装+is-a関係、インターフェースは複数実装可能なcan-do関係、が使い分けの目安
- 抽象メソッドにより、実装漏れをコンパイル時に検出できる
- OOP基礎編4原則は、1つのコードを段階的に改善する一連の手順として身につけると定着しやすい
🎓 理解度テスト
学んだ内容を確認しましょう。全5問。