オブジェクト指向の重要な要素の1つがインターフェースです。
C#で大規模な開発や保守しやすい設計を行うには、インターフェースの理解が欠かせません。
本記事では、C#のインターフェース(interface)の基本から、設計の考え方、具象クラスへの実装方法までを、サンプルコードと図解を交えて丁寧に解説します。
【C#】インターフェースとは何か
インターフェースのイメージ

インターフェースは「クラスが必ず持つべきメンバー(メソッドやプロパティなど)だけを定義した契約書」のようなものです。
クラスがインターフェースをimplementsすることで、その契約に従い、決められたメソッドやプロパティを必ず実装しなければなりません。
インターフェースは以下のような特徴を持ちます。
- メソッドやプロパティのシグネチャだけを定め、通常は処理内容を持ちません
- 1つのクラスが複数のインターフェースを実装できます
- インターフェースを使うことで依存関係を抽象化し、疎結合な設計にできます
interface の基本的な書き方
インターフェースの宣言
C#ではinterfaceキーワードを使って宣言します。
// 「働く人」を表すインターフェースの例
public interface IWorker
{
// メソッドのシグネチャだけを定義
void Work();
// 戻り値付きのメソッド
string GetReport();
// 読み取り専用プロパティ
string Name { get; }
// 読み書き可能プロパティ
int WorkHours { get; set; }
}
インターフェースでは、かつては本体のないメンバーのみを定義するのが基本でした。
C# 8以降ではデフォルト実装なども可能ですが、入門段階では「処理は一切書かず、形だけ定義する」と理解しておくとわかりやすいです。
インターフェースを実装するクラス
クラス側では:の後ろにインターフェース名を書きます。
// IWorker を実装する「社員」クラス
public class Employee : IWorker
{
// インターフェースで定義した Name プロパティ
public string Name { get; private set; }
// 読み書き可能な WorkHours プロパティ
public int WorkHours { get; set; }
// コンストラクタで名前を受け取る
public Employee(string name)
{
Name = name;
}
// IWorker.Work の実装
public void Work()
{
Console.WriteLine($"{Name} は会社で働いています。");
}
// IWorker.GetReport の実装
public string GetReport()
{
return $"{Name} の作業報告です。勤務時間: {WorkHours} 時間";
}
}
インターフェースに定義されているメンバーは、すべて実装しないとコンパイルエラーになります。
これが「契約」の強制力です。
interface を使うメリット
実装と利用の分離

インターフェースを使うことで、「使う側」はインターフェースだけを知っていればよく、具体的なクラスの種類を意識せずに処理を呼び出せるようになります。
これにより、
- 新しいクラスを追加しても、利用側のコードを変更しなくてよい
- テスト時にモック(Moqなど)の作成が容易になる
- 依存関係をインターフェースに向けることで、設計が安定する
といった利点があります。
共通処理の統一
異なるクラスにまたがって共通の操作をさせたい場合、インターフェースを使うことで統一した書き方ができます。
public class PartTimer : IWorker
{
public string Name { get; private set; }
public int WorkHours { get; set; }
public PartTimer(string name)
{
Name = name;
}
public void Work()
{
Console.WriteLine($"{Name} はアルバイトとして働いています。");
}
public string GetReport()
{
return $"{Name} のアルバイト報告です。勤務時間: {WorkHours} 時間";
}
}
// IWorker を受け取って一括で処理するクラス
public class WorkerManager
{
// どの IWorker 実装でも同じように扱える
public void DoWork(IWorker worker)
{
worker.Work();
Console.WriteLine(worker.GetReport());
}
}
interface の基本的な使い方(サンプルコード)
ここまでの内容を、実際に動くコンソールアプリとしてまとめます。
using System;
// 働く人を表すインターフェース
public interface IWorker
{
// 作業を行うメソッド
void Work();
// 作業報告を取得するメソッド
string GetReport();
// 名前(読み取り専用)
string Name { get; }
// 勤務時間(読み書き可能)
int WorkHours { get; set; }
}
// 社員クラス
public class Employee : IWorker
{
public string Name { get; private set; }
public int WorkHours { get; set; }
public Employee(string name)
{
Name = name;
}
public void Work()
{
Console.WriteLine($"{Name} は会社で働いています。");
}
public string GetReport()
{
return $"{Name} の作業報告です。勤務時間: {WorkHours} 時間";
}
}
// アルバイトクラス
public class PartTimer : IWorker
{
public string Name { get; private set; }
public int WorkHours { get; set; }
public PartTimer(string name)
{
Name = name;
}
public void Work()
{
Console.WriteLine($"{Name} はアルバイトとして働いています。");
}
public string GetReport()
{
return $"{Name} のアルバイト報告です。勤務時間: {WorkHours} 時間";
}
}
// IWorker を使う側のクラス
public class WorkerManager
{
// IWorker を引数にとり、共通の処理を行う
public void Execute(IWorker worker)
{
// 作業を実行
worker.Work();
// 報告を取得して表示
string report = worker.GetReport();
Console.WriteLine(report);
}
}
public class Program
{
public static void Main()
{
// 社員とアルバイトを準備
IWorker employee = new Employee("山田");
employee.WorkHours = 8;
IWorker partTimer = new PartTimer("佐藤");
partTimer.WorkHours = 5;
// マネージャーを生成
var manager = new WorkerManager();
// 同じメソッドで異なる実装を扱える
manager.Execute(employee);
manager.Execute(partTimer);
}
}
山田 は会社で働いています。
山田 の作業報告です。勤務時間: 8 時間
佐藤 はアルバイトとして働いています。
佐藤 のアルバイト報告です。勤務時間: 5 時間
利用側(WorkerManager)は、Employee と PartTimer の違いを一切意識せず、IWorker として統一的に扱えている点が重要です。
interface と 抽象クラスの違い
役割の違いを整理
インターフェースとよく比較されるのがabstract class(抽象クラス)です。
ざっくりとした違いを表にまとめます。
| 項目 | interface | abstract class |
|---|---|---|
| 多重実装 | 複数実装できる | 継承は1つだけ |
| フィールド | 持てない | 持てる |
| コンストラクタ | 持てない | 持てる |
| 既定実装 | C# 8以降一部可能だが入門では無と考える | 具象メソッドで可能 |
| 意味合い | 能力・役割の契約 | 共通実装 + 抽象メンバー |
「共通の処理や状態(フィールド)もまとめて持たせたいなら抽象クラス」「異なる系統のクラスに共通の役割を付与したいならインターフェース」と使い分けるのが基本です。
使用例イメージ

