メインコンテンツまでスキップ

Slice Architecture

概要

Slice Architecture(スライスアーキテクチャ)は、アプリケーションを機能単位の垂直スライスで分割する設計パターンです。従来の水平レイヤー型アーキテクチャ(Controller → Service → Repository)とは異なり、各機能を独立した垂直の「スライス」として実装します。

従来のレイヤードアーキテクチャとの違い

レイヤードアーキテクチャ(水平分割)

Controllers/
├── UserController.cs
├── OrderController.cs
└── ProductController.cs

Services/
├── UserService.cs
├── OrderService.cs
└── ProductService.cs

Repositories/
├── UserRepository.cs
├── OrderRepository.cs
└── ProductRepository.cs

Slice Architecture(垂直分割)

Features/
├── Users/
│ ├── CreateUser.cs
│ ├── GetUser.cs
│ └── UpdateUser.cs
├── Orders/
│ ├── CreateOrder.cs
│ ├── GetOrderById.cs
│ └── ListOrders.cs
└── Products/
├── CreateProduct.cs
└── SearchProducts.cs

主要な特徴

1. 機能ごとの独立性

各スライスは特定の機能(ユースケース)を表現し、その機能に必要なすべてのコードを含みます。

2. 高凝集・低結合

関連するコードが1箇所にまとまり、他の機能への依存を最小限に抑えます。

3. 変更の影響範囲の局所化

新機能の追加や既存機能の変更が他のスライスに影響しにくい構造です。

実装例

MediatRを使用した実装

// Features/Users/CreateUser.cs
namespace MyApp.Features.Users;

public class CreateUser
{
// コマンド定義
public record Command(string Name, string Email) : IRequest<Result>;

// バリデーション
public class Validator : AbstractValidator<Command>
{
public Validator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(100);
RuleFor(x => x.Email).NotEmpty().EmailAddress();
}
}

// ハンドラー(ビジネスロジック)
public class Handler : IRequestHandler<Command, Result>
{
private readonly AppDbContext _context;
private readonly IPasswordHasher _hasher;

public Handler(AppDbContext context, IPasswordHasher hasher)
{
_context = context;
_hasher = hasher;
}

public async Task<Result> Handle(Command request, CancellationToken ct)
{
// 重複チェック
if (await _context.Users.AnyAsync(u => u.Email == request.Email, ct))
{
return Result.Failure("Email already exists");
}

// ユーザー作成
var user = new User
{
Name = request.Name,
Email = request.Email,
PasswordHash = _hasher.Hash("defaultPassword")
};

_context.Users.Add(user);
await _context.SaveChangesAsync(ct);

return Result.Success(user.Id);
}
}
}

エンドポイント定義(Minimal API)

// Features/Users/CreateUserEndpoint.cs
public class CreateUserEndpoint : IEndpoint
{
public void MapEndpoint(IEndpointRouteBuilder app)
{
app.MapPost("/api/users", async (
CreateUser.Command command,
IMediator mediator,
CancellationToken ct) =>
{
var result = await mediator.Send(command, ct);
return result.IsSuccess
? Results.Created($"/api/users/{result.Value}", result.Value)
: Results.BadRequest(result.Error);
})
.WithTags("Users")
.WithName("CreateUser");
}
}

MediatRとの組み合わせ

MediatRは、Slice Architectureとの相性が非常に良いライブラリです。

主要なパターン

1. Command/Query パターン

// コマンド(データ変更)
public record CreateOrderCommand(int UserId, List<OrderItem> Items)
: IRequest<OrderDto>;

// クエリ(データ取得)
public record GetOrderByIdQuery(int OrderId)
: IRequest<OrderDto?>;

2. Pipelineビヘイビア

横断的な処理(バリデーション、ロギング、トランザクション)を共通化できます。

// バリデーションパイプライン
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)
{
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();
}
}

3. 登録

