【C#】インターフェースについて
はじめに
C#におけるインターフェースは、クラスや構造体が特定の機能を実装するための契約を定義する重要な要素です。インターフェースを使用することで、柔軟で拡張性の高いコード設計が可能になります。また、SOLID原則を実現するためには不可欠な要素となります。
本記事では、インターフェースのメリットとデメリットを実例を交えて解説します。
本記事の対象
- C#を用いてこれから開発をしていく方
- インターフェースについてあまり知らない方
メリット
1. 多重継承の代替
C#ではクラスの多重継承がサポートされていませんが、インターフェースを使用することで、複数の異なるインターフェースを同時に実装することが可能です。これにより、クラスの役割に沿った複数の機能を持たせることができます。
対応するSOLID原則:
-
インターフェース分離の原則 (Interface Segregation Principle):
実装クラスは、継承元の使用しないメソッドへの依存を強制されてはならない。大きなインターフェースよりも、特定の機能に特化した小さなインターフェースに分割すべきという考え方です。
具体例:
// 複数のインターフェースの定義
public interface IPrintable
{
void Print();
}
public interface IScannable
{
void Scan();
}
// インターフェースを実装するクラス(多重継承)
public class MultiFunctionPrinter : IPrintable, IScannable
{
public void Print()
{
Console.WriteLine("Printing document By MultiFunctionPrinter..");
}
public void Scan()
{
Console.WriteLine("Scanning document...");
}
}
// インターフェースを実装するクラス
public class SinglePrintFunctionPrinter : IPrintable
{
public void Print()
{
Console.WriteLine("Printing document By SinglePrintFunctionPrinter...");
}
}
2. 疎結合な設計
インターフェースを使用することで、依存関係を抽象化し、クラス間の結合度を低減させることができます。これにより、コードのメンテナンス性やテストの容易さが向上します。
対応するSOLID原則:
-
依存関係逆転の原則 (Dependency Inversion Principle)
高水準モジュールは低水準モジュールに依存してはならず、両者は抽象に依存すべきです。また、抽象は詳細に依存してはならず、詳細が抽象に依存すべきという考え方です。
具体例:
// エンティティ
public class User
{
public int Id { get; set; }
public string Name { get; set; }
}
// インターフェースの定義
public interface IUserRepository
{
User Find(string id);
}
// インターフェースを実装したクラス
public class UserRepository : IUserRepository
{
public User Find(string id)
{
// 外部サービスからユーザー情報を取得
return user;
}
}
// 依存性を注入するクラス
public class UserService
{
private readonly IUserRepository _userRepository;
public UserService(IUserRepository userRepository)
{
_logger = logger;
}
public void FindUser(string id)
{
// ユーザー取得
var user = _userRepository.Find(id);
}
}
3. ポリモーフィズムの実現
インターフェースを通じて、異なるクラスのオブジェクトを同一の型として扱うことができ、統一的な操作が可能になります。これにより、柔軟で拡張性の高いコードが実現します。
対応するSOLID原則:
-
リスコフの置換原則 (Liskov Substitution Principle)
派生クラスは、基底クラスと置換可能でなければなりません。つまり、基底クラスのオブジェクトが期待される場所に派生クラスのオブジェクトを置いても、正しく動作する必要があるという考え方です。
具体例:
// インターフェースの定義
public interface IPayment
{
void ProcessPayment(decimal amount);
}
// インターフェースを実装したクラス
public class CreditCardPayment : IPayment
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing credit card payment of {amount:C}.");
}
}
public class PayPalPayment : IPayment
{
public void ProcessPayment(decimal amount)
{
Console.WriteLine($"Processing PayPal payment of {amount:C}.");
}
}
// ポリモーフィックに利用するクラス
public class PaymentProcessor
{
private readonly IPayment _paymentMethod;
// インスタンス作成時にどの支払方法であるかを注入
public PaymentProcessor(IPayment paymentMethod)
{
_paymentMethod = paymentMethod;
}
public void MakePayment(decimal amount)
{
_paymentMethod.ProcessPayment(amount);
}
}
4. モックを用いたテスト容易性
インターフェースを使用することで、モックオブジェクトを作成しやすくなり、ユニットテストの効率性が向上します。これにより、実際の実装に依存せずにテストを行うことが可能となります。
具体例:
// インターフェースの定義
public interface IOrderRepository
{
string GetOrderStatus(int orderId);
}
// 実装クラス
public class OrderRepository : IOrderRepository
{
public string GetOrderStatus(int orderId)
{
// データベースから注文ステータスを取得
return {取得結果};
}
}
// テスト対象クラス
public class OrderProcessor
{
private readonly IOrderRepository _orderRepository;
public OrderProcessor(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
// インターフェースを介して実行した結果により、条件分岐が発生する処理
public string ProcessOrder(int orderId)
{
var status = _orderRepository.GetOrderStatus(orderId);
if (status == "0")
{
return "Completed";
}
else if (status == "1")
{
return "Pending";
}
else
{
return "Unknown";
}
}
}
// テストクラス(例: NUnitとMoqを使用)
using NUnit.Framework;
using Moq;
[TestFixture]
public class OrderProcessorTests
{
[Test]
public void ProcessOrder_OrderCompleted_ReturnsCompletedMessage()
{
// Arrange(準備)
var mockRepo = new Mock<IOrderRepository>();
// データベースから取得した想定でモックインスタンスに対して結果を指定してあげる
mockRepo.Setup(repo => repo.GetOrderStatus(It.IsAny<int>())).Returns("0");
var processor = new OrderProcessor(mockRepo.Object);
// Act(実行)
var result = processor.ProcessOrder(1);
// Assert(検証)
Assert.AreEqual("Completed", result);
}
[Test]
public void ProcessOrder_OrderPending_ReturnsPendingMessage()
{
// Arrange(準備)
var mockRepo = new Mock<IOrderRepository>();
mockRepo.Setup(repo => repo.GetOrderStatus(It.IsAny<int>())).Returns("1");
var processor = new OrderProcessor(mockRepo.Object);
// Act(実行)
var result = processor.ProcessOrder(2);
// Assert(検証)
Assert.AreEqual("Pending", result);
}
[Test]
public void ProcessOrder_UnknownStatus_ReturnsUnknownMessage()
{
// Arrange(準備)
var mockRepo = new Mock<IOrderRepository>();
mockRepo.Setup(repo => repo.GetOrderStatus(It.IsAny<int>())).Returns("Unknown");
var processor = new OrderProcessor(mockRepo.Object);
// Act(実行)
var result = processor.ProcessOrder(3);
// Assert(検証)
Assert.AreEqual("Unknown", result);
}
}
デメリット
1. 実装の冗長性
インターフェースを使用すると、同じメソッドの定義を複数のクラスで実装する必要があり、実装コードが冗長になる可能性があります。
2. 変更時の影響範囲
インターフェースの変更は、そのインターフェースを実装しているすべてのクラスに影響を及ぼします。そのため、インターフェースの設計段階で慎重な検討が必要です。
まとめ
インターフェースは、多重継承の代替、疎結合な設計、ポリモーフィズムの実現などのメリットを提供します。(他にもチーム開発時の統一性等のメリットはあるかと思います)
一方で、実装の冗長性や変更時の影響範囲が大きくなることもあるので、システムの規模やチーム状況を鑑みた上で実装可否を決定する必要があるかと思います。
ご質問やご指摘事項がございましたら、お気軽にお寄せください。
Discussion
そのための既定のインターフェイス メソッドではないでしょうか?
そのために 既定のインターフェスメソッドができました。
ご指摘ありがとうございます。
おっしゃる通りでデメリットとして言及していた箇所は「既定のインターフェイス メソッド」を用いることで安全な変更は可能かと思います。設計図やテスト観点が複雑になる(継承クラスのオーバーライド有無の考慮が必要)になるのが、欠点でしょうか。
本記事では、以下の観点で言及をしておりませんでしたが、記事の内容としては重要な部分かと思いましたので見直したいと思います。
機能によっては 言語バージョンを上げてやれば .NET Framework でも 機能するのですが、この規定のインターフェスメソッドに に関しては実行環境側の修正も必要な為、 .NET Core 3.0 以降のランタイムが必要みたいです。
ご共有ありがとうございます。。!
まだまだ、技術知識が乏しいので大変参考になります!