このように、継承階層を表現するのが抽象クラス、能力や機能のラベル付けに近いのがインターフェースというイメージで捉えると理解しやすくなります。
interface 設計の考え方
小さく、役割ごとに分ける
インターフェース設計でよくある失敗が、1つのインターフェースに機能を詰め込みすぎることです。
例えばIMonsterに「移動する」「攻撃する」「会話する」「アイテムを売る」などを全部入れてしまうと、モンスターごとに不要なメソッドだらけになってしまいます。
インターフェースはできるだけ小さく、役割ごとに分割するのがよい設計です。
これはISP(Interface Segregation Principle)と呼ばれる設計原則にも対応しています。
// 移動できるもの
public interface IMovable
{
void Move(float x, float y);
}
// 攻撃できるもの
public interface IAttackable
{
void Attack();
}
// 会話できるもの
public interface ITalkable
{
void Talk(string message);
}
このように分割しておくと、必要なインターフェースだけを実装して組み合わせられるため、クラスの責務がはっきりします。
名前の付け方
C#の慣習として、インターフェース名は先頭にIを付けるのが一般的です。
- IWorker
- IRepository
- ILogger
- IComparable
また、役割や能力を示す動詞や名詞を使うと、利用する側が直感的に理解しやすくなります。
複数の interface を組み合わせる
複数実装の例
クラスは1つの抽象クラスしか継承できませんが、インターフェースは複数同時に実装可能です。
public interface IReadable
{
void Read();
}
public interface IWritable
{
void Write(string text);
}
// 両方のインターフェースを実装したクラス
public class TextFile : IReadable, IWritable
{
private string _content = "";
public void Read()
{
Console.WriteLine($"内容: {_content}");
}
public void Write(string text)
{
_content += text;
Console.WriteLine($"追記しました: {text}");
}
}
このように、必要な機能だけを組み合わせて付与できるのがインターフェースの強みです。
実践的な設計例: リポジトリパターン
データアクセスをインターフェースで抽象化する

データベースアクセスを行うクラスを、そのままアプリケーションのあちこちで使うと、DB 変更やテストが非常に困難になります。
そこでリポジトリパターンと呼ばれる設計では、データアクセスのインターフェースを定義し、それを通じてやり取りするようにします。
// ユーザーエンティティの例
public class User
{
public int Id { get; set; }
public string Name { get; set; } = "";
}
// ユーザーリポジトリのインターフェース
public interface IUserRepository
{
void Add(User user);
User? FindById(int id);
}
実装クラスと利用クラス
// メモリ上にユーザーを保持する実装(テスト用途など)
public class InMemoryUserRepository : IUserRepository
{
private readonly Dictionary<int, User> _users = new();
public void Add(User user)
{
_users[user.Id] = user;
}
public User? FindById(int id)
{
_users.TryGetValue(id, out var user);
return user;
}
}
// ユーザーに関するビジネスロジックを扱うサービス
public class UserService
{
private readonly IUserRepository _userRepository;
// コンストラクタインジェクションでインターフェースを受け取る
public UserService(IUserRepository userRepository)
{
_userRepository = userRepository;
}
public void RegisterUser(int id, string name)
{
var user = new User { Id = id, Name = name };
_userRepository.Add(user);
Console.WriteLine($"ユーザー {name} を登録しました。");
}
public void ShowUser(int id)
{
var user = _userRepository.FindById(id);
if (user == null)
{
Console.WriteLine("ユーザーが見つかりません。");
}
else
{
Console.WriteLine($"ユーザー: {user.Id}, {user.Name}");
}
}
}
public class Program
{
public static void Main()
{
// 実際には DI コンテナで解決されることが多い
IUserRepository repo = new InMemoryUserRepository();
var service = new UserService(repo);
service.RegisterUser(1, "田中");
service.ShowUser(1);
service.ShowUser(2);
}
}
ユーザー 田中 を登録しました。
ユーザー: 1, 田中
ユーザーが見つかりません。
UserService は IUserRepository にのみ依存しており、具体的な実装(SqlUserRepository など)を知る必要がありません。
このように、インターフェースを設計に取り入れることで、テストしやすく、変更に強いコードを実現できます。
まとめ
本記事では、C#のインターフェースについて基礎から設計・実装の流れまで解説しました。
インターフェースは「クラスが満たすべき契約」を表し、利用側はその契約だけを意識すればよいため、実装と利用をきれいに分離できます。
抽象クラスとの違いや、役割ごとに小さく分割する設計のコツ、複数インターフェースの組み合わせ、リポジトリパターンでの実践例などを押さえれば、C#でのオブジェクト指向設計がぐっとやりやすくなります。
まずは小さなインターフェースを自分のプロジェクトに1つ導入し、その効果を体感してみてください。
