C#のinterfaceとabstractを徹底比較|違い・使い分け・実装例をわかりやすく解説
はじめに
C#でオブジェクト指向プログラミングを学んでいると、必ず出てくるのがinterfaceとabstractです。どちらも「直接インスタンス化できない」「派生クラスや実装クラスに処理を任せる」といった共通点があるため、初心者にとっては違いがわかりにくいポイントです。
特に実務では、「これはinterfaceで定義すべきか」「abstractクラスに共通処理を書くべきか」「両方を組み合わせるべきか」という判断が頻繁に発生します。設計段階で使い分けを誤ると、拡張しづらいコードやテストしにくいコードになってしまうこともあります。
この記事では、C#のinterfaceとabstractの違い、使い分け、実装例、設計上の考え方までをわかりやすく解説します。
1. C#のinterfaceとabstractの違いを結論から比較
C#のinterfaceとabstractクラスの違いを一言で表すなら、interfaceは「できることを定義する契約」、abstractクラスは「共通処理を持つ未完成の基底クラス」です。
どちらもクラスに対して一定のルールを与える仕組みですが、目的が異なります。interfaceは「この機能を持っていること」を表し、abstractクラスは「同じ種類のものとして共通の土台を持つこと」を表します。
1-1. interfaceは「できること」を定義する契約
interfaceは、クラスが実装すべきメソッドやプロパティを定義するための仕組みです。
たとえば、IPrintableというinterfaceがある場合、それを実装するクラスは「印刷できる」という能力を持つことを意味します。
C#public interface IPrintable
{
void Print();
}
このinterfaceを実装するクラスは、必ずPrintメソッドを実装する必要があります。
C#public class Report : IPrintable
{
public void Print()
{
Console.WriteLine("レポートを印刷します。");
}
}
ここで重要なのは、IPrintableは「どのように印刷するか」ではなく、「印刷できること」を定義している点です。
つまりinterfaceは、実装内容よりも「外部から見た振る舞い」を重視します。
1-2. abstractクラスは「共通処理を持つ未完成の基底クラス」
abstractクラスは、継承されることを前提にした未完成のクラスです。
共通のフィールド、コンストラクタ、通常メソッドを持ちながら、一部の処理だけ派生クラスに実装を強制できます。
C#public abstract class Animal
{
public string Name { get; }
protected Animal(string name)
{
Name = name;
}
public void Sleep()
{
Console.WriteLine($"{Name}は眠ります。");
}
public abstract void Speak();
}
このAnimalクラスは、Sleepという共通処理を持っています。一方で、Speakメソッドは動物ごとに異なるため、派生クラスで実装します。
C#public class Dog : Animal
{
public Dog(string name) : base(name)
{
}
public override void Speak()
{
Console.WriteLine($"{Name}はワンと鳴きます。");
}
}
abstractクラスは、「同じ種類のものに共通する処理や状態をまとめたい」ときに使います。
1-3. interfaceとabstractクラスの比較表
| 比較項目 | interface | abstractクラス |
|---|---|---|
| 目的 | 機能や振る舞いの契約を定義する | 共通処理を持つ基底クラスを定義する |
| 継承・実装 | 複数のinterfaceを実装できる | クラスは1つのabstractクラスしか継承できない |
| 実装の保持 | メンバー定義が中心。デフォルト実装も可能 | 通常メソッドや共通処理を持てる |
| フィールド | インスタンスフィールドは持てない | フィールドを持てる |
| コンストラクタ | 持てない | 持てる |
| アクセス修飾子 | 基本的に公開契約として扱う | public、protected、privateなどを使える |
| インスタンス化 | できない | できない |
| 向いている用途 | 複数の型に共通する能力を表す | 同じ分類のクラスに共通処理を提供する |
| DI・テスト | 相性が良い | 共通処理の再利用に向いている |
1-4. 迷ったときの使い分け早見表
迷ったときは、次のように考えると判断しやすくなります。
| 判断ポイント | 選ぶべきもの |
|---|---|
| 「〜できる」という能力を表したい | interface |
| 複数のクラスに同じ機能を実装させたい | interface |
| DIやモックを使ってテストしやすくしたい | interface |
| 共通のフィールドや状態を持たせたい | abstractクラス |
| 共通処理を基底クラスにまとめたい | abstractクラス |
| 同じ分類のクラスをまとめたい | abstractクラス |
| 契約と共通処理の両方が必要 | interfaceとabstractクラスを併用 |
たとえば、「保存できる」「印刷できる」「通知できる」はinterface向きです。一方で、「動物」「帳票」「ユーザー基底クラス」のように共通の状態や処理を持つ分類はabstractクラス向きです。
2. C#のinterfaceとは何か
interfaceは、クラスや構造体が実装すべきメンバーを定義するための型です。
interface自体は基本的に処理の詳細を持たず、「このメソッドを持っていること」「このプロパティを持っていること」を約束します。
C#では、interface名の先頭にIを付ける命名規則が一般的です。たとえば、IRepository、ILogger、IDisposable、IEnumerableなどがあります。
2-1. interfaceの基本構文
interfaceはinterfaceキーワードを使って定義します。
C#public interface ILogger
{
void Log(string message);
}
このinterfaceをクラスで実装するには、クラス名の後ろに: インターフェース名を書きます。
C#public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
ConsoleLoggerはILoggerを実装しているため、ILogger型として扱えます。
C#ILogger logger = new ConsoleLogger();
logger.Log("ログを出力します。");
このように、interfaceを使うと具体的なクラスではなく、抽象的な契約に依存したコードを書けます。
2-2. interfaceで定義できるメンバー
C#のinterfaceでは、主に次のようなメンバーを定義できます。
C#public interface IUserService
{
string Name { get; }
void Register(string userName);
Task DeleteAsync(int userId);
}
代表的なメンバーは、メソッド、プロパティ、イベント、インデクサーです。
また、現代のC#ではデフォルトインターフェースメソッドやstatic abstract members in interfacesなども利用できます。ただし、基本的な設計では「interfaceは契約を定義するもの」と考えると理解しやすいです。
2-3. interfaceの実装例
たとえば、決済処理をinterfaceで表してみます。
C#public interface IPaymentService
{
void Pay(decimal amount);
}
クレジットカード決済のクラスを作ります。
C#public class CreditCardPaymentService : IPaymentService
{
public void Pay(decimal amount)
{
Console.WriteLine($"{amount}円をクレジットカードで支払いました。");
}
}
銀行振込のクラスも同じinterfaceを実装できます。
C#public class BankTransferPaymentService : IPaymentService
{
public void Pay(decimal amount)
{
Console.WriteLine($"{amount}円を銀行振込で支払いました。");
}
}
利用側は、具体的な決済方法を知らなくてもIPaymentServiceを通して処理できます。
C#public class OrderService
{
private readonly IPaymentService _paymentService;
public OrderService(IPaymentService paymentService)
{
_paymentService = paymentService;
}
public void Checkout(decimal amount)
{
_paymentService.Pay(amount);
}
}
この設計にすると、決済方法を変更してもOrderService側のコードを大きく変更せずに済みます。
2-4. interfaceを使うメリット
interfaceを使う大きなメリットは、具体的な実装に依存しないコードを書けることです。
たとえば、OrderServiceがCreditCardPaymentServiceに直接依存していると、銀行振込やQR決済に変更したいときに修正範囲が広がります。しかし、IPaymentServiceに依存していれば、実装クラスを差し替えるだけで対応できます。
また、interfaceはテストとの相性が非常に良いです。単体テストでは、本物のデータベースや外部APIを使わずに、モックやスタブを使って処理を確認できます。
C#public class FakePaymentService : IPaymentService
{
public void Pay(decimal amount)
{
// テスト用の処理
}
}
さらに、C#では複数のinterfaceを実装できます。
C#public class MultiFunctionPrinter : IPrintable, IScannable, IFaxable
{
public void Print()
{
Console.WriteLine("印刷します。");
}
public void Scan()
{
Console.WriteLine("スキャンします。");
}
public void Fax()
{
Console.WriteLine("FAXを送信します。");
}
}
このように、複数の能力を柔軟に組み合わせられる点もinterfaceの強みです。
2-5. interfaceを使う際の注意点
interfaceは便利ですが、何でもinterfaceにすればよいわけではありません。
実装クラスが1つしかなく、将来的にも差し替えの予定がない場合、interfaceを作る意味が薄いことがあります。過剰にinterfaceを増やすと、ファイル数や型の数が増え、かえってコードが読みづらくなる場合があります。
また、1つのinterfaceに多くのメソッドを詰め込みすぎるのも避けるべきです。
C#public interface IUserManager
{
void CreateUser();
void DeleteUser();
void SendEmail();
void ExportCsv();
void PrintReport();
}
このようなinterfaceは責務が広すぎます。ユーザー管理、メール送信、CSV出力、帳票印刷が混ざっているため、実装クラスに不要なメソッドを強制してしまう可能性があります。
interfaceは小さく分け、必要な機能だけを表すように設計することが重要です。
3. C#のabstractクラスとは何か
abstractクラスは、直接インスタンス化できないクラスです。継承されることを前提にしており、共通処理を持たせながら、一部の処理を派生クラスに実装させるために使います。
abstractクラスは、通常のクラスと同じようにフィールド、プロパティ、コンストラクタ、通常メソッドを持てます。そのうえで、abstractメソッドを定義することで、派生クラスに実装を強制できます。
3-1. abstractクラスの基本構文
abstractクラスはabstractキーワードを使って定義します。
C#public abstract class ReportBase
{
public string Title { get; }
protected ReportBase(string title)
{
Title = title;
}
public void PrintTitle()
{
Console.WriteLine($"タイトル: {Title}");
}
public abstract void PrintBody();
}
このクラスはnew ReportBase()のようにインスタンス化できません。
C#// エラー
// ReportBase report = new ReportBase("月次レポート");
派生クラスで継承して使います。
C#public class SalesReport : ReportBase
{
public SalesReport(string title) : base(title)
{
}
public override void PrintBody()
{
Console.WriteLine("売上情報を出力します。");
}
}
3-2. abstractメソッドと通常メソッドの違い
abstractクラス内には、通常メソッドとabstractメソッドを定義できます。
通常メソッドは、基底クラス側に実装を持つメソッドです。
C#public void PrintTitle()
{
Console.WriteLine($"タイトル: {Title}");
}
一方、abstractメソッドは実装を持たず、派生クラスで必ずoverrideする必要があります。
C#public abstract void PrintBody();
違いを整理すると、通常メソッドは「すべての派生クラスで共通して使う処理」、abstractメソッドは「派生クラスごとに必ず異なる処理」です。
つまりabstractクラスは、「共通部分は基底クラスに書き、個別部分は派生クラスに任せる」設計に向いています。
3-3. abstractプロパティの実装例
abstractにできるのはメソッドだけではありません。プロパティもabstractにできます。
C#public abstract class Employee
{
public abstract string Role { get; }
public void ShowRole()
{
Console.WriteLine($"役割: {Role}");
}
}
派生クラスでは、abstractプロパティを実装します。
C#public class Engineer : Employee
{
public override string Role => "エンジニア";
}
public class Designer : Employee
{
public override string Role => "デザイナー";
}
利用例は次の通りです。
C#Employee employee = new Engineer();
employee.ShowRole();
このように、派生クラスごとに異なる値を返したい場合にabstractプロパティが役立ちます。
3-4. abstractクラスを継承する実装例
もう少し実務に近い例として、通知処理をabstractクラスで表してみます。
C#public abstract class NotificationBase
{
public void Send(string message)
{
Validate(message);
SendCore(message);
WriteLog(message);
}
private void Validate(string message)
{
if (string.IsNullOrWhiteSpace(message))
{
throw new ArgumentException("メッセージが空です。");
}
}
protected abstract void SendCore(string message);
private void WriteLog(string message)
{
Console.WriteLine($"送信ログ: {message}");
}
}
この例では、バリデーションとログ出力は共通処理です。一方で、実際の送信方法はメール、SMS、チャットなどで異なるため、SendCoreをabstractメソッドにしています。
C#public class EmailNotification : NotificationBase
{
protected override void SendCore(string message)
{
Console.WriteLine($"メール送信: {message}");
}
}
public class SmsNotification : NotificationBase
{
protected override void SendCore(string message)
{
Console.WriteLine($"SMS送信: {message}");
}
}
このように、処理の流れは共通化しつつ、一部だけ派生クラスに任せる設計ができます。
3-5. abstractクラスを使うメリット
abstractクラスを使うメリットは、共通処理をまとめられることです。
複数のクラスに同じ処理を書くと、修正が必要になったときにすべてのクラスを変更しなければなりません。abstractクラスに共通処理をまとめておけば、基底クラスの修正だけで済む場合があります。
また、処理の流れを基底クラスで固定し、一部の処理だけ派生クラスに任せる設計もできます。これはテンプレートメソッドパターンと呼ばれる考え方です。
C#public abstract class ImporterBase
{
public void Import()
{
ReadFile();
Parse();
Save();
}
protected void ReadFile()
{
Console.WriteLine("ファイルを読み込みます。");
}
protected abstract void Parse();
protected void Save()
{
Console.WriteLine("データを保存します。");
}
}
このように、全体の手順を統一しながら、ファイル形式ごとの解析処理だけを派生クラスに任せられます。
4. interfaceとabstractクラスの主な違い
ここからは、C#のinterfaceとabstractクラスの違いをより具体的に見ていきます。
どちらも抽象化のための仕組みですが、継承、多重実装、フィールド、コンストラクタ、アクセス修飾子、テストしやすさなどに違いがあります。
4-1. 継承と多重実装の違い
C#では、クラスの多重継承はできません。つまり、1つのクラスが継承できる基底クラスは1つだけです。
C#public abstract class Animal
{
}
public abstract class Machine
{
}
// C#ではクラスの多重継承はできない
// public class RobotDog : Animal, Machine
// {
// }
一方で、interfaceは複数実装できます。
C#public interface IWalkable
{
void Walk();
}
public interface IRunnable
{
void Run();
}
public class Dog : IWalkable, IRunnable
{
public void Walk()
{
Console.WriteLine("歩きます。");
}
public void Run()
{
Console.WriteLine("走ります。");
}
}
そのため、「複数の能力を組み合わせたい」場合はinterfaceが向いています。
4-2. 実装を持てるかどうかの違い
abstractクラスは通常メソッドを持てます。
C#public abstract class BaseService
{
public void Log(string message)
{
Console.WriteLine($"ログ: {message}");
}
public abstract void Execute();
}
この場合、派生クラスはLogメソッドをそのまま利用できます。
C#public class UserService : BaseService
{
public override void Execute()
{
Log("ユーザー処理を実行します。");
}
}
一方、interfaceは本来「契約」を定義するものです。現代のC#ではデフォルト実装を書けますが、基本的には実装クラス側に処理を書く設計が一般的です。
C#public interface IService
{
void Execute();
}
設計上は、共通処理をしっかり持たせたい場合はabstractクラス、機能の契約を定義したい場合はinterfaceと考えるとよいでしょう。
4-3. フィールド・コンストラクタを持てるかの違い
abstractクラスはフィールドやコンストラクタを持てます。
C#public abstract class Entity
{
protected readonly DateTime CreatedAt;
protected Entity()
{
CreatedAt = DateTime.Now;
}
}
これは、派生クラスに共通の状態を持たせたい場合に便利です。
一方、interfaceはインスタンスフィールドやコンストラクタを持てません。
C#public interface IEntity
{
int Id { get; }
}
interfaceは状態そのものを持つというより、「その状態にアクセスできること」を定義します。
つまり、共通の状態や初期化処理が必要ならabstractクラス、状態へのアクセス契約だけでよいならinterfaceが向いています。
4-4. アクセス修飾子の扱いの違い
abstractクラスでは、通常のクラスと同じようにpublic、protected、privateなどのアクセス修飾子を使えます。
C#public abstract class BaseController
{
protected void SetViewData()
{
Console.WriteLine("ViewDataを設定します。");
}
private void InternalLog()
{
Console.WriteLine("内部ログを出力します。");
}
public abstract void Execute();
}
基底クラス内だけで使う処理はprivate、派生クラスから使わせたい処理はprotectedにできます。
interfaceは、外部に公開する契約として使われることが多いため、基本的には実装クラスが公開すべきメンバーを定義します。アクセス制御を細かく設計したい場合はabstractクラスのほうが向いています。
4-5. 拡張性・保守性の違い
interfaceは、利用側のコードを具体クラスから切り離せるため、拡張性が高くなります。
C#public class NotificationService
{
private readonly INotifier _notifier;
public NotificationService(INotifier notifier)
{
_notifier = notifier;
}
public void Notify(string message)
{
_notifier.Send(message);
}
}
この設計では、メール通知、SMS通知、チャット通知などを簡単に差し替えられます。
abstractクラスは、共通処理を一か所にまとめられるため保守性が高くなります。ただし、継承関係が強くなるため、基底クラスの変更が派生クラス全体に影響する点には注意が必要です。
拡張性を重視するならinterface、共通処理の再利用を重視するならabstractクラスが向いています。
4-6. テストしやすさ・DIとの相性の違い
interfaceはDIと非常に相性が良いです。
DIとは、クラスの内部で依存オブジェクトを直接生成するのではなく、外部から渡す設計のことです。
C#public class UserController
{
private readonly IUserRepository _userRepository;
public UserController(IUserRepository userRepository)
{
_userRepository = userRepository;
}
}
このようにinterfaceに依存していれば、テスト時に本物のリポジトリではなく、テスト用の実装を渡せます。
C#public class FakeUserRepository : IUserRepository
{
public User FindById(int id)
{
return new User { Id = id, Name = "テストユーザー" };
}
}
abstractクラスもテストで使えますが、基底クラスの共通処理や状態に依存しやすくなります。テストのしやすさを最優先するなら、interfaceを使って依存関係を切り離す設計が有効です。
5. interfaceとabstractクラスの使い分け
C#でinterfaceとabstractクラスを使い分けるときは、「契約を表したいのか」「共通処理を再利用したいのか」を基準にすると判断しやすくなります。
5-1. interfaceを使うべきケース
interfaceを使うべき代表的なケースは、複数のクラスに共通の能力を持たせたい場合です。
たとえば、次のようなものはinterface向きです。
C#public interface ISavable
{
void Save();
}
public interface IExportable
{
void Export();
}
public interface ISendable
{
void Send();
}
これらは「保存できる」「出力できる」「送信できる」という能力を表しています。
また、DIを使って依存関係を切り替えたい場合もinterfaceが向いています。
C#public interface IUserRepository
{
User FindById(int id);
}
実装クラスは、データベース用、テスト用、外部API用などに分けられます。
C#public class SqlUserRepository : IUserRepository
{
public User FindById(int id)
{
Console.WriteLine("SQL Serverからユーザーを取得します。");
return new User();
}
}
public class ApiUserRepository : IUserRepository
{
public User FindById(int id)
{
Console.WriteLine("APIからユーザーを取得します。");
return new User();
}
}
このように、実装を差し替える可能性がある場合はinterfaceが適しています。
5-2. abstractクラスを使うべきケース
abstractクラスを使うべきケースは、複数の派生クラスに共通する処理や状態がある場合です。
C#public abstract class FileImporterBase
{
public void Import(string path)
{
ValidatePath(path);
Read(path);
Parse();
}
private void ValidatePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentException("パスが空です。");
}
}
protected abstract void Read(string path);
protected abstract void Parse();
}
このように、処理の流れや共通のバリデーションを基底クラスにまとめたい場合はabstractクラスが向いています。
また、共通のフィールドやコンストラクタを持たせたい場合もabstractクラスを選びます。
C#public abstract class EntityBase
{
public int Id { get; set; }
public DateTime CreatedAt { get; }
protected EntityBase()
{
CreatedAt = DateTime.Now;
}
}
interfaceではコンストラクタやインスタンスフィールドを持てないため、このような設計にはabstractクラスが適しています。
5-3. interfaceとabstractクラスを併用すべきケース
実務では、interfaceとabstractクラスを併用することもよくあります。
interfaceで外部に見せる契約を定義し、abstractクラスで共通処理を提供する設計です。
C#public interface INotification
{
void Send(string message);
}
C#public abstract class NotificationBase : INotification
{
public void Send(string message)
{
Validate(message);
SendCore(message);
}
private void Validate(string message)
{
if (string.IsNullOrWhiteSpace(message))
{
throw new ArgumentException("メッセージが空です。");
}
}
protected abstract void SendCore(string message);
}
C#public class EmailNotification : NotificationBase
{
protected override void SendCore(string message)
{
Console.WriteLine($"メール送信: {message}");
}
}
利用側はinterfaceに依存します。
C#public class NotificationClient
{
private readonly INotification _notification;
public NotificationClient(INotification notification)
{
_notification = notification;
}
public void Execute()
{
_notification.Send("こんにちは");
}
}
この設計にすると、外部からはINotificationという契約だけを見せ、内部ではNotificationBaseによって共通処理を再利用できます。
5-4. 実務でよくある判断パターン
実務でよくある判断パターンとして、まず利用側が何に依存すべきかを考えます。
アプリケーション層やコントローラーが使う対象であれば、interfaceを用意することが多いです。
C#public interface IOrderService
{
void PlaceOrder(int productId);
}
一方、複数のサービスクラスに共通するログ出力、バリデーション、例外処理などをまとめたい場合はabstractクラスを検討します。
C#public abstract class ServiceBase
{
protected void Log(string message)
{
Console.WriteLine($"ログ: {message}");
}
}
ただし、すべてのサービスを無理にServiceBaseから継承させると、不要な依存が増える可能性があります。共通化したい処理が本当に同じ責務なのかを確認することが重要です。
5-5. 使い分けで失敗しやすい例
失敗しやすい例の1つは、abstractクラスに大量の処理を詰め込むことです。
C#public abstract class ApplicationBase
{
protected void Log() { }
protected void SendMail() { }
protected void ExportCsv() { }
protected void ValidateUser() { }
protected void ConnectDatabase() { }
}
このような基底クラスは便利に見えますが、責務が広すぎます。派生クラスが不要な機能まで抱えることになり、変更の影響範囲も広がります。
もう1つの失敗例は、interfaceを細かく作りすぎることです。
C#public interface IUserNameGetter
{
string GetUserName();
}
このようなinterfaceが大量にあると、設計が複雑になります。interfaceは便利ですが、「本当に差し替える必要があるか」「複数の実装が想定されるか」を考えて使うべきです。
6. 実装例で理解するinterfaceとabstractの違い
ここでは、同じ題材を使ってinterfaceとabstractクラスの違いを実装例で確認します。
題材は「ファイル出力」です。
6-1. interfaceだけで実装する例
まずはinterfaceだけで実装してみます。
C#public interface IFileExporter
{
void Export(string content);
}
CSV出力クラスを作ります。
C#public class CsvExporter : IFileExporter
{
public void Export(string content)
{
Console.WriteLine($"CSV形式で出力: {content}");
}
}
JSON出力クラスも同じinterfaceを実装します。
C#public class JsonExporter : IFileExporter
{
public void Export(string content)
{
Console.WriteLine($"JSON形式で出力: {content}");
}
}
利用側は、具体的な出力形式を知る必要がありません。
C#public class ExportService
{
private readonly IFileExporter _exporter;
public ExportService(IFileExporter exporter)
{
_exporter = exporter;
}
public void ExportData()
{
_exporter.Export("商品データ");
}
}
この設計は、出力形式を差し替えやすいのがメリットです。ただし、共通の前処理や後処理が増えると、各実装クラスに同じコードが重複する可能性があります。
6-2. abstractクラスだけで実装する例
次に、abstractクラスだけで実装します。
C#public abstract class FileExporterBase
{
public void Export(string content)
{
Validate(content);
ExportCore(content);
WriteLog(content);
}
private void Validate(string content)
{
if (string.IsNullOrWhiteSpace(content))
{
throw new ArgumentException("出力内容が空です。");
}
}
protected abstract void ExportCore(string content);
private void WriteLog(string content)
{
Console.WriteLine($"出力完了: {content}");
}
}
CSV出力クラスを作ります。
C#public class CsvExporter : FileExporterBase
{
protected override void ExportCore(string content)
{
Console.WriteLine($"CSV形式で出力: {content}");
}
}
JSON出力クラスも作ります。
C#public class JsonExporter : FileExporterBase
{
protected override void ExportCore(string content)
{
Console.WriteLine($"JSON形式で出力: {content}");
}
}
abstractクラスを使うと、バリデーションやログ出力などの共通処理を一か所にまとめられます。ただし、利用側がFileExporterBaseに依存すると、継承構造に強く結びつきます。
6-3. interfaceとabstractクラスを組み合わせる例
実務では、interfaceとabstractクラスを組み合わせる設計がよく使われます。
C#public interface IFileExporter
{
void Export(string content);
}
C#public abstract class FileExporterBase : IFileExporter
{
public void Export(string content)
{
Validate(content);
ExportCore(content);
WriteLog(content);
}
private void Validate(string content)
{
if (string.IsNullOrWhiteSpace(content))
{
throw new ArgumentException("出力内容が空です。");
}
}
protected abstract void ExportCore(string content);
private void WriteLog(string content)
{
Console.WriteLine($"出力完了: {content}");
}
}
C#public class CsvExporter : FileExporterBase
{
protected override void ExportCore(string content)
{
Console.WriteLine($"CSV形式で出力: {content}");
}
}
利用側はinterfaceに依存します。
C#public class ExportService
{
private readonly IFileExporter _exporter;
public ExportService(IFileExporter exporter)
{
_exporter = exporter;
}
public void Run()
{
_exporter.Export("売上データ");
}
}
この構成では、外部にはIFileExporterという契約を見せ、内部ではFileExporterBaseによって共通処理を再利用できます。
6-4. overrideとimplementsの違い
C#では、abstractメソッドを実装するときはoverrideを使います。
C#public abstract class Animal
{
public abstract void Speak();
}
public class Cat : Animal
{
public override void Speak()
{
Console.WriteLine("ニャー");
}
}
一方、interfaceのメンバーを実装するときは、通常overrideは使いません。
C#public interface ISpeaker
{
void Speak();
}
public class Cat : ISpeaker
{
public void Speak()
{
Console.WriteLine("ニャー");
}
}
つまり、overrideは基底クラスのメンバーを上書きするためのキーワードです。interfaceの実装は「契約を満たす」ものであり、継承メソッドの上書きとは意味が異なります。
6-5. 実装コードから見る設計上の違い
interfaceは、利用側から見ると「何ができるか」を表します。
C#IFileExporter exporter = new CsvExporter();
exporter.Export("データ");
この場合、利用側はCsvExporterの内部構造を知る必要がありません。Exportできることだけが重要です。
abstractクラスは、実装側から見ると「共通処理をどう再利用するか」を表します。
C#public class CsvExporter : FileExporterBase
{
protected override void ExportCore(string content)
{
Console.WriteLine($"CSV形式で出力: {content}");
}
}
つまり、interfaceは利用側のための抽象化、abstractクラスは実装側の共通化と考えると理解しやすいです。
7. C#の新しいinterface機能とabstractとの関係
C#はバージョンアップによってinterfaceの機能が強化されてきました。特に、デフォルトインターフェースメソッドやstatic abstract members in interfacesは、従来のinterfaceの考え方を広げる機能です。
ただし、これらの機能があるからといってabstractクラスが不要になるわけではありません。
7-1. デフォルトインターフェースメソッドとは
デフォルトインターフェースメソッドとは、interface内にメソッドの実装を書ける機能です。
C#public interface ILogger
{
void Log(string message);
void LogError(string message)
{
Log($"ERROR: {message}");
}
}
この例では、LogErrorにデフォルト実装があります。実装クラスはLogだけを実装すれば、LogErrorの処理を利用できます。
C#public class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
デフォルトインターフェースメソッドは、既存のinterfaceに新しいメソッドを追加したいときに役立ちます。すべての実装クラスを一度に修正しなくても、互換性を保ちやすくなるためです。
ただし、共通処理を大量にinterfaceへ書くと、interfaceの責務が曖昧になります。通常は、契約を中心に定義し、共通処理が多い場合はabstractクラスを検討するほうが自然です。
7-2. static abstract members in interfacesとは
static abstract members in interfacesは、interfaceに静的な抽象メンバーを定義できる機能です。
代表的な用途は、数値型のように「型自身が持つ演算や生成処理」を抽象化するケースです。
簡単な例を見てみます。
C#public interface ICreatable<TSelf>
where TSelf : ICreatable<TSelf>
{
static abstract TSelf Create();
}
このinterfaceを実装する型は、Createという静的メソッドを実装する必要があります。
C#public class User : ICreatable<User>
{
public string Name { get; set; } = "";
public static User Create()
{
return new User { Name = "新規ユーザー" };
}
}
この機能により、ジェネリックなコードで静的メンバーを扱いやすくなります。
ただし、初心者が通常の業務アプリケーションを作る段階では、まず通常のinterfaceとabstractクラスの使い分けを理解するほうが重要です。
7-3. 新機能によってabstractクラスは不要になるのか
デフォルトインターフェースメソッドによって、interfaceにも実装を書けるようになりました。しかし、それによってabstractクラスが不要になるわけではありません。
abstractクラスには、interfaceにはない役割があります。
たとえば、インスタンスフィールドを持つ、コンストラクタで初期化する、protectedメソッドで派生クラス向けの共通処理を提供する、といった設計はabstractクラスが得意です。
C#public abstract class RepositoryBase
{
protected readonly string ConnectionString;
protected RepositoryBase(string connectionString)
{
ConnectionString = connectionString;
}
protected void OpenConnection()
{
Console.WriteLine($"接続します: {ConnectionString}");
}
}
このような状態を持つ共通処理は、interfaceよりabstractクラスのほうが適しています。
7-4. 現代のC#でのinterfaceとabstractの考え方
現代のC#では、interfaceの機能が強化されています。しかし、基本的な考え方は大きく変わりません。
interfaceは、外部に見せる契約や能力を表すものです。abstractクラスは、共通処理や共通状態を持つ基底クラスです。
新機能は便利ですが、使いすぎると設計が複雑になります。特にデフォルトインターフェースメソッドは、ライブラリの後方互換性を保つ目的では有効ですが、通常のアプリケーション設計で多用する必要はありません。
まずはシンプルに、契約はinterface、共通処理はabstractクラスと考えるのがおすすめです。
8. interfaceとabstractクラスの設計ベストプラクティス
C#でinterfaceとabstractクラスをうまく使うには、単に文法を覚えるだけでなく、設計上の考え方を理解することが大切です。
ここでは、実務で意識したいベストプラクティスを紹介します。
8-1. interfaceは小さく分ける
interfaceは、小さく分けることが重要です。
悪い例を見てみます。
C#public interface IWorker
{
void Work();
void Eat();
void Sleep();
}
このinterfaceをロボットに実装させると、EatやSleepが不要になるかもしれません。
C#public class Robot : IWorker
{
public void Work()
{
Console.WriteLine("作業します。");
}
public void Eat()
{
throw new NotSupportedException();
}
public void Sleep()
{
throw new NotSupportedException();
}
}
このような設計はよくありません。不要なメソッドの実装を強制しているからです。
改善するなら、interfaceを分割します。
C#public interface IWorkable
{
void Work();
}
public interface IEatable
{
void Eat();
}
public interface ISleepable
{
void Sleep();
}
こうすれば、必要な能力だけを実装できます。
C#public class Robot : IWorkable
{
public void Work()
{
Console.WriteLine("作業します。");
}
}
interfaceは「小さく、明確な責務」を持たせるのが基本です。
8-2. abstractクラスに共通処理を詰め込みすぎない
abstractクラスは共通処理をまとめられるため便利ですが、詰め込みすぎると問題になります。
基底クラスが肥大化すると、派生クラスが不要な機能まで引き継ぐことになります。また、基底クラスの変更が多くの派生クラスに影響するため、保守が難しくなります。
C#public abstract class BaseManager
{
protected void Log() { }
protected void Validate() { }
protected void SendMail() { }
protected void ExportCsv() { }
protected void ConnectDatabase() { }
}
このような何でも入りの基底クラスは避けるべきです。
共通処理が本当に同じ責務なのかを確認し、必要であれば別クラスに分離して合成することを検討しましょう。
8-3. 継承より合成を優先する
オブジェクト指向設計では、「継承より合成を優先する」という考え方があります。
継承は強力ですが、基底クラスと派生クラスの結びつきが強くなります。一方、合成は必要な機能を別クラスとして持たせる設計です。
たとえば、ログ出力をabstractクラスで提供するのではなく、ILoggerを使って外部から渡す設計にできます。
C#public interface ILogger
{
void Log(string message);
}
C#public class UserService
{
private readonly ILogger _logger;
public UserService(ILogger logger)
{
_logger = logger;
}
public void CreateUser()
{
_logger.Log("ユーザーを作成しました。");
}
}
この設計では、UserServiceは特定の基底クラスに縛られません。必要な機能を部品として組み合わせられるため、拡張性が高くなります。
abstractクラスを使う前に、「これは継承で表すべきか、それとも別クラスを持たせるべきか」を考えることが大切です。
8-4. 命名規則と可読性を意識する
C#では、interface名の先頭にIを付けるのが一般的です。
C#public interface IUserRepository
{
}
abstractクラスには、Baseを付けることがあります。
C#public abstract class UserRepositoryBase
{
}
ただし、名前は機械的に付ければよいわけではありません。役割が伝わる名前にすることが重要です。
悪い例です。
C#public interface IData
{
}
public abstract class BaseClass
{
}
これでは何を表しているのかわかりません。
良い例です。
C#public interface IOrderRepository
{
}
public abstract class FileImporterBase
{
}
名前を見ただけで、何の契約なのか、何の共通基底クラスなのかがわかるようにしましょう。
8-5. 将来の変更に強い設計にする
interfaceとabstractクラスを使う目的は、単に文法を使うことではありません。変更に強いコードを書くことです。
将来、実装が増える可能性があるならinterfaceを使うと差し替えやすくなります。
C#public interface IMessageSender
{
void Send(string message);
}
メール、SMS、チャットなどの実装を追加できます。
C#public class EmailSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine($"メール: {message}");
}
}
public class ChatSender : IMessageSender
{
public void Send(string message)
{
Console.WriteLine($"チャット: {message}");
}
}
一方、共通処理が増えることが明確ならabstractクラスを使うと重複を減らせます。
C#public abstract class MessageSenderBase : IMessageSender
{
public void Send(string message)
{
Validate(message);
SendCore(message);
}
private void Validate(string message)
{
if (string.IsNullOrWhiteSpace(message))
{
throw new ArgumentException("メッセージが空です。");
}
}
protected abstract void SendCore(string message);
}
大切なのは、「今便利だから」ではなく、「将来の変更に耐えられるか」という視点で選ぶことです。
9. よくある質問
ここでは、C#のinterfaceとabstractクラスについてよくある質問に答えます。
9-1. interfaceとabstractクラスはどちらを先に覚えるべき?
初心者は、まずinterfaceから覚えるのがおすすめです。
理由は、interfaceがC#の実務開発で非常によく使われるからです。特にDI、単体テスト、ASP.NET Core、リポジトリパターンなどではinterfaceが頻繁に登場します。
ただし、オブジェクト指向の継承を理解するうえではabstractクラスも重要です。最初は「interfaceは契約」「abstractクラスは共通処理を持つ基底クラス」と覚えるとよいでしょう。
9-2. interfaceに実装を書けるならabstractクラスは不要?
不要ではありません。
デフォルトインターフェースメソッドにより、interfaceにも実装を書けるようになりました。しかし、abstractクラスにはフィールド、コンストラクタ、protectedメンバー、共通状態の管理といった役割があります。
interfaceはあくまで契約を表すのが基本です。共通処理や状態をしっかり持たせたい場合は、abstractクラスのほうが自然です。
9-3. abstractクラスは複数継承できる?
C#では、abstractクラスを含めてクラスの複数継承はできません。
C#public abstract class A
{
}
public abstract class B
{
}
// エラー
// public class C : A, B
// {
// }
1つのクラスが継承できる基底クラスは1つだけです。
一方、interfaceは複数実装できます。
C#public class C : IA, IB
{
}
複数の能力を組み合わせたい場合はinterfaceを使いましょう。
9-4. interfaceとabstractクラスはインスタンス化できる?
interfaceもabstractクラスも、直接インスタンス化はできません。
C#// エラー
// ILogger logger = new ILogger();
// エラー
// Animal animal = new Animal();
ただし、実装クラスや派生クラスのインスタンスを、interface型やabstractクラス型の変数に代入することはできます。
C#ILogger logger = new ConsoleLogger();
C#Animal animal = new Dog("ポチ");
これはポリモーフィズムと呼ばれる考え方で、C#のオブジェクト指向設計で非常に重要です。
9-5. 初心者はどのように使い分ければよい?
初心者は、まず次の基準で使い分けるとよいでしょう。
「できること」を表したいならinterfaceです。
C#public interface IPrintable
{
void Print();
}
「共通処理を持つ親クラス」を作りたいならabstractクラスです。
C#public abstract class ReportBase
{
public void PrintHeader()
{
Console.WriteLine("ヘッダーを印刷します。");
}
public abstract void PrintBody();
}
また、実務では利用側をinterfaceに依存させ、共通処理が必要な場合にabstractクラスを内部実装として使う設計がよくあります。
最初から完璧に使い分ける必要はありません。コードを書きながら、「これは契約なのか」「共通処理なのか」を意識することが大切です。
まとめ
C#のinterfaceとabstractクラスは、どちらも抽象化を実現するための重要な仕組みです。ただし、役割は異なります。
interfaceは「できること」を定義する契約です。複数のクラスに共通の能力を持たせたい場合、DIやテストで実装を差し替えたい場合、利用側を具体クラスから切り離したい場合に向いています。
一方、abstractクラスは「共通処理を持つ未完成の基底クラス」です。複数の派生クラスで共通する処理や状態をまとめたい場合、一部の処理だけ派生クラスに実装させたい場合に向いています。
使い分けの基本は次の通りです。
| 判断基準 | 選択肢 |
|---|---|
| 能力や契約を表したい | interface |
| 複数の能力を組み合わせたい | interface |
| DIやテストをしやすくしたい | interface |
| 共通処理をまとめたい | abstractクラス |
| 共通の状態やコンストラクタを持ちたい | abstractクラス |
| 契約と共通処理を両立したい | interfaceとabstractクラスを併用 |
C#で保守性の高いコードを書くには、interfaceとabstractを正しく理解し、目的に応じて使い分けることが重要です。
最初は「interfaceは契約」「abstractクラスは共通処理」と覚えておけば問題ありません。そのうえで、実務では小さなinterface、責務を絞ったabstractクラス、継承より合成を意識すると、変更に強く読みやすい設計に近づけます。

