C#におけるオブジェクト指向プログラミングを理解する上で、抽象クラスとインターフェースの違いを把握することは極めて重要です。どちらもクラスに特定の構造や振る舞いを強制する「契約」として機能しますが、その目的と特性には明確な違いがあります。本稿では、両者の概念を具体例と共に解説し、実際の開発でどのように使い分けるべきかを考察します。
抽象クラスの概要
抽象クラスは、`abstract`キーワードを用いて定義される特別なクラスです。その主な目的は、共通の基底となる機能や性質をサブクラスに提供することにあります。抽象クラス自体はインスタンス化することができず、必ず他のクラスによって継承されることを前提として設計されます。
抽象クラスの役割は、関連するクラス群(例:「自動車」「バイク」など「乗り物」カテゴリに属するもの)に共通する性質(例:最高速度、ナンバープレート)や振る舞い(例:移動する)を定義し、サブクラスがそれらを必ず実装するよう強制することです。これにより、同じ種類のオブジェクト群に一貫性を持たせ、コードの階層構造を明確にします。
抽象クラスには、実装済みのメソッドやプロパティ(通常のメンバー)と、実装を持たない抽象メンバー(抽象メソッドや抽象プロパティ)の両方を含めることができます。サブクラスは、抽象クラスから継承した際に、すべての抽象メンバーをオーバーライドして具体的な実装を提供する必要があります。
// 乗り物を表す抽象基底クラス
public abstract class VehicleBase
{
// 抽象プロパティ:サブクラスでの実装が必須
public abstract string IdentificationNumber { get; }
public abstract int MaxSpeed { get; }
// 抽象メソッド:サブクラスでの実装が必須
public abstract void Move();
// 通常の仮想メソッド:サブクラスでオーバーライド可能
public virtual void ShowInfo()
{
Console.WriteLine($"ID: {IdentificationNumber}, Max Speed: {MaxSpeed} km/h");
}
}
// 自動車クラス
public class Automobile : VehicleBase
{
public override string IdentificationNumber { get; } = "CAR-12345";
public override int MaxSpeed { get; } = 200;
public override void Move()
{
Console.WriteLine("四輪で走行します。");
}
}
// 自転車クラス
public class Bicycle : VehicleBase
{
public override string IdentificationNumber { get; } = "BIKE-67890";
public override int MaxSpeed { get; } = 50;
public override void Move()
{
Console.WriteLine("ペダルをこいで進みます。");
}
}
上記の例では、`VehicleBase`が「乗り物」の共通概念を定義しています。`Automobile`や`Bicycle`はこのクラスを継承し、`IdentificationNumber`や`Move()`といった抽象メンバーを具体的に実装しています。
抽象クラスの主な特性
- 定義には`abstract`キーワードが必須です。
- インスタンス化できず、継承されることを前提とします。
- 実装済みのメンバーと、実装を持たない抽象メンバーを混在できます。
- サブクラスは、継承時にすべての抽象メンバーを実装する義務を負います。
- フィールド(変数)を保持することができます。
インターフェースの概要
インターフェースは、`interface`キーワードを使って定義される「契約」の另一种形式です。インターフェースは、クラスが「何を実行できるか(何ができるか)」という能力や機能を定義します。抽象クラスが「〇〇の一種である」というアイデンティティ(Is-aの関係)を表すのに対し、インターフェースは「〇〇という動作が可能である」という振る舞いを定義します。
インターフェースは、メソッド、プロパティ、イベント、インデクサーのシグネチャ(宣言)のみを含み、実装を一切持ちません(C# 8.0以降のデフォルト実装を除く)。そのため、あるクラスがインターフェースを「実装」する場合、そのインターフェースが定義するすべてのメンバーの実装をクラス自身で提供する必要があります。
クラスは複数のインターフェースを同時に実装できるため、多様な能力を組み合わせることが可能になります。これが、単一継承しか許されない抽象クラスとの最大の違いです。
// 充電可能な能力を定義するインターフェース
public interface IChargeable
{
int BatteryLevel { get; }
void Charge();
}
// 輸送能力を定義するインターフェース
public interface ITransportable
{
void LoadCargo(string cargo);
void UnloadCargo();
}
// 電動スクーター(VehicleBaseを継承し、IChargeableとITransportableを実装)
public class ElectricScooter : VehicleBase, IChargeable, ITransportable
{
private int _batteryLevel;
private string _cargo;
public override string IdentificationNumber { get; } = "SCOOTER-A1B2";
public override int MaxSpeed { get; } = 30;
public int BatteryLevel => _batteryLevel;
public override void Move()
{
if (_batteryLevel > 0)
{
Console.WriteLine("モーターを駆動して静かに走行します。");
_batteryLevel -= 5;
}
else
{
Console.WriteLine("バッテリーがありません。充電してください。");
}
}
public void Charge()
{
_batteryLevel = 100;
Console.WriteLine("充電が完了しました。");
}
public void LoadCargo(string cargo)
{
_cargo = cargo;
Console.WriteLine($"{cargo}を積み込みました。");
}
public void UnloadCargo()
{
Console.WriteLine($"{_cargo}を降ろしました。");
_cargo = null;
}
}
この例では、`ElectricScooter`は`VehicleBase`という「乗り物」であると同時に、「充電可能(`IChargeable`)」であり、「輸送可能(`ITransportable`)」であるという複数の能力を持ち合わせています。このように、クラスの本質的なアイデンティティは抽象クラスで、付随的な機能はインターフェースで定義するのが効果的です。
インターフェースの主な特性
- 定義には`interface`キーワードが必須です。
- インスタンス化できず、クラスに実装されることを前提とします。
- メンバーのシグネチャのみを定義し、実装を含みません(原則)。
- 1つのクラスが複数のインターフェースを実装できます(多重継承の代替)。
- メンバーはデフォルトで`public`であり、アクセス修飾子を付けません。
- フィールド(変数)を定義することはできません。
抽象クラスとインターフェースの比較
以下の表は、両者の特性を要約したものです。
| 特徴 | 抽象クラス | インターフェース |
|---|---|---|
| 継承・実装 | クラスは1つの抽象クラスのみを継承可能(単一継承) | クラスは複数のインターフェースを実装可能 |
| メンバーの実装 | 実装済みのメンバーと抽象メンバーを定義できる | 原則として、実装を持つメンバーを定義できない(シグネチャのみ) |
| アクセス修飾子 | メンバーに`protected`、`public`など様々なアクセス修飾子を指定可能 | メンバーは暗黙的に`public`となる |
| フィールドの定義 | フィールド(変数)を持つことができる | フィールドを持つことができない |
| 主な用途 | 共通のアイデンティティを持つクラス群の基本的な骨格を定義する「Is-a」関係 | クラスに特定の機能や能力を付与する「Can-do」関係 |
| 変更の影響度 | 非抽象メンバーの追加は、既存のサブクラスに影響を与えない | メンバーの追加や変更は、実装している全てのクラスに影響を及ぼす |
使い分けの指針
設計において、抽象クラスとインターフェースのどちらを選択すべきかの指針は非常にシンプルです。
- 共通の「核となるアイデンティティ」を表現したい場合:
複数のクラスが同じ「種類」に属し、共通の状態や基本振る舞いを共有する場合は、抽象クラスを選択します。例えば、`Animal`(動物)や`VehicleBase`(乗り物)のように、オブジェクトの本質を定義するのに適しています。 - 特定の「能力」や「振る舞い」を付与したい場合:
異なる種類のクラスにまたがって、特定の機能を持たせたい場合は、インターフェースを選択します。例えば、`IFlyable`(飛行可能)、`IComparable`(比較可能)のように、オブジェクトが「何ができるか」を定義するのに適しています。
最終的に、両者はコードに構造化された制約を課し、特に大規模なチーム開発において一貫性を保ち、保守性を高めるための強力なツールです。目的に応じてこれらを使い分けることで、より柔軟で堅牢なオブジェクト指向設計が実現できます。