01.4 インターフェース — 抽象化とポリモーフィズム¶
01.3 で、犬・猫・鳥はそれぞれ違う Speak を持ちました。それらを共通の型として扱いたい
(「動物の集まり」に対して一括で鳴かせたい)場合、どうすれば良いでしょうか?
Go では interface(インターフェース) を使って「共通の契約」を定義します。
このレッスンのゴール:
- interface を定義して「これだけのメソッドを持てば良い」という契約を作れる
- interface を満たす型を、共通の型として扱える(ポリモーフィズム)
- 呼び出し側が「具体的な型」でなく「interface」に依存する設計(依存の反転の準備)を理解する
1. 抽象化とは¶
抽象化(abstraction)とは、複数の具体から「共通の本質」を取り出すことです。
- メール・SMS・チャットへの通知は、どれも「メッセージを送る」という共通の本質を持つ
- 犬・猫・鳥の鳴き声は、どれも「鳴く」という共通の本質を持つ
interface はこの「共通の本質」を メソッドの契約として書き下したものです。
2. interface の定義と実装¶
Notifier という interface を定義します。「Notify(message string) を持っていれば Notifier として扱える」という契約です。
Go の interface は「宣言して実装する」のではなく、メソッドを持っていれば自動的に満たす(ダックタイピング)のが特徴です。
import (
"errors"
"fmt"
"strings"
"github.com/janpfeifer/gonb/gonbui"
)
// ErrUnanswered は、練習問題が未回答のときにプレースホルダ関数が返す特別なエラー。
var ErrUnanswered = errors.New("未回答: この関数はまだ実装されていません")
// Notifier は「Notify メソッドを持つ型」の契約。
type Notifier interface {
Notify(message string) error
}
type EmailNotifier struct {
address string
}
func (n EmailNotifier) Notify(message string) error {
fmt.Printf("[Email → %s] %s\n", n.address, message)
return nil
}
type SMSNotifier struct {
phone string
}
func (n SMSNotifier) Notify(message string) error {
fmt.Printf("[SMS → %s] %s\n", n.phone, message)
return nil
}
type LogNotifier struct{}
func (n LogNotifier) Notify(message string) error {
fmt.Printf("[Log] %s\n", message)
return nil
}
ここがポイント¶
EmailNotifierもSMSNotifierもLogNotifierも、「Notify メソッドを持っている」だけでNotifierとして扱えますimplementsのような宣言は不要です(構造的型付け / ダックタイピング)
3. ポリモーフィズム: 同じコードが実装ごとに動く¶
SendAlert は Notifier を受け取ります。つまり「何の通知方法か」を知らなくても、
「Notify できるもの」なら何でも受け取れます。
// SendAlert は具体的な通知方法を知らずに、Notifier にだけ依存する
func SendAlert(n Notifier, message string) error {
return n.Notify(message)
}
%%
notifiers := []Notifier{
EmailNotifier{address: "taro@example.com"},
SMSNotifier{phone: "090-1234-5678"},
LogNotifier{},
}
for _, n := range notifiers {
_ = SendAlert(n, "サーバー障害が発生しました")
}
fmt.Println("→ 呼び出し側の SendAlert は 1 つだけ。実装を切り替えても動く(ポリモーフィズム)")
[Email → taro@example.com] サーバー障害が発生しました [SMS → 090-1234-5678] サーバー障害が発生しました [Log] サーバー障害が発生しました → 呼び出し側の SendAlert は 1 つだけ。実装を切り替えても動く(ポリモーフィズム)
通知方法ごとの出力先を表で見える化します。
func notifierTable() string {
var b strings.Builder
b.WriteString(`<table border="1" cellpadding="4" style="border-collapse:collapse">
<tr><th>実装</th><th>保持するデータ</th><th>Notify の出力先</th></tr>`)
b.WriteString(`<tr><td>EmailNotifier</td><td>address</td><td>メールアドレスへ送信</td></tr>`)
b.WriteString(`<tr><td>SMSNotifier</td><td>phone</td><td>電話番号へ送信</td></tr>`)
b.WriteString(`<tr><td>LogNotifier</td><td>(なし)</td><td>ログへ出力</td></tr>`)
b.WriteString(`</table>`)
return b.String()
}
%%
gonbui.DisplayHTML(notifierTable())
gonbui.Sync()
| 実装 | 保持するデータ | Notify の出力先 |
|---|---|---|
| EmailNotifier | address | メールアドレスへ送信 |
| SMSNotifier | phone | 電話番号へ送信 |
| LogNotifier | (なし) | ログへ出力 |
読んでください: SendAlert のコードは一切変わっていません。notifiers の中身を変えるだけで、
振る舞いだけが変わります。これが「同じコードが型によって異なる振る舞いをする」ポリモーフィズムです。
4. つまずきポイント: interface は「大きすぎ」にしない¶
interface にメソッドを詰め込みすぎると、満たす型が増えず、抽象化の意味が薄くなります。
Notifierは 1 つのメソッドだけ。満たすのが簡単 → 自由度が高い- もし
Notifyに加えてSetChannelやCheckHealthまで要求すると、LogNotifierは満たせなくなる
「今のコードが必要とする最小のメソッドだけ」 を interface に置くのが Go の慣習です(後述の 01.6 で依存の反転と合わせて詳しく見ます)。
5. 直感・類推: 電源プラグの規格¶
家電製品(具体の型)は、コンセント(interface)の規格に合っていれば、どんな製品でも どの部屋のコンセントにも差し込めます。
- コンセントは「プラグの形」という契約だけを決めている
- コンセント側は中身(ドライヤーかテレビか)を知らない
- 新しい家電を買っても、コンセントの規格は変えない
バックエンドでは、外部サービス(メール・決済・DB)を interface に抽象化しておくと、 実装を差し替えても(テスト用モック → 本番実装など)呼び出し側を変えずに済みます。
練習問題 1.4: SlackNotifier を追加しよう¶
新しい通知方法 SlackNotifier を実装してください。
仕様:
type SlackNotifier struct {
channel string
messages []string // 送ったメッセージを記録する
}
func (s *SlackNotifier) Notify(message string) error
Notifyはfmt.Printf("[Slack → %s] %s\n", s.channel, message)で出力するs.messagesにmessageを追加してnilを返す
ヒント: 01.4 で学んだ通り、Notify のシグネチャを Notifier と一致させるだけで、
SlackNotifier は自動的に Notifier として扱えます。
// YOUR CODE HERE
// type SlackNotifier struct {...} と Notify メソッドを実装してください。
// (未実装のままチェックセルを実行すると「未回答」と表示されます)
type SlackNotifier struct {
channel string
messages []string
}
func (s *SlackNotifier) Notify(message string) error {
return ErrUnanswered
}
コンパイル時の interface 保証(Go のイディオム)¶
var _ Notifier = (*SlackNotifier)(nil) と書くと、コンパイル時に
「SlackNotifier が Notifier を満たしているか」を保証できます。
// コンパイル時に SlackNotifier が Notifier を満たすことを確認する
var _ Notifier = (*SlackNotifier)(nil)
チェックのためのヘルパー¶
import "reflect"
import "fmt"
func mustEqual(got, want any, name string) {
if reflect.DeepEqual(got, want) {
fmt.Printf("✅ Passed: %s\n", name)
return
}
panic(fmt.Sprintf("❌ %s\n got = %v (%T)\n want = %v (%T)", name, got, got, want, want))
}
%%
slack := &SlackNotifier{channel: "#general"}
err := SendAlert(slack, "障害発生")
if errors.Is(err, ErrUnanswered) {
fmt.Println("⚠️ 未回答: 練習問題を解いてから、このセルを再度実行してください")
} else {
mustEqual(err, nil, "SendAlert がエラーなし")
mustEqual(slack.messages, []string{"障害発生"}, "メッセージが記録された")
mustEqual(len(slack.messages), 1, "1 件だけ記録")
fmt.Println("🎉 すべてのチェックが通りました")
}
⚠️ 未回答: 練習問題を解いてから、このセルを再度実行してください
まとめ¶
- interface = 「必要なメソッド」の契約。メソッドを持てば自動的に満たす
- 呼び出し側は具体的な型でなく interface に依存する → 実装を差し替えても動く(ポリモーフィズム)
- interface は必要最小限のメソッドにする
var _ Interface = (*Type)(nil)でコンパイル時に満たすことを保証できる
答え合わせは 01.4-interfaces-abstraction-polymorphism-solutions.ipynb で行ってください。
次は 01.5 で「ジェネリクス」を学びます。