1. なんで複数インターフェースを実装するの?
リアルなシステムを設計するとき、オブジェクトはよく1つの「役割」だけじゃなくて、いくつも持つことが多いんだ。例えば、電子書籍があって、それは読むだけじゃなくて、編集もできるし、クラウドに保存もできる。オブジェクト指向プログラミング(OOP)的に言うと、そのオブジェクトは複数のインターフェースを実装しなきゃいけないってことだね:
- IReadable — コンテンツの読み取り。
- IWritable — コンテンツの編集。
- ISyncable — リモートストレージとの同期。
ここで複数インターフェース実装の出番!これがC#(てかOOP全体)のスーパーパワーで、クラスにはできないことなんだよ。クラスは2つ以上継承できないけど、インターフェースはいくらでも実装できる。
リアルライフの例え
会社の社員を想像してみて。バシャは同時に:
- プログラマー(コード書く)
- テスター(たまに他人のコードチェック)
- マネージャー(スプリントやコーヒーのノルマ計画)
この「役割」には全然違う責任がある。でもバシャは全部の「契約」をちゃんとこなしてる!プログラミングでも同じで、クラスがインターフェースを実装して、その「責任」を引き受けるってわけ。
2. 複数インターフェース実装のシンタックス
めっちゃ簡単:クラス宣言のときにカンマで区切って並べるだけ:
public interface IReadable
{
void Read();
}
public interface IWritable
{
void Write(string text);
}
public class Note : IReadable, IWritable
{
private string content = "";
public void Read()
{
Console.WriteLine("メモ: " + content);
}
public void Write(string text)
{
content = text;
Console.WriteLine("メモが更新された!");
}
}
この例だとNoteクラスは両方のインターフェースを実装してる。だから、ReadもWriteも両方のメソッドを実装しなきゃダメってこと。
3. 「うちら」のアプリの例で見ると
前の例では、口座操作用のシンプルなバンキングアプリを作ってたよね。今度は、印刷できる(IPrintable)、ファイルに保存できる(ISavable)、メール送信もできる(IEmailable)ドキュメント用の万能クラスが必要になったとしよう。
インターフェースを定義しよう:
public interface IPrintable
{
void Print();
}
public interface ISavable
{
void Save(string filePath);
}
public interface IEmailable
{
void Email(string toAddress);
}
全部入りクラス:
public class Statement : IPrintable, ISavable, IEmailable
{
public string Content { get; set; }
public void Print()
{
Console.WriteLine("明細を印刷中…");
Console.WriteLine(Content);
}
public void Save(string filePath)
{
// 標準ライブラリのFile.WriteAllTextを使う。
File.WriteAllText(filePath, Content);
Console.WriteLine($"明細がファイルに保存された: {filePath}");
}
public void Email(string toAddress)
{
Console.WriteLine($"明細がメールで送信された: {toAddress}(シミュレーション)");
}
}
このクラスは好きなように使えるよ:
var stat = new Statement { Content = "今月の取引: +1000 ユニット, -500 クレジット." };
stat.Print();
stat.Save("statement.txt");
stat.Email("boss@bank.corp");
ちなみに、こういうオブジェクトを、必要なインターフェースだけ要求するメソッドに渡すのが超便利。他の機能は気にしなくてOK。例えば、印刷用メソッドはIPrintableだけ受け取ればよくて、中身が「保存屋」か「メール屋」か知らなくてもいい。
4. インターフェース参照の使い方:オブジェクトの「できること」はどこで見える?
ここからがマジック。複数インターフェースを実装したら、オブジェクトをその契約の1つとしてだけ扱える。例えば:
IPrintable printable = new Statement { Content = "抽象化のレクチャー" };
printable.Print(); // 印刷だけできる
// printable.Save("file.txt"); // エラー: IPrintableインターフェースはSaveを知らない。
でも、別のインターフェースに切り替えれば:
ISavable savable = printable as ISavable;
if (savable != null)
{
savable.Save("file.txt");
}
これは、特定のインターフェース参照を受け取るメソッドにオブジェクトを渡すときに便利。こうすると依存が減って、コードがめっちゃ柔軟になる。新しい実装を追加しても、古いコードを変える必要なし!
5. 複数実装と、違うインターフェースで同じ名前のメソッド
ここが面白いところ!もし2つのインターフェースが同じ名前だけど意味が違うメソッドを要求してたら?例えば、コーヒーマシン用のインターフェースを考えてみよう:
public interface IStartable
{
void Start();
}
public interface IRunnable
{
void Start();
}
コーヒーマシンは「スタートできる」(IStartable — ドリンク作り開始)し、「ランナブル」(IRunnable — 全体的に動き出す)でもある。
デフォルト実装(普通のやつ):
public class CoffeeMachine : IStartable, IRunnable
{
public void Start()
{
Console.WriteLine("コーヒーマシンが両方の役割でスタート!");
}
}
この場合、1つのStart()実装が両方のインターフェースを「カバー」してる。
意味を分けたい場合は?
明示的インターフェース実装を使えばOK:
public class CoffeeMachine : IStartable, IRunnable
{
void IStartable.Start()
{
Console.WriteLine("スタート: ドリンク作り開始!");
}
void IRunnable.Start()
{
Console.WriteLine("スタート: マシンが動作モードに入った。");
}
}
この場合、特定の実装はインターフェース変数経由でしか呼べない:
CoffeeMachine cm = new CoffeeMachine();
IStartable startable = cm;
startable.Start(); // スタート: ドリンク作り開始!
IRunnable runnable = cm;
runnable.Start(); // スタート: マシンが動作モードに入った。
// cm.Start(); // コンパイルエラー!Startはクラスのメソッドとしては使えない。
ちなみに、このテクニックは.NETの標準ライブラリでもよく使われてる。例えば、クラスが複数の似たインターフェース(別フレームワーク由来)を実装してて、それぞれ違う動作が必要なときとかね。
6. 複数インターフェース実装と継承の違い
| できること | クラス(継承) | インターフェース |
|---|---|---|
| 基底型の数 | 1つだけ | いくらでも |
| コードの継承 | あり(基本実装できる) | なし、シグネチャだけ(デフォルトメソッド除く) |
| 状態の保持 | あり | なし |
| 新しい役割の追加 | 不可(または複雑、コンポジション経由) | 簡単、インターフェース実装するだけ |
| 向いてる用途… | 「物理的」な階層 | 「役割」や論理的な機能 |
7. 実践例:「ハイブリッド」オブジェクト
複数インターフェース実装のおかげで、不要な継承を気にせず、独自の「役割」セットを持つクラスを作れるよ。
例えば、うちらのバンキングアプリなら、自分の情報を保存できて、自分でバリデーションもできて、印刷もできるクラスを作れる。全部別々のインターフェースで:
public interface IValidatable
{
bool Validate();
}
public class Check : IPrintable, ISavable, IValidatable
{
public string Data { get; set; }
public void Print()
{
Console.WriteLine("チェック印刷: " + Data);
}
public void Save(string filePath)
{
File.WriteAllText(filePath, Data);
Console.WriteLine("チェックが保存された: " + filePath);
}
public bool Validate()
{
return !string.IsNullOrEmpty(Data);
}
}
こういうやり方だと、必要に応じて役割を組み合わせて、拡張しやすいアーキテクチャが作れる。新しい「責任」が必要になったら、新しいインターフェースを実装するだけでOK。
8. よくあるミスと落とし穴
初心者がよくやるミス:インターフェースの全メンバーを実装し忘れる。コンパイラは容赦なくエラーを出してくるし、「クラスはインターフェースメンバーを実装しなきゃダメ」って教えてくれる。
もう1つよくある問題は、メソッドのアクセス範囲の混乱。明示的に実装したメソッドは、クラス変数からはアクセスできない。インターフェース経由だけ。
さらにありがちなのがリファクタ時の問題:インターフェースに変更(例えばメソッド追加)を加えたのに、全部の実装を更新し忘れる。これもコンパイルエラーになる。だから、設計時には、たくさんのクラスが使い始めた後はインターフェースを変えないようにしよう。
GO TO FULL VERSION