C# インターフェース完全ガイド:設計の基礎から実践的な活用まで7つのテクニック

はじめに

C#におけるインターフェースは、堅牢で保守性の高いソフトウェアを設計する上で欠かせない機能です。
本記事では、インターフェースの基本概念から実践的な活用方法まで、実務で即座に活用できる知識を体系的に解説します。

本記事で学べること

インターフェースの基本概念と実装方法

SOLID原則に基づいたインターフェース設計の手法

依存性注入とテストを考慮した実装テクニック

一般的なデザインパターンでのインターフェースの活用方法

パフォーマンスとバージョン管理を考慮した実装のコツ

実際のプロジェクトで使える具体的な実装例

インターフェースとは?使う理由と基本概念

インターフェース(interface)は、C#プログラミングにおける重要な概念の1つで、クラスが実装すべきメンバーを定義する契約のような役割を果たします。
実務では、ECサイトでの決済処理、データベースアクセス、ログ出力など、実装の詳細を抽象化したい場合に特に有効です。

クラスと異なるインターフェースの特徴

1. 実装を持たない

  • インターフェースはメソッドやプロパティの「シグネチャ」のみを定義
  • 具体的な実装はインターフェースを実装するクラスが担当
// インターフェースの定義例
public interface ILogger
{
    void Log(string message);    // メソッドの宣言のみ
    LogLevel Level { get; set; } // プロパティの宣言のみ
}

// インターフェースの実装例
public class FileLogger : ILogger
{
    public LogLevel Level { get; set; }

    public void Log(string message)
    {
        // 具体的な実装をここで行う
        File.WriteAllText($"log_{DateTime.Now:yyyyMMdd}.txt", message);
    }
}

// 別の実装例
public class DatabaseLogger : ILogger
{
    public LogLevel Level { get; set; }

    public void Log(string message)
    {
        using var connection = new SqlConnection("connection_string");
        // データベースにログを保存する実装
    }
}

2. 多重継承が可能

  • クラスと異なり、複数のインターフェースを同時に実装可能
  • これにより、柔軟な機能の組み合わせが実現できる
public interface IEmailSender
{
    Task SendEmailAsync(string to, string subject, string body);
}

public interface ISMSSender
{
    Task SendSMSAsync(string to, string message);
}

// 複数のインターフェースを実装
public class NotificationService : IEmailSender, ISMSSender
{
    public async Task SendEmailAsync(string to, string subject, string body)
    {
        // メール送信の実装
        await Task.CompletedTask; // 実際の実装ではSMTPクライアントなどを使用
    }

    public async Task SendSMSAsync(string to, string message)
    {
        // SMS送信の実装
        await Task.CompletedTask; // 実際の実装ではSMSサービスのAPIを使用
    }
}

3. フィールドを持てない

  • インターフェースは状態を持つことができない
  • メソッド、プロパティ、イベント、インデクサーのみを定義可能

なぜインターフェースを使うのか?3つの主要なメリット

1. 疎結合の実現

  • コンポーネント間の依存関係を最小限に抑える
  • テスト容易性の向上
  • 変更に強い設計の実現
// 疎結合の例
public class OrderProcessor
{
    private readonly IPaymentGateway _paymentGateway;
    private readonly ILogger _logger;

    public OrderProcessor(IPaymentGateway paymentGateway, ILogger logger)
    {
        _paymentGateway = paymentGateway;
        _logger = logger;
    }

    public async Task ProcessOrderAsync(Order order)
    {
        try
        {
            // 支払い処理の実行
            await _paymentGateway.ProcessPaymentAsync(order.Amount);
            _logger.Log($"Order {order.Id} processed successfully");
        }
        catch (Exception ex)
        {
            _logger.Log($"Error processing order {order.Id}: {ex.Message}");
            throw;
        }
    }
}

2. 柔軟性と拡張性の向上

  • 新しい実装の追加が容易
  • 既存コードの変更なしで機能拡張が可能
  • プラグイン型アーキテクチャの実現

3. 契約による設計の実現

  • インターフェースが明確な契約として機能
  • チーム開発での役割分担が明確に
  • コードの品質と保守性の向上
// 契約による設計の例
public interface ICustomerRepository
{
    Task<Customer> GetByIdAsync(int id);
    Task<Customer> CreateAsync(Customer customer);
    Task UpdateAsync(Customer customer);
    Task DeleteAsync(int id);
}

// チームメンバーAが実装
public class SqlCustomerRepository : ICustomerRepository
{
    private readonly string _connectionString;

    public SqlCustomerRepository(string connectionString)
    {
        _connectionString = connectionString;
    }

    // SQLデータベースを使用した実装
    public async Task<Customer> GetByIdAsync(int id)
    {
        using var connection = new SqlConnection(_connectionString);
        return await connection.QueryFirstOrDefaultAsync<Customer>(
            "SELECT * FROM Customers WHERE Id = @Id",
            new { Id = id }
        );
    }

    // その他のメソッド実装
}

// チームメンバーBが実装
public class MongoCustomerRepository : ICustomerRepository
{
    private readonly IMongoDatabase _database;

    public MongoCustomerRepository(IMongoDatabase database)
    {
        _database = database;
    }

    // MongoDBを使用した実装
    public async Task<Customer> GetByIdAsync(int id)
    {
        var collection = _database.GetCollection<Customer>("customers");
        return await collection.Find(c => c.Id == id).FirstOrDefaultAsync();
    }

    // その他のメソッド実装
}

インターフェースを適切に活用することで、保守性が高く、拡張性のあるコードを書くことができます。
次のセクションでは、これらのインターフェースを実際にどのように実装していくのかを、より詳しく見ていきましょう。

インターフェースの基本的な実装方法

プログラミングの実践では、インターフェースを正しく実装することが重要です。
このセクションでは、C#でのインターフェースの実装方法を、現代的なプラクティスに基づいて解説していきます。

インターフェースの宣言と実装の基本構文

インターフェースの宣言と実装には、以下のような基本的なルールがあります。

1. インターフェースの宣言

public interface IDataService<T> where T : class
{
    // 非同期メソッドの宣言
    Task<T> GetByIdAsync(int id);
    Task<IEnumerable<T>> GetAllAsync();
    Task<T> CreateAsync(T entity);

    // プロパティの宣言
    bool IsConnected { get; }

    // イベントの宣言
    event EventHandler<DataChangedEventArgs<T>> DataChanged;
}

// イベント引数の定義
public class DataChangedEventArgs<T> : EventArgs
{
    public T Data { get; }
    public ChangeType ChangeType { get; }
    public DateTime Timestamp { get; }

    public DataChangedEventArgs(T data, ChangeType changeType)
    {
        Data = data;
        ChangeType = changeType;
        Timestamp = DateTime.UtcNow;
    }
}

public enum ChangeType
{
    Created,
    Updated,
    Deleted
}

2. ジェネリックインターフェースの実装

public class SqlDataService<T> : IDataService<T> where T : class
{
    private readonly DbContext _context;
    private readonly DbSet<T> _dbSet;
    private bool _isConnected;

    public SqlDataService(DbContext context)
    {
        _context = context;
        _dbSet = context.Set<T>();
        _isConnected = true;
    }

