C#クリーンアーキテクチャ完全ガイド:面接対策と実装パターン 2026年版
C#におけるクリーンアーキテクチャの設計原則、SOLID原則の実践的な適用方法、そして技術面接で頻出する質問と回答を徹底解説します。

C#開発において、クリーンアーキテクチャは保守性と拡張性の高いアプリケーションを構築するための基盤となります。本記事では、クリーンコードの原則からSOLID原則の実装、そして面接で問われる重要なポイントまでを体系的に解説します。
クリーンアーキテクチャを習得することで、テスト可能で変更に強いコードベースを構築できるようになります。これは技術面接においても高く評価されるスキルです。
クリーンアーキテクチャとは何か
クリーンアーキテクチャは、Robert C. Martin(Uncle Bob)によって提唱された設計思想です。この設計思想の核心は、ビジネスロジックを外部の詳細(データベース、UI、フレームワーク)から分離することにあります。
C#におけるクリーンアーキテクチャは、以下の層で構成されます:
- Domain層(エンティティ): ビジネスルールとエンティティ
- Application層(ユースケース): アプリケーション固有のビジネスルール
- Infrastructure層: データベースアクセス、外部サービス
- Presentation層: UIとAPI
依存関係の方向は常に内側(Domain層)に向かう必要があります。これにより、ビジネスロジックがフレームワークや技術的詳細に依存しなくなります。
SOLID原則の実践的適用
SOLID原則は、クリーンコードを書くための5つの設計原則です。C#での実装例を見ていきます。
単一責任の原則(SRP)
// ❌ 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)
// ❌ 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)
// ❌ 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)
// ❌ 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)
// ❌ 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#プロジェクトでは、以下のような構造が推奨されます:
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ライブラリを使った実装例を示します:
// 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を使用した入力検証の実装:
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、技術テストで練習しましょう。
ベストプラクティスとアンチパターン
クリーンアーキテクチャを実装する際に避けるべきアンチパターンがあります:
避けるべきアンチパターン
- 貧血ドメインモデル: エンティティがデータの入れ物だけになり、ロジックがサービス層に漏れ出す
- 過度な抽象化: すべてにインターフェースを作成し、不必要な複雑さを生む
- 層の境界違反: プレゼンテーション層からドメインエンティティを直接返す
- 循環参照: 層間で双方向の依存関係を作る
推奨されるベストプラクティス
- リッチドメインモデル: ビジネスロジックをエンティティ内にカプセル化
- DTOの適切な使用: 層間のデータ転送にはDTOを使用
- 例外の適切な使用: ドメイン固有の例外を定義し、適切にハンドリング
- テスト駆動開発: 各層に対して適切なテストを作成
まとめ
C#でのクリーンアーキテクチャの実装は、長期的な保守性とテスト容易性を確保するための重要な投資です。SOLID原則を理解し、適切な層の分離を行うことで、変更に強く、理解しやすいコードベースを構築できます。技術面接では、これらの概念を実際のコードで説明できることが重要です。理論だけでなく、実践的な実装経験を積むことで、クリーンアーキテクチャの真価を発揮できるようになります。
.NET のバグを見つけられますか
実際のコード、隠れたバグ、1日1回。アカウントなしで試せます。

執筆
Anthony Fillion-MailletSharpSkill 創業者
10 年以上フルスタック開発に携わっています。SharpSkill を運営し、ここで公開される内容に責任を負っています。
2026年8月24日 更新
タグ
共有
関連記事

C#/.NET面接質問集:2026年完全ガイド
C#と.NETの技術面接で頻出の17問を徹底解説。LINQ、async/await、依存性注入、Entity Framework Coreからアーキテクチャパターンまで、コード例付きで実践的に学べます。

Entity Framework Core:2026年のパフォーマンス最適化とベストプラクティス
EF Core 10のパフォーマンス最適化を解説。AsNoTracking、コンパイル済みクエリ、バッチ操作、スプリットクエリ、LeftJoin演算子など、.NET 10アプリケーション向けの実践的なC#コード例を紹介します。

.NETによるClean Architecture実践ガイド
C#を用いた.NETでClean Architectureを習得します。SOLID原則、レイヤー分離、保守性の高いアプリケーションを構築するための実装パターンを学びます。