インターフェースとは?レストランの注文システムで理解する抽象化の基本
こんにちは、とまだです。
「インターフェース」って聞いたことはあるけど、正直よくわからない…。
そんな風に思ったことはありませんか?
実は私も最初は「クラスと何が違うの?」と混乱していました。
でも、レストランでの注文の仕組みを思い浮かべてみると、意外とすんなり理解できるんです。
今回は、インターフェースの基本的な考え方から実践的な使い方まで、具体例を交えながら解説していきます。
インターフェースは「約束事」
インターフェースを一言で表すなら「約束事」です。
レストランに例えてみましょう。
どのレストランでも「注文を受ける」という機能があります。
でも、実際の注文方法は店によって違いますよね。
- ファミレスではタブレットで注文
- 高級レストランではウェイターが聞きに来る
- 回転寿司では注文パネルを使う
「注文を受ける」という約束は同じ。
でも、やり方は自由。
これがインターフェースの考え方です。
なぜインターフェースが必要なの?
プログラミングでは、コードを再利用したいことがよくあります。
でも、具体的な実装に依存していると、ちょっとした変更で全体を書き直すことに。
そこでインターフェースの出番です。
たとえば、メッセージ送信機能を考えてみましょう。
最初はメールだけでよかったのに、あとからLINEやSlackにも対応したくなった。
インターフェースがあれば、送信方法を追加するだけで済みます。
呼び出し側のコードは一切変更不要。
これが大きなメリットです。
実際のコードで見てみよう(Java編)
まずはインターフェースの定義から。
public interface MessageSender {
void sendMessage(String message);
}
これは「sendMessageというメソッドを持つこと」という約束を定義しています。
次に、この約束を守る実装クラスを作ります。
public class EmailSender implements MessageSender {
@Override
public void sendMessage(String message) {
System.out.println("Email送信: " + message);
}
}
public class SlackSender implements MessageSender {
@Override
public void sendMessage(String message) {
System.out.println("Slack送信: " + message);
}
}
EmailSenderもSlackSenderも、同じ約束を守っています。
でも、実際の送信処理は違う。
これがインターフェースの力です。
TypeScriptでの活用例
TypeScriptでは、もっと身近な例で考えてみましょう。
決済処理のインターフェースです。
interface PaymentProcessor {
process(amount: number): void;
}
class CreditCardPayment implements PaymentProcessor {
process(amount: number): void {
console.log(`クレジットカードで ${amount} 円を支払いました`);
}
}
class PayPalPayment implements PaymentProcessor {
process(amount: number): void {
console.log(`PayPalで ${amount} 円を支払いました`);
}
}
どの決済方法でも「金額を受け取って処理する」という約束は同じ。
だから、利用する側はどの決済方法かを気にしなくていいんです。
インターフェースと抽象クラスはどう違う?
よく比較される概念に「抽象クラス」があります。
簡単に言うと、こんな違いがあります。
インターフェースは「何をするか」だけを決める純粋な約束事。
一方、抽象クラスは「一部の実装も含められる」という特徴があります。
また、インターフェースは複数実装できるのも大きな違い。
「走れる」と「泳げる」という2つのインターフェースを、1つのクラスで実装できるんです。
実務でよく使われる場面
実際の開発現場では、こんな場面でインターフェースが活躍します。
ログ出力は典型的な例です。
開発中はコンソールに出力。
本番環境ではファイルに保存。
エラーが起きたらクラウドに送信。
インターフェースを使えば、これらを簡単に切り替えられます。
public interface ILogger {
void Log(string message);
}
public class ConsoleLogger : ILogger {
public void Log(string message) {
Console.WriteLine($"[LOG] {message}");
}
}
public class FileLogger : ILogger {
public void Log(string message) {
// ファイルに書き込む処理
File.AppendAllText("app.log", $"{DateTime.Now}: {message}\n");
}
}
環境に応じてログの出力先を変えたい。
そんなときに威力を発揮します。
テストが楽になる理由
インターフェースを使うと、テストも書きやすくなります。
メール送信機能をテストしたいとき。
本当にメールを送ってしまったら困りますよね。
そこで、テスト用の「偽物」を作ります。
public class MockEmailSender implements MessageSender {
private List<String> sentMessages = new ArrayList<>();
@Override
public void sendMessage(String message) {
sentMessages.add(message);
}
public boolean wasMessageSent(String message) {
return sentMessages.contains(message);
}
}
本物と同じインターフェースを実装しているから、差し替えるだけでOK。
テストでは偽物を使い、本番では本物を使う。
これがモックオブジェクトの考え方です。
インターフェースを使うときの注意点
便利なインターフェースですが、使いすぎは禁物です。
すべてをインターフェースにすると、かえって複雑になることも。
実装が1つしかないのにインターフェースを作る。
これは過度な抽象化かもしれません。
「将来的に実装が増えそうか」を考えてから導入しましょう。
また、メソッドが多すぎるインターフェースも避けたいところ。
必要最小限に絞ることで、実装する側も楽になります。
APIでも同じ考え方
Web APIでも「インターフェース」という言葉を聞きますよね。
これも基本的な考え方は同じです。
「このURLにこのデータを送ったら、こんな結果が返ってくる」
という約束事を決めているだけ。
実際の処理がどうなっているかは、利用者は知らなくていい。
プログラミングのインターフェースと同じ発想なんです。
効果的な使い方のコツ
インターフェースを効果的に使うには、いくつかのコツがあります。
まず、役割を明確にすること。
「何のための約束か」がはっきりしていないと、使いづらくなります。
次に、メソッドは必要最小限に。
あれもこれも詰め込むと、実装が大変になります。
最後に、実装の差し替えパターンを想定しておくこと。
「どんな実装が考えられるか」を事前に考えておくと、適切な設計ができます。
まとめ
インターフェースは「約束事」を定義する仕組み。
レストランの注文システムのように、やり方は自由でも守るべきルールは同じ。
この考え方を理解すれば、柔軟で拡張しやすいコードが書けるようになります。
実装の差し替えが簡単になる。
テストが書きやすくなる。
チーム開発がスムーズになる。
これらのメリットを活かして、ぜひ実際のコードで試してみてください。
最初は難しく感じるかもしれません。
でも、一度理解すれば強力な武器になります。
まずは小さなプロジェクトで、1つインターフェースを作ってみることから始めてみてはいかがでしょうか。
著者について

とまだ
フルスタックエンジニア
Learning Next の創設者。Ruby on Rails と React を中心に、プログラミング教育に情熱を注いでいます。初心者が楽しく学べる環境作りを目指しています。
著者の詳細を見る →