    // プロパティの実装
    public bool IsConnected => _isConnected;

    // イベントの実装
    public event EventHandler<DataChangedEventArgs<T>> DataChanged;

    // 非同期メソッドの実装
    public async Task<T> GetByIdAsync(int id)
    {
        return await _dbSet.FindAsync(id);
    }

    public async Task<IEnumerable<T>> GetAllAsync()
    {
        return await _dbSet.ToListAsync();
    }

    public async Task<T> CreateAsync(T entity)
    {
        var entry = await _dbSet.AddAsync(entity);
        await _context.SaveChangesAsync();

        // イベントの発火
        OnDataChanged(entity, ChangeType.Created);

        return entry.Entity;
    }

    // イベント発火用の保護メソッド
    protected virtual void OnDataChanged(T data, ChangeType changeType)
    {
        DataChanged?.Invoke(this, new DataChangedEventArgs<T>(data, changeType));
    }
}

複数インターフェースの実装テクニック

複数のインターフェースを実装する際の主要なテクニックを見ていきましょう。

1. 明示的実装を使用した競合解決

public interface IReadableStorage
{
    Task<string> ReadAsync(string key);
}

public interface IWritableStorage
{
    Task WriteAsync(string key, string value);
}

public interface IVersionedStorage
{
    Task<string> ReadAsync(string key, int version);
}

public class FileStorage : IReadableStorage, IWritableStorage, IVersionedStorage
{
    private readonly string _basePath;

    public FileStorage(string basePath)
    {
        _basePath = basePath;
        Directory.CreateDirectory(basePath);
    }

    // IReadableStorageの実装
    async Task<string> IReadableStorage.ReadAsync(string key)
    {
        var path = Path.Combine(_basePath, key);
        return await File.ReadAllTextAsync(path);
    }

    // IWritableStorageの実装
    async Task IWritableStorage.WriteAsync(string key, string value)
    {
        var path = Path.Combine(_basePath, key);
        await File.WriteAllTextAsync(path, value);
    }

    // IVersionedStorageの実装
    async Task<string> IVersionedStorage.ReadAsync(string key, int version)
    {
        var path = Path.Combine(_basePath, $"{key}.{version}");
        return await File.ReadAllTextAsync(path);
    }
}

2. インターフェースの継承を活用した階層的実装

public interface IBaseLogger
{
    Task LogAsync(string message);
}

public interface IAdvancedLogger : IBaseLogger
{
    Task LogWithSeverityAsync(string message, LogSeverity severity);
    Task<IEnumerable<LogEntry>> GetRecentLogsAsync(int count);
}

public class FullLogger : IAdvancedLogger
{
    private readonly List<LogEntry> _logs = new();
    private readonly IConfiguration _configuration;

    public FullLogger(IConfiguration configuration)
    {
        _configuration = configuration;
    }

    public async Task LogAsync(string message)
    {
        await LogWithSeverityAsync(message, LogSeverity.Information);
    }

    public async Task LogWithSeverityAsync(string message, LogSeverity severity)
    {
        var entry = new LogEntry
        {
            Message = message,
            Severity = severity,
            Timestamp = DateTime.UtcNow
        };

        _logs.Add(entry);

        // 設定に基づいてログを永続化
        if (_configuration.GetValue<bool>("PersistLogs"))
        {
            await PersistLogEntryAsync(entry);
        }
    }

    public Task<IEnumerable<LogEntry>> GetRecentLogsAsync(int count)
    {
        return Task.FromResult(
            _logs.OrderByDescending(l => l.Timestamp)
                 .Take(count)
                 .AsEnumerable()
        );
    }

    private async Task PersistLogEntryAsync(LogEntry entry)
    {
        // ログの永続化処理
        await Task.CompletedTask;
    }
}

デフォルト実装の活用方法

C# 8.0以降で導入されたデフォルト実装機能を使用すると、インターフェースにデフォルトの振る舞いを定義できます。

1. 基本的なデフォルト実装

public interface IMetricsCollector
{
    // 基本的なメトリクス収集メソッド
    Task CollectMetricsAsync();

    // デフォルト実装を持つメソッド
    Task<MetricsSummary> GetSummaryAsync() => Task.FromResult(new MetricsSummary
    {
        CollectedAt = DateTime.UtcNow,
        Status = "Default Summary"
    });

    // デフォルト実装を持つプロパティ
    bool IsEnabled { get => true; }
}

// デフォルト実装を使用
public class BasicMetricsCollector : IMetricsCollector
{
    public async Task CollectMetricsAsync()
    {
        // メトリクス収集の基本実装
        await Task.CompletedTask;
    }
    // GetSummaryAsync とIsEnabled はデフォルト実装を使用
}

// デフォルト実装をオーバーライド
public class CustomMetricsCollector : IMetricsCollector
{
    private readonly ILogger<CustomMetricsCollector> _logger;

    public CustomMetricsCollector(ILogger<CustomMetricsCollector> logger)
    {
        _logger = logger;
    }

    public async Task CollectMetricsAsync()
    {
        await Task.CompletedTask;
        _logger.LogInformation("Metrics collected");
    }

    public async Task<MetricsSummary> GetSummaryAsync()
    {
        // カスタムサマリー生成ロジック
        return await Task.FromResult(new MetricsSummary
        {
            CollectedAt = DateTime.UtcNow,
            Status = "Custom Summary"
        });
    }
}

実装時の重要なポイント

  1. 非同期処理の一貫した使用
    • 非同期メソッドには必ずAsync接尾辞を付ける
    • Task/ValueTaskの適切な使用
    • 非同期操作のキャンセレーション対応を検討
  2. インターフェースの責務の明確化
    • 単一責任の原則に従う
    • 適切な粒度でのインターフェース分割
    • 明確な命名規則の適用
  3. エラーハンドリング
    • 適切な例外型の使用
    • 例外の適切な伝播
    • リソースの確実な解放

次のセクションでは、これらの実装テクニックを活用したインターフェース設計のベストプラクティスについて詳しく見ていきましょう。

インターフェース設計のベストプラクティス

優れたインターフェース設計は、保守性の高い堅牢なシステムの基盤となります。
このセクションでは、SOLID原則に基づいた実践的なインターフェース設計の原則とベストプラクティスについて解説します。

インターフェース分離の原則(ISP)の実践

インターフェース分離の原則は、「クライアントは、使用しないメソッドへの依存を強制されるべきではない」という考え方です。
この原則を実践することで、より柔軟で保守性の高いコードを実現できます。

1. 問題のある設計例

// 避けるべき設計:肥大化したインターフェース
public interface IUserService
{
    // ユーザー管理
    Task<User> CreateUserAsync(User user);
    Task<User> GetUserAsync(int userId);
    Task UpdateUserAsync(User user);
    Task DeleteUserAsync(int userId);

    // メール送信
    Task SendWelcomeEmailAsync(string email);
    Task SendPasswordResetEmailAsync(string email);

    // 認証
    Task<bool> ValidateCredentialsAsync(string email, string password);
    Task<string> GeneratePasswordResetTokenAsync(string email);

