1. はじめに
さて、インターフェースってめっちゃ強力なコントラクトだってことはもう分かったよね。じゃあインターフェースレベルでプログラミングって何?これは設計の哲学で、「具体的な実装じゃなくて、抽象に依存しよう」ってことなんだ。
また例え話しよう。コーヒーを作りたいとする。コーヒーマシンを買うよね。でもさ、具体的な「スーパー・プーパー・マシンV3000」でも「メガ・オート・バリスタ5000」でも、どっちでもいいでしょ?大事なのは、そのマシンが「コーヒーを作れる」ってこと、つまり「ICoffeeMaker」コントラクトを実装してるってこと。君は「コーヒー作る」ボタンを押すだけでOK。どうやって作るか(豆を挽くのか、カプセルなのか、他の方法なのか)はユーザーとしては気にしないよね。
コードで言うと、メソッドでDocumentとかImageみたいな具体的な型を要求する代わりに、IPrintableを要求するってこと。君のコードは、このインターフェースを実装してるどんなオブジェクトとも動くし、その具体的な型を知らなくてもいいんだ。
インターフェースのコントラクトってのは、そのインターフェースを実装するクラスは必ず全部のメンバー(メソッド、プロパティ、イベント)を定義しなきゃいけないって保証だよ。プログラムから見れば、インターフェースを実装してるオブジェクトは「ある機能セット」を持ってるって信じてOKで、中身がどうなってるか知らなくても大丈夫。
例えば、君が同僚と「いつもロビーの赤いソファで会おう」って約束したとする。彼がどうやってそこに来るか(エレベーター、階段、壁を走る…)はどうでもいい。大事なのは、彼がそこにいるってこと。インターフェースはコードにとっての「赤いソファ」なんだ。
例
public interface IPrintable
{
void Print();
}
コントラクト:IPrintableを実装するやつは、必ず自分を画面にプリントできなきゃいけない。やり方は自由。
2. コントラクトはシステムのパーツ同士の接点
実際どう動くの?
システムは、いろんな人が、いろんな時に、いろんな目的で作ったクラスが何十個もあるかもしれない。でも、みんな同じコントラクト(つまり同じインターフェース)を守ってれば、同じように使えるんだ。
リアルなアプリの例:顧客データベースを想像してみて。Customerクラス、Employeeクラス、Contractorクラス。それぞれ情報の持ち方は違うけど、全部が例えばIContactInfoインターフェースを実装してて、GetEmail()とGetPhoneNumber()メソッドを持ってれば、外から見たら型が何かなんて気にしなくていい。大事なのは、emailと電話番号が取れること。
public interface IContactInfo
{
string GetEmail();
string GetPhoneNumber();
}
public class Customer : IContactInfo
{
public string Email { get; set; }
public string Phone { get; set; }
public string GetEmail() => Email;
public string GetPhoneNumber() => Phone;
}
// EmployeeやContractorも同じように...
これで、会社がコンタクトある全員のデータをプリントしたい時(誰でもOK)、IContactInfoのリストをループして必要なメソッド呼ぶだけで全部動く。
3. 「インターフェースレベルでプログラミング」
インターフェースレベルでプログラミングするってのは、具体的なクラスじゃなくてインターフェース(つまりコントラクト)だけに依存してコードを書くってこと。インターフェースを実装してるクラスなら、どんなやつでもOK。
なぜ大事?
- スケーラビリティ:新しい型を追加するのが簡単。既存コードを変えなくていい。
- テストしやすさ:テスト時にモック(ダミー)オブジェクトに簡単に差し替えられる。
- 柔軟性:実装は毎日変わっても、インターフェースは安定してる。
- アーキテクチャの綺麗さ:モジュール同士の結びつきが弱くて、再利用しやすい。
例 — おなじみの「銀行口座」プログラム
前のアプリ例では、BankAccountって抽象クラスに抽象メソッドWithdraw()があって、具体的な型(SavingsAccountやCheckingAccount)が詳細を実装してたよね。
今度はインターフェースを追加してみよう。例えば残高情報を出力するためのやつ:
public interface IBalanceReporter
{
void ReportBalance();
}
public abstract class BankAccount : IBalanceReporter
{
public double Balance { get; set; }
public abstract void Withdraw(double amount);
public void ReportBalance()
{
Console.WriteLine($"現在の残高: {Balance} ユーロ");
}
}
これでIBalanceReporterで動くやつは、アカウントの具体的な型に関係なくReportBalance()を呼べる。
4. コントラクトによる共通処理とポリモーフィズム
例:共通ハンドラー
インターフェースでやりとりするなら、オブジェクトの型に依存しない汎用メソッドが作れるよ:
static void PrintAllBalances(IBalanceReporter[] accounts)
{
foreach (var reporter in accounts)
{
reporter.ReportBalance();
}
}
このリストには、IBalanceReporterを実装してるやつなら何でも入れられる:SavingsAccount、CheckingAccount、テスト用のMockAccountForTestingとかも。マジックじゃなくて、ちゃんと動く。
図解:インターフェースコントラクトがもたらすもの
+-------------------+ 実装してる +-------------------+
| BankAccount | <---------------- | IBalanceReporter |
| (SavingsAccount) | |-------------------|
| (CheckingAccount)| | + ReportBalance() |
+-------------------+ +-------------------+
| ^
| |
+-----------+-----------------------+
|
コントラクトを実装してる他のどんなクラスでもOK
5. コントラクト、ビジネスロジック、アーキテクチャ設計
インターフェースのコントラクトは、「何をできるべきか」と「どうやって実装するか」をはっきり分けてくれる。だからソフトウェアアーキテクトはロジックをインターフェースで設計するのが好きなんだ。よく、まずインターフェース(コントラクト)を作って、実装は後から考えるって流れになる。
実例:決済システム
電子マネー、カード、PayPal、暗号通貨…それぞれ細かい違いはあるけど、抽象化してIPaymentProviderインターフェースを作れば:
public interface IPaymentProvider
{
void Pay(decimal amount);
bool Refund(decimal amount);
}
このインターフェースでやりとりするコードは、カードから払うのか口座から払うのか全然気にしなくていい。アーキテクチャ的にも、実生活的にも便利。新しい決済システムを追加しても、他のコードをいじらなくて済む。
6. ビジネスロジックをコントラクトに切り出す
コントラクトを使えば、メインのビジネスルールをインターフェースレベルに持ってきて、細かい部分(例えばリミットチェックやキャッシュバック付与とか)は各クラスに任せられる。
もう一つ例
public interface ILogger
{
void LogInfo(string message);
void LogError(string message);
}
public class ConsoleLogger : ILogger
{
public void LogInfo(string message) => Console.WriteLine($"INFO: {message}");
public void LogError(string message) => Console.WriteLine($"ERROR: {message}");
}
public class FileLogger : ILogger
{
public void LogInfo(string message) => /* ファイルに書き込み */;
public void LogError(string message) => /* ファイルに書き込み */;
}
クライアントコードはこんな感じ(どのロガーでも関係ない):
void DoWork(ILogger logger)
{
logger.LogInfo("作業を開始した。");
// ... なんか作業 ...
logger.LogError("作業がうまくいかなかった。");
}
これがまさにインターフェースレベルでプログラミングするってこと。クライアントコードはコントラクト(インターフェース)だけに依存してて、具体的な実装には依存しない。
7. ミスとよくある落とし穴
初心者がよくやるミス:コードが具体的なクラスにガチガチに依存してて、インターフェースを使ってない。これだと色々困ることになる:
- 何かを差し替えたりテストしたりするのが大変。大量のコードを書き直さなきゃいけない。
- 新しい型を追加するのに、あちこち修正が必要になる。
- モジュール同士がガッチリ「くっついて」しまう。
逆に、最初からインターフェースでシステムを組んで、パラメータもインターフェース経由で渡すようにすれば、疎結合で柔軟な設計ができるよ。
一番のアドバイス:モジュール同士のやりとりは、具体的な実装じゃなくてコントラクト同士のやりとりだって考えるようにしよう。
GO TO FULL VERSION