C#クリーンアーキテクチャ完全ガイド:面接対策と実装パターン 2026年版

C#におけるクリーンアーキテクチャの設計原則、SOLID原則の実践的な適用方法、そして技術面接で頻出する質問と回答を徹底解説します。

C#クリーンアーキテクチャの設計図

C#開発において、クリーンアーキテクチャは保守性と拡張性の高いアプリケーションを構築するための基盤となります。本記事では、クリーンコードの原則からSOLID原則の実装、そして面接で問われる重要なポイントまでを体系的に解説します。

クリーンアーキテクチャを習得することで、テスト可能で変更に強いコードベースを構築できるようになります。これは技術面接においても高く評価されるスキルです。

クリーンアーキテクチャとは何か

クリーンアーキテクチャは、Robert C. Martin(Uncle Bob)によって提唱された設計思想です。この設計思想の核心は、ビジネスロジックを外部の詳細(データベース、UI、フレームワーク)から分離することにあります。

C#におけるクリーンアーキテクチャは、以下の層で構成されます:

  • Domain層(エンティティ): ビジネスルールとエンティティ
  • Application層(ユースケース): アプリケーション固有のビジネスルール
  • Infrastructure層: データベースアクセス、外部サービス
  • Presentation層: UIとAPI

依存関係の方向は常に内側(Domain層)に向かう必要があります。これにより、ビジネスロジックがフレームワークや技術的詳細に依存しなくなります。

SOLID原則の実践的適用

SOLID原則は、クリーンコードを書くための5つの設計原則です。C#での実装例を見ていきます。

単一責任の原則(SRP)

csharp
// ❌ Bad: 複数の責任を持つクラス
public class UserService
{
    public void CreateUser(User user) { /* ... */ }
    public void SendEmail(string email, string message) { /* ... */ }
    public void LogActivity(string activity) { /* ... */ }
}

// ✅ Good: 単一責任に分割
public class UserService
{
    private readonly IEmailService _emailService;
    private readonly ILogger<UserService> _logger;

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

    public async Task CreateUserAsync(User user)
    {
        // ユーザー作成のみに集中
        await _userRepository.AddAsync(user);
        _logger.LogInformation("User created: {UserId}", user.Id);
    }
}

オープン・クローズドの原則(OCP)

csharp
// ❌ Bad: 新しい支払い方法追加時にクラス修正が必要
public class PaymentProcessor
{
    public void ProcessPayment(string paymentType, decimal amount)
    {
        if (paymentType == "CreditCard")
        {
            // クレジットカード処理
        }
        else if (paymentType == "PayPal")
        {
            // PayPal処理
        }
    }
}

// ✅ Good: 拡張に対して開かれ、修正に対して閉じている
public interface IPaymentStrategy
{
    Task ProcessAsync(decimal amount);
}

public class CreditCardPayment : IPaymentStrategy
{
    public async Task ProcessAsync(decimal amount)
    {
        // クレジットカード固有の処理
    }
}

public class PayPalPayment : IPaymentStrategy
{
    public async Task ProcessAsync(decimal amount)
    {
        // PayPal固有の処理
    }
}

public class PaymentProcessor
{
    private readonly IPaymentStrategy _paymentStrategy;

    public PaymentProcessor(IPaymentStrategy paymentStrategy)
    {
        _paymentStrategy = paymentStrategy;
    }

    public async Task ProcessPaymentAsync(decimal amount)
    {
        await _paymentStrategy.ProcessAsync(amount);
    }
}

リスコフの置換原則(LSP)

csharp
// ❌ Bad: LSP違反 - 派生クラスが基底クラスの契約を破る
public class Bird
{
    public virtual void Fly() { /* ... */ }
}

public class Penguin : Bird
{
    public override void Fly()
    {
        throw new NotSupportedException("Penguins cannot fly");
    }
}

// ✅ Good: インターフェースで適切に分離
public interface IFlyable
{
    void Fly();
}

public interface ISwimmable
{
    void Swim();
}

public class Sparrow : IFlyable
{
    public void Fly() { /* ... */ }
}

public class Penguin : ISwimmable
{
    public void Swim() { /* ... */ }
}

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

csharp
// ❌ Bad: 巨大なインターフェース
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
}