    // レポート生成
    Task<UserReport> GenerateUserReportAsync(int userId);
    Task<ActivityReport> GenerateActivityReportAsync(DateTime startDate, DateTime endDate);
}

2. 改善された設計例

// ユーザー管理の基本操作
public interface IUserRepository
{
    Task<User> CreateAsync(User user);
    Task<User> GetByIdAsync(int userId);
    Task UpdateAsync(User user);
    Task DeleteAsync(int userId);
}

// メール送信機能
public interface IEmailService
{
    Task SendEmailAsync(string email, string subject, string body);
    Task SendTemplatedEmailAsync(string email, string templateName, object templateData);
}

// ユーザー認証
public interface IAuthenticationService
{
    Task<bool> ValidateCredentialsAsync(string email, string password);
    Task<string> GeneratePasswordResetTokenAsync(string email);
    Task<bool> ValidatePasswordResetTokenAsync(string email, string token);
}

// レポート生成
public interface IReportGenerator<T>
{
    Task<T> GenerateReportAsync(ReportParameters parameters);
}

// 実装例
public class UserService : IUserRepository
{
    private readonly IEmailService _emailService;
    private readonly ILogger<UserService> _logger;
    private readonly DbContext _context;

    public UserService(
        IEmailService emailService,
        ILogger<UserService> logger,
        DbContext context)
    {
        _emailService = emailService;
        _logger = logger;
        _context = context;
    }

    public async Task<User> CreateAsync(User user)
    {
        try
        {
            var entry = await _context.Users.AddAsync(user);
            await _context.SaveChangesAsync();

            await _emailService.SendTemplatedEmailAsync(
                user.Email,
                "WelcomeEmail",
                new { user.Name }
            );

            return entry.Entity;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error creating user");
            throw;
        }
    }

    // その他のメソッド実装
}

依存性逆転の原則(DIP)とインターフェース

依存性逆転の原則は、「上位モジュールは下位モジュールに依存すべきではない。両者は抽象に依存すべき」という考え方です。

// ドメインレイヤー(中心部)
public interface IOrderRepository
{
    Task<Order> GetByIdAsync(int id);
    Task<Order> CreateAsync(Order order);
    Task UpdateAsync(Order order);
}

// アプリケーションレイヤー(ユースケース)
public class OrderProcessingService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IPaymentProcessor _paymentProcessor;
    private readonly INotificationService _notificationService;

    public OrderProcessingService(
        IOrderRepository orderRepository,
        IPaymentProcessor paymentProcessor,
        INotificationService notificationService)
    {
        _orderRepository = orderRepository;
        _paymentProcessor = paymentProcessor;
        _notificationService = notificationService;
    }

    public async Task<ProcessOrderResult> ProcessOrderAsync(Order order)
    {
        try
        {
            // 注文の保存
            var savedOrder = await _orderRepository.CreateAsync(order);

            // 支払い処理
            var paymentResult = await _paymentProcessor.ProcessPaymentAsync(
                new PaymentRequest
                {
                    OrderId = savedOrder.Id,
                    Amount = savedOrder.TotalAmount,
                    Currency = savedOrder.Currency
                }
            );

            if (paymentResult.Success)
            {
                // 注文ステータスの更新
                savedOrder.Status = OrderStatus.Paid;
                await _orderRepository.UpdateAsync(savedOrder);

                // 通知の送信
                await _notificationService.SendOrderConfirmationAsync(savedOrder);

                return new ProcessOrderResult
                {
                    Success = true,
                    OrderId = savedOrder.Id
                };
            }
            else
            {
                return new ProcessOrderResult
                {
                    Success = false,
                    ErrorMessage = paymentResult.ErrorMessage
                };
            }
        }
        catch (Exception ex)
        {
            return new ProcessOrderResult
            {
                Success = false,
                ErrorMessage = "Order processing failed"
            };
        }
    }
}

命名規則とコーディング規約

1. 命名規則のベストプラクティス

// 良い例:明確な名前と責務
public interface IOrderValidator
{
    Task<ValidationResult> ValidateAsync(Order order);
}

public interface IProductCatalog
{
    Task<Product> GetProductAsync(int productId);
    Task<IEnumerable<Product>> SearchProductsAsync(ProductSearchCriteria criteria);
}

// 避けるべき例:曖昧な名前と責務
public interface IProcessor  // 曖昧すぎる
{
    Task ProcessAsync(object data);  // 汎用的すぎる
}

public interface IHandler   // 具体性に欠ける
{
    void Handle(string input);
}

2. インターフェース設計の規約

// 良い例:一貫性のある抽象化レベル
public interface IDocumentProcessor
{
    Task<ProcessingResult> ProcessDocumentAsync(Document document);
    Task<ValidationResult> ValidateDocumentAsync(Document document);
    Task<DocumentMetadata> ExtractMetadataAsync(Document document);
}

// 良い例:イベントを含むインターフェース
public interface IDocumentStorage
{
    Task<Document> SaveAsync(Document document);
    Task<Document> GetByIdAsync(string documentId);

    // イベント
    event EventHandler<DocumentChangedEventArgs> DocumentChanged;
    event EventHandler<DocumentDeletedEventArgs> DocumentDeleted;
}

// 避けるべき例:抽象化レベルが不統一
public interface IDocumentHandler
{
    Task<Document> ProcessDocumentAsync(Document document);
    byte[] ConvertToPdf();  // 具体的すぎる
    void SaveToDatabase();  // インフラストラクチャの責務が混入
}

実装時の重要なガイドライン

  1. インターフェースの責務
    • 単一の目的に焦点を当てる
    • ビジネスドメインの用語を使用
    • 実装の詳細を抽象化
  2. メソッドの設計
    • 非同期メソッドには必ずAsync接尾辞を付ける
    • パラメータは明確で型安全なものにする
    • 戻り値の型は具体的に指定
  3. バージョニング
    • 既存のインターフェースは変更しない
    • 新機能は新しいインターフェースとして追加
    • 後方互換性を維持

これらの原則とベストプラクティスを適用することで、より保守性が高く、拡張しやすいシステムを設計することができます。
次のセクションでは、これらの原則を活用した実践的な実装テクニックについて見ていきましょう。

実践的なインターフェース活用テクニック

このセクションでは、実務でのインターフェース活用について、具体的な実装例とともに解説します。
依存性注入(DI)、テスト、プラグインアーキテクチャなど、実践的なテクニックを詳しく見ていきましょう。

依存性注入(DI)におけるインターフェースの役割

依存性注入は、コンポーネント間の結合度を下げ、テスト容易性を高めるための重要な手法です。