// Program.cs
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssembly(typeof(Program).Assembly);
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
});

builder.Services.AddValidatorsFromAssembly(typeof(Program).Assembly);

フォルダ構成例

src/
├── MyApp.Api/
│ ├── Program.cs
│ └── Features/
│ ├── Users/
│ │ ├── CreateUser.cs
│ │ ├── GetUser.cs
│ │ ├── UpdateUser.cs
│ │ └── DeleteUser.cs
│ ├── Orders/
│ │ ├── CreateOrder.cs
│ │ ├── GetOrderById.cs
│ │ ├── ListOrders.cs
│ │ └── CancelOrder.cs
│ └── Common/
│ ├── Behaviors/
│ │ ├── ValidationBehavior.cs
│ │ └── LoggingBehavior.cs
│ └── Models/
│ └── Result.cs
├── MyApp.Domain/
│ └── Entities/
│ ├── User.cs
│ └── Order.cs
└── MyApp.Infrastructure/
└── Data/
└── AppDbContext.cs

メリット

1. 開発の並列化

各スライスが独立しているため、複数の開発者が同時に異なる機能を実装できます。

2. 理解しやすさ

機能に関連するすべてのコードが1つのファイルまたはフォルダにまとまっているため、コードの追跡が容易です。

3. テストの容易性

各スライスを独立してテストでき、モックの必要性が減ります。

public class CreateUserTests
{
[Fact]
public async Task Handle_ValidCommand_CreatesUser()
{
// Arrange
var context = CreateInMemoryContext();
var hasher = new MockPasswordHasher();
var handler = new CreateUser.Handler(context, hasher);
var command = new CreateUser.Command("John Doe", "john@example.com");

// Act
var result = await handler.Handle(command, CancellationToken.None);

// Assert
Assert.True(result.IsSuccess);
Assert.Equal(1, await context.Users.CountAsync());
}
}

4. 変更の影響範囲の明確化

特定の機能を変更する際、どのファイルを修正すべきかが明確です。

デメリットと注意点

1. コードの重複

各スライスが独立しているため、似たようなコードが複数のスライスに現れることがあります。

対策: 本当に共通化すべきロジックはCommonフォルダに抽出します。

2. 学習コスト

従来のレイヤードアーキテクチャに慣れた開発者には、新しい考え方の習得が必要です。

3. 小規模プロジェクトでは過剰

シンプルなCRUDアプリケーションでは、オーバーエンジニアリングになる可能性があります。

いつ使うべきか

適している場合

  • 複雑なビジネスロジックを持つアプリケーション
  • 複数の開発者が並行して作業する大規模プロジェクト
  • 機能ごとに異なる要件やワークフローがあるシステム
  • マイクロサービスへの移行を見据えた設計

適していない場合

  • シンプルなCRUDアプリケーション
  • 小規模で固定的な要件のプロジェクト
  • チームが従来の方法に慣れており、変更のメリットが少ない場合

ツールとライブラリ

推奨ライブラリ

  • MediatR: Command/Queryパターンの実装
  • FluentValidation: バリデーションロジックの定義
  • Carter: Minimal APIのエンドポイント管理
  • FastEndpoints: より高速なエンドポイント実装

プロジェクトテンプレート

# Clean Architectureベースのテンプレート
dotnet new install Clean.Architecture.Solution.Template
dotnet new ca-sln -o MyProject

# Vertical Slice Architecture テンプレート
dotnet new install VerticalSliceArchitecture.Template
dotnet new vsa -o MyProject

まとめ

Slice Architectureは、従来のレイヤードアーキテクチャの欠点を補う強力なパターンです。特に複雑なドメインロジックを持つアプリケーションや、スケーラブルな開発体制が求められるプロジェクトにおいて、その真価を発揮します。

MediatRやFluentValidationといった.NETエコシステムのライブラリとの組み合わせにより、保守性と拡張性に優れたアプリケーションを構築できます。

参考リンク