// ✅ Good: 役割ごとに分離されたインターフェース
public interface IWorkable
{
    void Work();
}

public interface IFeedable
{
    void Eat();
}

public interface ISleepable
{
    void Sleep();
}

public class HumanWorker : IWorkable, IFeedable, ISleepable
{
    public void Work() { /* ... */ }
    public void Eat() { /* ... */ }
    public void Sleep() { /* ... */ }
}

public class RobotWorker : IWorkable
{
    public void Work() { /* ... */ }
}

依存性逆転の原則(DIP)

csharp
// ❌ Bad: 高レベルモジュールが低レベルモジュールに依存
public class OrderService
{
    private readonly SqlOrderRepository _repository = new SqlOrderRepository();

    public void CreateOrder(Order order)
    {
        _repository.Save(order);
    }
}

// ✅ Good: 両方が抽象に依存
public interface IOrderRepository
{
    Task SaveAsync(Order order);
    Task<Order?> GetByIdAsync(Guid id);
}

public class OrderService
{
    private readonly IOrderRepository _repository;

    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }

    public async Task CreateOrderAsync(Order order)
    {
        await _repository.SaveAsync(order);
    }
}

クリーンアーキテクチャのプロジェクト構造

実際のC#プロジェクトでは、以下のような構造が推奨されます:

text
Solution/
├── src/
│   ├── Domain/
│   │   ├── Entities/
│   │   ├── ValueObjects/
│   │   ├── Interfaces/
│   │   └── Exceptions/
│   ├── Application/
│   │   ├── Common/
│   │   │   ├── Interfaces/
│   │   │   └── Behaviors/
│   │   ├── Features/
│   │   │   └── Orders/
│   │   │       ├── Commands/
│   │   │       └── Queries/
│   │   └── DTOs/
│   ├── Infrastructure/
│   │   ├── Persistence/
│   │   ├── Services/
│   │   └── DependencyInjection.cs
│   └── WebApi/
│       ├── Controllers/
│       ├── Filters/
│       └── Program.cs
└── tests/
    ├── Domain.Tests/
    ├── Application.Tests/
    └── Integration.Tests/

CQRSとMediatRパターン

クリーンアーキテクチャでは、CQRS(コマンドクエリ責務分離)パターンがよく使用されます。MediatRライブラリを使った実装例を示します:

csharp
// Command定義
public record CreateOrderCommand(string CustomerId, List<OrderItem> Items) 
    : IRequest<Result<Guid>>;

// Commandハンドラー
public class CreateOrderCommandHandler 
    : IRequestHandler<CreateOrderCommand, Result<Guid>>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IUnitOfWork _unitOfWork;

    public CreateOrderCommandHandler(
        IOrderRepository orderRepository,
        IUnitOfWork unitOfWork)
    {
        _orderRepository = orderRepository;
        _unitOfWork = unitOfWork;
    }

    public async Task<Result<Guid>> Handle(
        CreateOrderCommand request, 
        CancellationToken cancellationToken)
    {
        var order = Order.Create(request.CustomerId, request.Items);
        
        await _orderRepository.AddAsync(order, cancellationToken);
        await _unitOfWork.SaveChangesAsync(cancellationToken);
        
        return Result.Success(order.Id);
    }
}

// Query定義
public record GetOrderByIdQuery(Guid OrderId) : IRequest<OrderDto?>;

// Queryハンドラー
public class GetOrderByIdQueryHandler 
    : IRequestHandler<GetOrderByIdQuery, OrderDto?>
{
    private readonly IOrderRepository _orderRepository;
    private readonly IMapper _mapper;

    public GetOrderByIdQueryHandler(
        IOrderRepository orderRepository,
        IMapper mapper)
    {
        _orderRepository = orderRepository;
        _mapper = mapper;
    }

    public async Task<OrderDto?> Handle(
        GetOrderByIdQuery request, 
        CancellationToken cancellationToken)
    {
        var order = await _orderRepository
            .GetByIdAsync(request.OrderId, cancellationToken);
        
        return order is null ? null : _mapper.Map<OrderDto>(order);
    }
}

バリデーションとエラーハンドリング

FluentValidationを使用した入力検証の実装:

csharp
public class CreateOrderCommandValidator 
    : AbstractValidator<CreateOrderCommand>
{
    public CreateOrderCommandValidator()
    {
        RuleFor(x => x.CustomerId)
            .NotEmpty()
            .WithMessage("顧客IDは必須です");

        RuleFor(x => x.Items)
            .NotEmpty()
            .WithMessage("注文には少なくとも1つの商品が必要です");

        RuleForEach(x => x.Items)
            .ChildRules(item =>
            {
                item.RuleFor(x => x.Quantity)
                    .GreaterThan(0)
                    .WithMessage("数量は1以上である必要があります");
            });
    }
}

// ValidationBehavior(MediatRパイプライン)
public class ValidationBehavior<TRequest, TResponse> 
    : IPipelineBehavior<TRequest, TResponse>
    where TRequest : IRequest<TResponse>
{
    private readonly IEnumerable<IValidator<TRequest>> _validators;

    public ValidationBehavior(IEnumerable<IValidator<TRequest>> validators)
    {
        _validators = validators;
    }

    public async Task<TResponse> Handle(
        TRequest request,
        RequestHandlerDelegate<TResponse> next,
        CancellationToken cancellationToken)
    {
        if (!_validators.Any())
            return await next();

        var context = new ValidationContext<TRequest>(request);
        var failures = _validators
            .Select(v => v.Validate(context))
            .SelectMany(result => result.Errors)
            .Where(f => f != null)
            .ToList();

        if (failures.Any())
            throw new ValidationException(failures);

        return await next();
    }
}

技術面接での頻出質問

クリーンアーキテクチャに関する面接では、以下のような質問が頻繁に出題されます:

Q: クリーンアーキテクチャとオニオンアーキテクチャの違いは何ですか?

両者は非常に似ていますが、クリーンアーキテクチャはより汎用的な概念です。オニオンアーキテクチャはドメインモデルを中心に据え、依存性の方向を厳密に定義します。クリーンアーキテクチャはこれをさらに発展させ、ユースケース層を明確に分離しています。

Q: なぜDomain層はフレームワークに依存してはいけないのですか?

Domain層がフレームワークに依存すると、フレームワークの変更がビジネスロジックに影響を与えます。また、単体テストが困難になり、ビジネスルールの再利用性も低下します。

Q: Repository パターンとUnit of Workパターンの関係を説明してください。

Repositoryパターンはデータアクセスの抽象化を提供し、Unit of Workパターンはトランザクション境界を管理します。両者を組み合わせることで、複数のリポジトリ操作を単一のトランザクションで実行できます。

.NETの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

ベストプラクティスとアンチパターン

クリーンアーキテクチャを実装する際に避けるべきアンチパターンがあります:

避けるべきアンチパターン

  1. 貧血ドメインモデル: エンティティがデータの入れ物だけになり、ロジックがサービス層に漏れ出す
  2. 過度な抽象化: すべてにインターフェースを作成し、不必要な複雑さを生む
  3. 層の境界違反: プレゼンテーション層からドメインエンティティを直接返す
  4. 循環参照: 層間で双方向の依存関係を作る

推奨されるベストプラクティス

  1. リッチドメインモデル: ビジネスロジックをエンティティ内にカプセル化
  2. DTOの適切な使用: 層間のデータ転送にはDTOを使用
  3. 例外の適切な使用: ドメイン固有の例外を定義し、適切にハンドリング
  4. テスト駆動開発: 各層に対して適切なテストを作成

まとめ

C#でのクリーンアーキテクチャの実装は、長期的な保守性とテスト容易性を確保するための重要な投資です。SOLID原則を理解し、適切な層の分離を行うことで、変更に強く、理解しやすいコードベースを構築できます。技術面接では、これらの概念を実際のコードで説明できることが重要です。理論だけでなく、実践的な実装経験を積むことで、クリーンアーキテクチャの真価を発揮できるようになります。

今日のチャレンジ

.NET のバグを見つけられますか

実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

Anthony Fillion-Maillet

執筆

Anthony Fillion-Maillet

SharpSkill 創業者

10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。

2026年8月24日 更新

タグ

#csharp
#clean-architecture
#solid
#design-patterns
#interview

共有

関連記事