1. DIコンテナの設定と登録

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // トランジェントサービス(都度インスタンス生成)
        services.AddTransient<IOrderValidator, OrderValidator>();

        // スコープ付きサービス(リクエストスコープ内で共有)
        services.AddScoped<IOrderRepository, SqlOrderRepository>();
        services.AddScoped<ICustomerRepository, SqlCustomerRepository>();

        // シングルトンサービス(アプリケーション全体で共有)
        services.AddSingleton<ICacheService, RedisCacheService>();

        // 条件付き登録
        services.AddScoped<IPaymentProcessor>(sp =>
        {
            var configuration = sp.GetRequiredService<IConfiguration>();
            return configuration["PaymentProvider"] switch
            {
                "Stripe" => new StripePaymentProcessor(configuration),
                "PayPal" => new PayPalPaymentProcessor(configuration),
                _ => throw new InvalidOperationException("Invalid payment provider")
            };
        });

        // デコレーターパターンの登録
        services.AddScoped<IOrderProcessor, OrderProcessor>();
        services.Decorate<IOrderProcessor, OrderProcessorWithLogging>();
        services.Decorate<IOrderProcessor, OrderProcessorWithValidation>();
    }
}

2. DIを活用したサービスの実装

public interface IOrderProcessor
{
    Task<OrderResult> ProcessOrderAsync(Order order);
}

public class OrderProcessor : IOrderProcessor
{
    private readonly IOrderRepository _orderRepository;
    private readonly IPaymentProcessor _paymentProcessor;
    private readonly INotificationService _notificationService;
    private readonly ILogger<OrderProcessor> _logger;

    public OrderProcessor(
        IOrderRepository orderRepository,
        IPaymentProcessor paymentProcessor,
        INotificationService notificationService,
        ILogger<OrderProcessor> logger)
    {
        _orderRepository = orderRepository;
        _paymentProcessor = paymentProcessor;
        _notificationService = notificationService;
        _logger = logger;
    }

    public async Task<OrderResult> ProcessOrderAsync(Order order)
    {
        try
        {
            _logger.LogInformation("Processing order {OrderId}", order.Id);

            // 注文の保存
            await _orderRepository.SaveAsync(order);

            // 支払い処理
            var paymentResult = await _paymentProcessor.ProcessPaymentAsync(
                new PaymentRequest(order));

            if (paymentResult.Success)
            {
                order.Status = OrderStatus.Paid;
                await _orderRepository.UpdateAsync(order);

                // 非同期で通知を送信
                _ = _notificationService.SendOrderConfirmationAsync(order);

                return OrderResult.Success(order.Id);
            }

            return OrderResult.Failure(paymentResult.ErrorMessage);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to process order {OrderId}", order.Id);
            return OrderResult.Failure("Order processing failed");
        }
    }
}

モックを使用したユニットテストの実装

インターフェースを活用することで、依存コンポーネントのモック化が容易になり、効果的なユニットテストが実現できます。

1. xUnit と Moq を使用したテスト実装

public class OrderProcessorTests
{
    private readonly Mock<IOrderRepository> _orderRepositoryMock;
    private readonly Mock<IPaymentProcessor> _paymentProcessorMock;
    private readonly Mock<INotificationService> _notificationServiceMock;
    private readonly Mock<ILogger<OrderProcessor>> _loggerMock;
    private readonly OrderProcessor _processor;

    public OrderProcessorTests()
    {
        _orderRepositoryMock = new Mock<IOrderRepository>();
        _paymentProcessorMock = new Mock<IPaymentProcessor>();
        _notificationServiceMock = new Mock<INotificationService>();
        _loggerMock = new Mock<ILogger<OrderProcessor>>();

        _processor = new OrderProcessor(
            _orderRepositoryMock.Object,
            _paymentProcessorMock.Object,
            _notificationServiceMock.Object,
            _loggerMock.Object);
    }

    [Fact]
    public async Task ProcessOrder_WhenPaymentSucceeds_UpdatesOrderAndSendsNotification()
    {
        // Arrange
        var order = new Order { Id = 1, Status = OrderStatus.Created };
        var paymentRequest = new PaymentRequest(order);
        var paymentResult = PaymentResult.Success();

        _paymentProcessorMock
            .Setup(p => p.ProcessPaymentAsync(It.IsAny<PaymentRequest>()))
            .ReturnsAsync(paymentResult);

        // Act
        var result = await _processor.ProcessOrderAsync(order);

        // Assert
        Assert.True(result.Success);
        Assert.Equal(order.Id, result.OrderId);

        _orderRepositoryMock.Verify(
            r => r.UpdateAsync(It.Is<Order>(o => 
                o.Id == order.Id && 
                o.Status == OrderStatus.Paid)),
            Times.Once);

        _notificationServiceMock.Verify(
            n => n.SendOrderConfirmationAsync(order),
            Times.Once);
    }

    [Fact]
    public async Task ProcessOrder_WhenPaymentFails_DoesNotUpdateOrder()
    {
        // Arrange
        var order = new Order { Id = 1, Status = OrderStatus.Created };
        var errorMessage = "Insufficient funds";

        _paymentProcessorMock
            .Setup(p => p.ProcessPaymentAsync(It.IsAny<PaymentRequest>()))
            .ReturnsAsync(PaymentResult.Failure(errorMessage));

        // Act
        var result = await _processor.ProcessOrderAsync(order);

        // Assert
        Assert.False(result.Success);
        Assert.Equal(errorMessage, result.ErrorMessage);

        _orderRepositoryMock.Verify(
            r => r.UpdateAsync(It.IsAny<Order>()),
            Times.Never);

        _notificationServiceMock.Verify(
            n => n.SendOrderConfirmationAsync(It.IsAny<Order>()),
            Times.Never);
    }
}

プラグインアーキテクチャの実現方法

インターフェースを活用することで、柔軟なプラグインアーキテクチャを実現できます。

1. プラグインシステムの基本構造

public interface IPlugin
{
    string Name { get; }
    string Version { get; }
    Task InitializeAsync();
    Task ExecuteAsync();
    Task ShutdownAsync();
}

public interface IPluginMetadata
{
    string Name { get; }
    string Version { get; }
    string Description { get; }
    IReadOnlyCollection<string> Dependencies { get; }
}

public interface IPluginManager
{
    Task LoadPluginAsync(string pluginPath);
    Task UnloadPluginAsync(string pluginName);
    Task<IPlugin> GetPluginAsync(string name);
    IReadOnlyCollection<IPluginMetadata> GetLoadedPlugins();
}

2. プラグインマネージャーの実装

public class PluginManager : IPluginManager
{
    private readonly Dictionary<string, IPlugin> _plugins = new();
    private readonly ILogger<PluginManager> _logger;
    private readonly IConfiguration _configuration;

    public PluginManager(
        ILogger<PluginManager> logger,
        IConfiguration configuration)
    {
        _logger = logger;
        _configuration = configuration;
    }

    public async Task LoadPluginAsync(string pluginPath)
    {
        try
        {
            // アセンブリのロード
            var assembly = Assembly.LoadFrom(pluginPath);

            // プラグイン型の検索
            var pluginTypes = assembly.GetTypes()
                .Where(t => typeof(IPlugin).IsAssignableFrom(t) && !t.IsInterface);

            foreach (var pluginType in pluginTypes)
            {
                // プラグインのインスタンス化
                if (Activator.CreateInstance(pluginType) is IPlugin plugin)
                {
                    await InitializePluginAsync(plugin);
                    _plugins[plugin.Name] = plugin;

                    _logger.LogInformation(
                        "Loaded plugin: {Name} v{Version}",
                        plugin.Name,
                        plugin.Version);
                }
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to load plugin from {Path}", pluginPath);
            throw;
        }
    }

    private async Task InitializePluginAsync(IPlugin plugin)
    {
        try
        {
            await plugin.InitializeAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to initialize plugin {Name}", plugin.Name);
            throw;
        }
    }

    // その他のメソッド実装
}

これらのテクニックを適切に組み合わせることで、保守性が高く、拡張性のあるアプリケーションを構築することができます。
次のセクションでは、よく使用されるインターフェースの実装パターンについて詳しく見ていきましょう。

よくあるインターフェースの実装パターン

実務でのインターフェース活用では、確立されたデザインパターンを適切に適用することが重要です。
このセクションでは、一般的なデザインパターンのインターフェースを使用した実装方法を解説します。

リポジトリパターンでのインターフェース活用

リポジトリパターンは、データアクセス層を抽象化し、ビジネスロジックを分離するために広く使用されます。

1. ジェネリックリポジトリパターンの実装

public interface IRepository<TEntity, TKey> where TEntity : class
{
    Task<TEntity> GetByIdAsync(TKey id);
    Task<IReadOnlyList<TEntity>> GetAllAsync();
    Task<IReadOnlyList<TEntity>> FindAsync(Expression<Func<TEntity, bool>> predicate);
    Task<TEntity> AddAsync(TEntity entity);
    Task UpdateAsync(TEntity entity);
    Task DeleteAsync(TKey id);
}

public interface IUnitOfWork : IDisposable
{
    Task<int> SaveChangesAsync();
    Task BeginTransactionAsync();
    Task CommitAsync();
    Task RollbackAsync();
}

// 具体的なリポジトリの実装
public class EntityFrameworkRepository<TEntity, TKey> : IRepository<TEntity, TKey>
    where TEntity : class
{
    protected readonly DbContext _context;
    protected readonly DbSet<TEntity> _dbSet;
    private readonly ILogger<EntityFrameworkRepository<TEntity, TKey>> _logger;

    public EntityFrameworkRepository(
        DbContext context,
        ILogger<EntityFrameworkRepository<TEntity, TKey>> logger)
    {
        _context = context;
        _dbSet = context.Set<TEntity>();
        _logger = logger;
    }

    public virtual async Task<TEntity> GetByIdAsync(TKey id)
    {
        try
        {
            return await _dbSet.FindAsync(id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting entity with ID {Id}", id);
            throw;
        }
    }

    public virtual async Task<IReadOnlyList<TEntity>> FindAsync(
        Expression<Func<TEntity, bool>> predicate)
    {
        try
        {
            return await _dbSet.Where(predicate).ToListAsync();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error finding entities with predicate");
            throw;
        }
    }

    // その他のメソッド実装
}

// Unit of Workの実装
public class EntityFrameworkUnitOfWork : IUnitOfWork
{
    private readonly DbContext _context;
    private IDbContextTransaction _transaction;

    public EntityFrameworkUnitOfWork(DbContext context)
    {
        _context = context;
    }

    public async Task BeginTransactionAsync()
    {
        _transaction = await _context.Database.BeginTransactionAsync();
    }

    public async Task CommitAsync()
    {
        try
        {
            await _context.SaveChangesAsync();
            await _transaction?.CommitAsync();
        }
        catch
        {
            await RollbackAsync();
            throw;
        }
    }

    // その他のメソッド実装
}

Strategyパターンの実装例

Strategyパターンは、アルゴリズムの切り替えを容易にする設計パターンです。

1. 割引計算のStrategy実装

public interface IDiscountStrategy
{
    Task<decimal> CalculateDiscountAsync(Order order);
    bool IsApplicable(Order order);
}

public class PercentageDiscountStrategy : IDiscountStrategy
{
    private readonly decimal _percentage;
    private readonly decimal _minimumOrderAmount;

    public PercentageDiscountStrategy(decimal percentage, decimal minimumOrderAmount)
    {
        _percentage = percentage;
        _minimumOrderAmount = minimumOrderAmount;
    }

    public Task<decimal> CalculateDiscountAsync(Order order)
    {
        return Task.FromResult(order.TotalAmount * (_percentage / 100));
    }

    public bool IsApplicable(Order order)
    {
        return order.TotalAmount >= _minimumOrderAmount;
    }
}

public class BulkDiscountStrategy : IDiscountStrategy
{
    private readonly int _minimumItems;
    private readonly decimal _discountPerItem;

    public BulkDiscountStrategy(int minimumItems, decimal discountPerItem)
    {
        _minimumItems = minimumItems;
        _discountPerItem = discountPerItem;
    }

    public Task<decimal> CalculateDiscountAsync(Order order)
    {
        return Task.FromResult(order.ItemCount * _discountPerItem);
    }

    public bool IsApplicable(Order order)
    {
        return order.ItemCount >= _minimumItems;
    }
}

// Strategyの使用
public class OrderProcessor
{
    private readonly IEnumerable<IDiscountStrategy> _discountStrategies;
    private readonly ILogger<OrderProcessor> _logger;

    public OrderProcessor(
        IEnumerable<IDiscountStrategy> discountStrategies,
        ILogger<OrderProcessor> logger)
    {
        _discountStrategies = discountStrategies;
        _logger = logger;
    }

    public async Task<decimal> CalculateFinalAmountAsync(Order order)
    {
        decimal totalDiscount = 0;

        foreach (var strategy in _discountStrategies)
        {
            if (strategy.IsApplicable(order))
            {
                var discount = await strategy.CalculateDiscountAsync(order);
                totalDiscount += discount;

                _logger.LogInformation(
                    "Applied discount of {Discount} using {Strategy}",
                    discount,
                    strategy.GetType().Name);
            }
        }

        return order.TotalAmount - totalDiscount;
    }
}

Observerパターンでのイベント処理

Observerパターンは、オブジェクト間の1対多の依存関係を定義するのに適しています。

1. イベントベースの注文処理システム

public interface IOrderEventObserver
{
    Task OnOrderCreatedAsync(OrderCreatedEvent @event);
    Task OnOrderUpdatedAsync(OrderUpdatedEvent @event);
    Task OnOrderCancelledAsync(OrderCancelledEvent @event);
}

public class OrderEvent
{
    public int OrderId { get; }
    public DateTime Timestamp { get; }

    protected OrderEvent(int orderId)
    {
        OrderId = orderId;
        Timestamp = DateTime.UtcNow;
    }
}

public class OrderCreatedEvent : OrderEvent
{
    public Order Order { get; }

    public OrderCreatedEvent(int orderId, Order order) : base(orderId)
    {
        Order = order;
    }
}

public class EmailNotificationObserver : IOrderEventObserver
{
    private readonly IEmailService _emailService;
    private readonly ILogger<EmailNotificationObserver> _logger;

    public EmailNotificationObserver(
        IEmailService emailService,
        ILogger<EmailNotificationObserver> logger)
    {
        _emailService = emailService;
        _logger = logger;
    }

    public async Task OnOrderCreatedAsync(OrderCreatedEvent @event)
    {
        try
        {
            await _emailService.SendEmailAsync(
                @event.Order.CustomerEmail,
                "Order Confirmation",
                $"Your order {@event.OrderId} has been received.");

            _logger.LogInformation(
                "Sent order confirmation email for order {@OrderId}",
                @event.OrderId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to send order confirmation email");
            throw;
        }
    }

    // その他のイベントハンドラ実装
}

// イベントディスパッチャー
public class OrderEventDispatcher
{
    private readonly IList<IOrderEventObserver> _observers = new List<IOrderEventObserver>();
    private readonly ILogger<OrderEventDispatcher> _logger;

    public OrderEventDispatcher(ILogger<OrderEventDispatcher> logger)
    {
        _logger = logger;
    }

    public void RegisterObserver(IOrderEventObserver observer)
    {
        _observers.Add(observer);
    }

    public async Task DispatchOrderCreatedAsync(OrderCreatedEvent @event)
    {
        foreach (var observer in _observers)
        {
            try
            {
                await observer.OnOrderCreatedAsync(@event);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex,
                    "Observer {ObserverType} failed to handle OrderCreatedEvent",
                    observer.GetType().Name);
            }
        }
    }
}

これらのパターンを適切に組み合わせることで、保守性が高く、拡張性のあるシステムを構築することができます。
次のセクションでは、インターフェースを使用する際の注意点について詳しく見ていきましょう。

インターフェースを使用する際の注意点

インターフェースは強力な設計ツールですが、適切に使用しないと様々な問題を引き起こす可能性があります。このセクションでは、実務での経験に基づく具体的な注意点と対策について解説します。

パフォーマンスへの影響と最適化

インターフェースの使用は、わずかながらパフォーマンスオーバーヘッドを引き起こす可能性があります。

1. メソッド呼び出しのオーバーヘッド測定

public class PerformanceTest
{
    private const int IterationCount = 10_000_000;
    private readonly ILogger<PerformanceTest> _logger;

    public PerformanceTest(ILogger<PerformanceTest> logger)
    {
        _logger = logger;
    }

    public void MeasureCallOverhead()
    {
        // 直接呼び出し
        var directImpl = new DataProcessor();
        var sw1 = Stopwatch.StartNew();

        for (int i = 0; i < IterationCount; i++)
        {
            directImpl.ProcessData("test");
        }

        sw1.Stop();

        // インターフェース経由の呼び出し
        IDataProcessor interfaceImpl = directImpl;
        var sw2 = Stopwatch.StartNew();

        for (int i = 0; i < IterationCount; i++)
        {
            interfaceImpl.ProcessData("test");
        }

        sw2.Stop();

        _logger.LogInformation(
            "Performance comparison:\n" +
            "Direct calls: {DirectTime}ms\n" +
            "Interface calls: {InterfaceTime}ms\n" +
            "Overhead per call: {Overhead}ns",
            sw1.ElapsedMilliseconds,
            sw2.ElapsedMilliseconds,
            (sw2.ElapsedTicks - sw1.ElapsedTicks) * 1000.0 / IterationCount);
    }
}

2. キャッシュを活用した最適化例

public interface ICacheableRepository<TEntity, TKey>
    where TEntity : class
{
    Task<TEntity> GetByIdAsync(TKey id);
    Task<IReadOnlyList<TEntity>> GetAllAsync();
    Task UpdateAsync(TEntity entity);
    Task InvalidateCacheAsync(TKey id);
}

public class CachedRepository<TEntity, TKey> : ICacheableRepository<TEntity, TKey>
    where TEntity : class
{
    private readonly IRepository<TEntity, TKey> _repository;
    private readonly IDistributedCache _cache;
    private readonly ILogger<CachedRepository<TEntity, TKey>> _logger;
    private readonly string _cachePrefix;
    private readonly TimeSpan _cacheExpiration;

    public CachedRepository(
        IRepository<TEntity, TKey> repository,
        IDistributedCache cache,
        ILogger<CachedRepository<TEntity, TKey>> logger,
        string cachePrefix = "",
        TimeSpan? cacheExpiration = null)
    {
        _repository = repository;
        _cache = cache;
        _logger = logger;
        _cachePrefix = cachePrefix;
        _cacheExpiration = cacheExpiration ?? TimeSpan.FromMinutes(10);
    }

    public async Task<TEntity> GetByIdAsync(TKey id)
    {
        var cacheKey = $"{_cachePrefix}{typeof(TEntity).Name}_{id}";

        try
        {
            // キャッシュからの取得を試みる
            var cachedData = await _cache.GetAsync(cacheKey);
            if (cachedData != null)
            {
                _logger.LogDebug("Cache hit for {Key}", cacheKey);
                return JsonSerializer.Deserialize<TEntity>(cachedData);
            }

            // データベースから取得
            var entity = await _repository.GetByIdAsync(id);
            if (entity != null)
            {
                // キャッシュに保存
                var options = new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = _cacheExpiration,
                    SlidingExpiration = TimeSpan.FromMinutes(2)
                };

                await _cache.SetAsync(
                    cacheKey,
                    JsonSerializer.SerializeToUtf8Bytes(entity),
                    options);
            }

            return entity;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error accessing cache for {Key}", cacheKey);
            // キャッシュエラー時はリポジトリから直接取得
            return await _repository.GetByIdAsync(id);
        }
    }

    // その他のメソッド実装
}

バージョン管理とインターフェースの進化

インターフェースの変更は既存のコードに重大な影響を与える可能性があります。

1. バージョニングの戦略実装

// 基本インターフェース(V1)
public interface IUserService
{
    Task<User> GetUserAsync(int id);
    Task<User> CreateUserAsync(UserCreateRequest request);
    Task UpdateUserAsync(User user);
}

// 拡張インターフェース(V2)
public interface IUserServiceV2 : IUserService
{
    Task<UserDetails> GetUserDetailsAsync(int id);
    Task<IReadOnlyList<User>> SearchUsersAsync(UserSearchCriteria criteria);
}

// さらなる拡張(V3)
public interface IUserServiceV3 : IUserServiceV2
{
    Task<User> CreateUserWithRolesAsync(UserCreateRequest request, IEnumerable<string> roles);
    Task<bool> ValidateUserAsync(int id);
}

// 最新機能を全て実装したサービス
public class ModernUserService : IUserServiceV3
{
    private readonly IUserRepository _repository;
    private readonly ILogger<ModernUserService> _logger;

    public ModernUserService(
        IUserRepository repository,
        ILogger<ModernUserService> logger)
    {
        _repository = repository;
        _logger = logger;
    }

    // V1のメソッド実装
    public async Task<User> GetUserAsync(int id)
    {
        try
        {
            return await _repository.GetByIdAsync(id);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error getting user {Id}", id);
            throw;
        }
    }

    // V2のメソッド実装
    public async Task<UserDetails> GetUserDetailsAsync(int id)
    {
        var user = await GetUserAsync(id);
        if (user == null) return null;

        return await _repository.GetUserDetailsAsync(id);
    }

    // V3のメソッド実装
    public async Task<User> CreateUserWithRolesAsync(
        UserCreateRequest request,
        IEnumerable<string> roles)
    {
        var user = await CreateUserAsync(request);
        await _repository.AssignRolesAsync(user.Id, roles);
        return user;
    }

    // その他のメソッド実装
}

過剰な抽象化を避けるための指針

過剰な抽象化は、コードの複雑性を不必要に増加させる可能性があります。

1. 避けるべき実装パターン

// 過剰な抽象化の例
public interface IStringManipulator
{
    string Manipulate(string input);
}

public interface IStringValidator
{
    bool Validate(string input);
}

public interface IStringFormatter
{
    string Format(string input);
}

public class StringProcessor : IStringManipulator, IStringValidator, IStringFormatter
{
    public string Manipulate(string input) => input.Trim();
    public bool Validate(string input) => !string.IsNullOrEmpty(input);
    public string Format(string input) => input.ToUpper();
}

// 改善された実装
public interface ITextProcessor
{
    string Process(string text);
    bool IsValid(string text);
}

public class TextProcessor : ITextProcessor
{
    public string Process(string text)
    {
        if (!IsValid(text)) throw new ArgumentException("Invalid text");
        return text.Trim().ToUpper();
    }

    public bool IsValid(string text) => !string.IsNullOrEmpty(text);
}

2. 適切な抽象化レベルの例

// 明確な責務を持つインターフェース
public interface IOrderValidator
{
    Task<ValidationResult> ValidateOrderAsync(Order order);
    Task<ValidationResult> ValidateShippingDetailsAsync(ShippingDetails details);
}

public interface IOrderProcessor
{
    Task<OrderResult> ProcessOrderAsync(Order order);
    Task<OrderStatus> GetOrderStatusAsync(int orderId);
}

// 実装クラス
public class OrderValidator : IOrderValidator
{
    private readonly IEnumerable<IValidationRule<Order>> _orderRules;
    private readonly IEnumerable<IValidationRule<ShippingDetails>> _shippingRules;
    private readonly ILogger<OrderValidator> _logger;

    public OrderValidator(
        IEnumerable<IValidationRule<Order>> orderRules,
        IEnumerable<IValidationRule<ShippingDetails>> shippingRules,
        ILogger<OrderValidator> logger)
    {
        _orderRules = orderRules;
        _shippingRules = shippingRules;
        _logger = logger;
    }

    public async Task<ValidationResult> ValidateOrderAsync(Order order)
    {
        try
        {
            var failures = new List<string>();

            foreach (var rule in _orderRules)
            {
                if (!await rule.ValidateAsync(order))
                {
                    failures.Add(rule.ErrorMessage);
                }
            }

            return new ValidationResult
            {
                IsValid = !failures.Any(),
                Errors = failures
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error validating order");
            throw;
        }
    }

    // その他のメソッド実装
}

実装時の重要なポイント

  1. パフォーマンスの最適化
    • 頻繁に呼び出されるメソッドでのインターフェース使用を最小限に
    • キャッシュの戦略的な活用
    • 非同期処理の適切な使用
  2. バージョン管理のベストプラクティス
    • 既存のインターフェースは変更しない
    • 新機能は継承を通じて追加
    • 適切なデフォルト実装の提供
  3. 抽象化レベルの設計指針
    • ビジネスドメインに基づいた抽象化
    • 単一責任の原則の遵守
    • 実装の詳細の適切な隠蔽

これらの注意点を考慮することで、より効果的でメンテナンス可能なインターフェース設計を実現できます。
次のセクションでは、これらの知識を活用した実践的な設計演習を行っていきましょう。

実践的なインターフェース設計演習

このセクションでは、実務で遭遇する具体的なシナリオに基づいて、インターフェース設計の実践的な演習を行います。
これまでに学んだ設計原則とパターンを実際のプロジェクトに適用する方法を見ていきましょう。

ECサイトの支払い処理システムの設計

複数の決済方法に対応する柔軟な支払い処理システムを設計します。

1. ドメインモデルとインターフェース

public interface IPaymentGateway
{
    Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request);
    Task<PaymentStatus> CheckPaymentStatusAsync(string transactionId);
    Task<RefundResult> ProcessRefundAsync(RefundRequest request);
}

public interface IPaymentValidator
{
    Task<ValidationResult> ValidatePaymentRequestAsync(PaymentRequest request);
    Task<ValidationResult> ValidateRefundRequestAsync(RefundRequest request);
}

public interface IPaymentNotificationHandler
{
    Task HandlePaymentCompletedAsync(PaymentCompletedEvent @event);
    Task HandlePaymentFailedAsync(PaymentFailedEvent @event);
    Task HandleRefundCompletedAsync(RefundCompletedEvent @event);
}

// イベントモデル
public record PaymentCompletedEvent(
    string TransactionId,
    decimal Amount,
    string Currency,
    DateTime CompletedAt);

// 支払いリクエストモデル
public record PaymentRequest(
    string OrderId,
    decimal Amount,
    string Currency,
    PaymentMethod Method,
    Dictionary<string, string> PaymentDetails);

// バリデーション結果
public record ValidationResult(
    bool IsValid,
    IReadOnlyList<string> Errors)
{
    public static ValidationResult Success() => new(true, Array.Empty<string>());
    public static ValidationResult Failure(IEnumerable<string> errors) 
        => new(false, errors.ToList());
}

2. Stripeペイメントゲートウェイの実装

public class StripePaymentGateway : IPaymentGateway
{
    private readonly IStripeClient _stripeClient;
    private readonly ILogger<StripePaymentGateway> _logger;
    private readonly IPaymentNotificationHandler _notificationHandler;

    public StripePaymentGateway(
        IStripeClient stripeClient,
        ILogger<StripePaymentGateway> logger,
        IPaymentNotificationHandler notificationHandler)
    {
        _stripeClient = stripeClient;
        _logger = logger;
        _notificationHandler = notificationHandler;
    }

    public async Task<PaymentResult> ProcessPaymentAsync(PaymentRequest request)
    {
        try
        {
            var paymentIntentCreateOptions = new PaymentIntentCreateOptions
            {
                Amount = (long)(request.Amount * 100), // Convert to cents
                Currency = request.Currency.ToLower(),
                PaymentMethod = request.PaymentDetails["paymentMethodId"],
                Confirm = true,
                ReturnUrl = request.PaymentDetails.GetValueOrDefault("returnUrl")
            };

            var paymentIntent = await _stripeClient.PaymentIntents
                .CreateAsync(paymentIntentCreateOptions);

            if (paymentIntent.Status == "succeeded")
            {
                await _notificationHandler.HandlePaymentCompletedAsync(
                    new PaymentCompletedEvent(
                        paymentIntent.Id,
                        request.Amount,
                        request.Currency,
                        DateTime.UtcNow));

                return PaymentResult.Success(
                    paymentIntent.Id,
                    "Payment processed successfully");
            }

            return PaymentResult.RequiresAction(
                paymentIntent.Id,
                paymentIntent.NextAction?.RedirectToUrl?.Url);
        }
        catch (StripeException ex)
        {
            _logger.LogError(ex, "Stripe payment processing failed");
            return PaymentResult.Failure(ex.Message);
        }
    }

    // その他のメソッド実装
}

ログ機能のインターフェース設計

異なる出力先に対応する柔軟なログシステムを設計します。

public interface ILogEntry
{
    string Message { get; }
    LogLevel Level { get; }
    DateTime Timestamp { get; }
    IDictionary<string, object> Properties { get; }
    Exception Exception { get; }
}

public interface ILogFormatter
{
    string FormatLog(ILogEntry entry);
}

public interface ILogDestination
{
    Task WriteAsync(ILogEntry entry);
    Task FlushAsync();
}

public interface ILogRouter
{
    Task RouteLogAsync(ILogEntry entry);
    void AddDestination(ILogDestination destination, LogLevel minimumLevel);
}

// 構造化ログエントリの実装
public class StructuredLogEntry : ILogEntry
{
    public string Message { get; }
    public LogLevel Level { get; }
    public DateTime Timestamp { get; }
    public IDictionary<string, object> Properties { get; }
    public Exception Exception { get; }

    public StructuredLogEntry(
        string message,
        LogLevel level,
        Exception exception = null,
        IDictionary<string, object> properties = null)
    {
        Message = message;
        Level = level;
        Timestamp = DateTime.UtcNow;
        Exception = exception;
        Properties = properties ?? new Dictionary<string, object>();
    }
}

// JSONフォーマッタの実装
public class JsonLogFormatter : ILogFormatter
{
    public string FormatLog(ILogEntry entry)
    {
        var logObject = new
        {
            Message = entry.Message,
            Level = entry.Level.ToString(),
            Timestamp = entry.Timestamp.ToString("O"),
            Properties = entry.Properties,
            Exception = entry.Exception?.ToString()
        };

        return JsonSerializer.Serialize(logObject, new JsonSerializerOptions
        {
            WriteIndented = true
        });
    }
}

// ファイル出力先の実装
public class FileLogDestination : ILogDestination, IDisposable
{
    private readonly string _filePath;
    private readonly ILogFormatter _formatter;
    private readonly StreamWriter _writer;
    private readonly SemaphoreSlim _lock = new(1, 1);

    public FileLogDestination(string filePath, ILogFormatter formatter)
    {
        _filePath = filePath;
        _formatter = formatter;
        _writer = new StreamWriter(filePath, true);
    }

    public async Task WriteAsync(ILogEntry entry)
    {
        await _lock.WaitAsync();
        try
        {
            var formattedLog = _formatter.FormatLog(entry);
            await _writer.WriteLineAsync(formattedLog);
        }
        finally
        {
            _lock.Release();
        }
    }

    public async Task FlushAsync()
    {
        await _writer.FlushAsync();
    }

    public void Dispose()
    {
        _writer.Dispose();
        _lock.Dispose();
    }
}

マルチプラットフォーム対応の実装例

クロスプラットフォームで動作するアプリケーションの共通インターフェースを設計します。

1. プラットフォーム抽象化レイヤー

public interface IPlatformService
{
    string GetPlatformName();
    Task<bool> RequestPermissionAsync(Permission permission);
    Task<string> GetStoragePathAsync();
    Task<bool> IsNetworkAvailableAsync();
}

public interface IFileSystemService
{
    Task<Stream> OpenFileAsync(string path, FileMode mode);
    Task<bool> FileExistsAsync(string path);
    Task<IEnumerable<string>> ListFilesAsync(string directory);
    Task<string> GetTempPathAsync();
}

public enum Permission
{
    Camera,
    Location,
    Storage,
    Notifications
}

// Windows実装
public class WindowsPlatformService : IPlatformService
{
    private readonly ILogger<WindowsPlatformService> _logger;

    public WindowsPlatformService(ILogger<WindowsPlatformService> logger)
    {
        _logger = logger;
    }

    public string GetPlatformName() => "Windows";

    public async Task<bool> RequestPermissionAsync(Permission permission)
    {
        try
        {
            // Windowsでの権限リクエスト処理
            switch (permission)
            {
                case Permission.Camera:
                    return await RequestCameraPermissionAsync();
                case Permission.Location:
                    return await RequestLocationPermissionAsync();
                default:
                    return true; // Windows では一部の権限は不要
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error requesting permission: {Permission}", permission);
            return false;
        }
    }

    private async Task<bool> RequestCameraPermissionAsync()
    {
        // Windows Media Capture APIを使用した実装
        await Task.CompletedTask;
        return true;
    }

    private async Task<bool> RequestLocationPermissionAsync()
    {
        // Windows Location APIを使用した実装
        await Task.CompletedTask;
        return true;
    }

    // その他のメソッド実装
}

// クロスプラットフォームファイルシステム実装
public class CrossPlatformFileSystem : IFileSystemService
{
    private readonly IPlatformService _platformService;
    private readonly ILogger<CrossPlatformFileSystem> _logger;

    public CrossPlatformFileSystem(
        IPlatformService platformService,
        ILogger<CrossPlatformFileSystem> logger)
    {
        _platformService = platformService;
        _logger = logger;
    }

    public async Task<Stream> OpenFileAsync(string path, FileMode mode)
    {
        try
        {
            var fullPath = await GetFullPathAsync(path);
            return new FileStream(fullPath, mode);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error opening file: {Path}", path);
            throw;
        }
    }

    private async Task<string> GetFullPathAsync(string path)
    {
        var basePath = await _platformService.GetStoragePathAsync();
        return Path.Combine(basePath, path);
    }

    // その他のメソッド実装
}

これらの実装例は、以下の設計原則と実践を示しています。

  1. インターフェース設計のポイント
    • 単一責任の原則の遵守
    • プラットフォーム固有の実装の分離
    • 適切な抽象化レベルの維持
  2. エラーハンドリングとロギング
    • 一貫したログ記録
    • 適切な例外処理
    • エラー情報の伝播
  3. 非同期処理のベストプラクティス
    • Async/Awaitの一貫した使用
    • キャンセレーション対応
    • リソースの適切な解放

これらの実践的な例を通じて、インターフェースを活用した柔軟で拡張性の高いシステム設計の方法を学ぶことができます。

インターフェースのまとめ

インターフェースは、適切に使用することで柔軟で保守性の高いシステムを実現できる強力なツールです。
本記事で解説した設計原則とパターンを実践することで、より堅牢なアプリケーション開発が可能になります。特に、単一責任の原則、依存性の適切な管理、そして実装の詳細の抽象化を意識することが、良質なインターフェース設計の鍵となります。

この記事の主なポイント
  • 設計原則の重視
    • 単一責任の原則に基づくインターフェース設計
    • インターフェース分離の原則による適切な粒度の維持
    • 依存性逆転の原則を活用した疎結合の実現
  • 実装のベストプラクティス
    • 非同期処理の一貫した使用
    • 適切なエラーハンドリングとロギング
    • パフォーマンスを考慮したキャッシュ戦略
  • 実用的な活用方法
    • 依存性注入を活用したテスト容易性の向上
    • デザインパターンを用いた柔軟な実装
    • プラグインアーキテクチャによる拡張性の確保
  • 保守性への配慮
    • バージョン管理を考慮したインターフェースの進化
    • 過剰な抽象化を避けるための指針
    • クリーンアーキテクチャの実践