GitHubリポジトリ: https://github.com/cardamon04/GoF
- (Decoratorパターンどんなやったっけって本を開いたものの、いまいちピンとこなかったので復習がてら)。
- 本記事では、予約作成ユースケースを題材にして、Decorator(デコレータ:責務を外側に追加する設計)をC#で実装した例を紹介します。
- 目的は「本体ロジックを太らせずに、周辺の責務を後付けできる」点を、実務の流れに近い形で理解することです。
1. Decoratorパターンとは
Decoratorパターンは、中身(本体)の処理を変えずに、外側で機能を追加する設計手法です。継承で機能を盛るのではなく、合成(オブジェクトを包む)で「前後処理」を差し込めるのがポイントです。
今回の例では、予約作成の処理に以下の周辺責務を追加します。
- 入力チェック
- 認可チェック
- 監査ログ
- 取引(トランザクション)
習うより慣れろ、サンプルコードを理解するとこの説明が腑に落ちると思います。
2. サンプルの構成
主要クラス
-
ICreateBookingUseCase:ユースケースの口 -
CreateBookingUseCaseImpl:本体ロジック(予約作成) -
CreateBookingUseCaseDecorator:Decoratorの土台 -
ValidationDecorator/AuthorizationDecorator/AuditDecorator/TransactionDecorator
クラス図
以下は本サンプルの最小構成のクラス図です。
3. コアとなるインターフェースと実装
// ICreateBookingUseCase.cs
public interface ICreateBookingUseCase
{
Result<BookingId> Execute(CreateBookingCommand command);
}
// CreateBookingUseCaseImpl.cs
public sealed class CreateBookingUseCaseImpl : ICreateBookingUseCase
{
private readonly IBookingRepository _repo;
public CreateBookingUseCaseImpl(IBookingRepository repo)
{
_repo = repo;
}
public Result<BookingId> Execute(CreateBookingCommand command)
{
return _repo.SaveNew(command);
}
}
本体は「予約を作る」だけに集中させます。入力チェックやログなどは後述のDecoratorで追加します。
4. Decoratorの土台
// CreateBookingUseCaseDecorator.cs
public abstract class CreateBookingUseCaseDecorator : ICreateBookingUseCase
{
// 内側のユースケース本体。Decoratorは前後に処理を追加する。
private readonly ICreateBookingUseCase _inner;
protected CreateBookingUseCaseDecorator(ICreateBookingUseCase inner)
{
_inner = inner;
}
protected Result<BookingId> Next(CreateBookingCommand command)
{
// 包んでいる本体へ処理を渡す。
return _inner.Execute(command);
}
public abstract Result<BookingId> Execute(CreateBookingCommand command);
}
Next で内側のユースケースに処理を渡し、外側で前後処理を差し込みます。
5. 追加責務の例
入力チェック
// ValidationDecorator.cs
public sealed class ValidationDecorator : CreateBookingUseCaseDecorator
{
public ValidationDecorator(ICreateBookingUseCase inner) : base(inner)
{
}
public override Result<BookingId> Execute(CreateBookingCommand command)
{
// 内側に渡す前に入力を検証する。
if (string.IsNullOrWhiteSpace(command.StudioId))
{
return Result<BookingId>.Fail(new ValidationError("StudioId is required."));
}
if (string.IsNullOrWhiteSpace(command.TimeSlot))
{
return Result<BookingId>.Fail(new ValidationError("TimeSlot is required."));
}
if (string.IsNullOrWhiteSpace(command.RequestedBy))
{
return Result<BookingId>.Fail(new ValidationError("RequestedBy is required."));
}
return Next(command);
}
}
認可チェック
// AuthorizationDecorator.cs
public sealed class AuthorizationDecorator : CreateBookingUseCaseDecorator
{
private readonly HashSet<string> _allowedRequesterPrefixes;
public AuthorizationDecorator(
ICreateBookingUseCase inner,
IEnumerable<string> allowedRequesterPrefixes
) : base(inner)
{
_allowedRequesterPrefixes = new HashSet<string>(allowedRequesterPrefixes);
}
public override Result<BookingId> Execute(CreateBookingCommand command)
{
// 内側に渡す前に認可チェックを行う。
var ok = false;
foreach (var prefix in _allowedRequesterPrefixes)
{
if (command.RequestedBy.StartsWith(prefix))
{
ok = true;
break;
}
}
if (!ok)
{
return Result<BookingId>.Fail(
new AuthorizationError("Requester is not allowed to create bookings.")
);
}
return Next(command);
}
}
監査ログ
// AuditDecorator.cs
public sealed class AuditDecorator : CreateBookingUseCaseDecorator
{
private readonly IAuditLogger _logger;
public AuditDecorator(ICreateBookingUseCase inner, IAuditLogger logger) : base(inner)
{
_logger = logger;
}
public override Result<BookingId> Execute(CreateBookingCommand command)
{
// 前後に監査ログを差し込む。
_logger.Info(
$"CreateBooking start requester={command.RequestedBy} studioId={command.StudioId} date={command.UsageDate} slot={command.TimeSlot}"
);
var result = Next(command);
if (result.IsSuccess && result.Value is not null)
{
_logger.Info($"CreateBooking success bookingId={result.Value.Value}");
}
else if (result.Error is not null)
{
_logger.Warn($"CreateBooking failed reason={result.Error.Message}");
}
return result;
}
}
取引(トランザクション)
// TransactionDecorator.cs
public sealed class TransactionDecorator : CreateBookingUseCaseDecorator
{
private readonly ITransactionManager _transactionManager;
public TransactionDecorator(
ICreateBookingUseCase inner,
ITransactionManager transactionManager
) : base(inner)
{
_transactionManager = transactionManager;
}
public override Result<BookingId> Execute(CreateBookingCommand command)
{
// 内側の処理をトランザクションで包む。
return _transactionManager.InTransaction(() => Next(command));
}
}
6. 組み立て(Wiring)
// CreateBookingWiring.cs
public sealed class CreateBookingWiring
{
private readonly IBookingRepository _repo;
private readonly ITransactionManager _tx;
private readonly IAuditLogger _logger;
public CreateBookingWiring(
IBookingRepository repo,
ITransactionManager tx,
IAuditLogger logger
)
{
_repo = repo;
_tx = tx;
_logger = logger;
}
public ICreateBookingUseCase Build()
{
// 内側(本体)から外側(周辺責務)へ順に組み立てる。
var core = new CreateBookingUseCaseImpl(_repo);
var validated = new ValidationDecorator(core);
var authorized = new AuthorizationDecorator(validated, new[] { "TA-" });
var transactional = new TransactionDecorator(authorized, _tx);
var audited = new AuditDecorator(transactional, _logger);
return audited;
}
}
この組み立て順が、実行順(外側→内側)になります。
7. 正常系のシーケンス図
実行の流れは以下のようになります。
8. 悪い例:継承で機能を盛るとどうなるか
Decoratorを使わずに、継承だけで「入力チェック+認可+監査ログ+取引」を盛っていくと、次のようなクラスが増えていきます。
// 悪い例:組み合わせごとにクラスが増える
public class CreateBookingUseCaseImpl { /* 本体 */ }
public class CreateBookingWithValidation : CreateBookingUseCaseImpl { /* 入力チェック */ }
public class CreateBookingWithAuthorization : CreateBookingUseCaseImpl { /* 認可 */ }
public class CreateBookingWithAudit : CreateBookingUseCaseImpl { /* 監査ログ */ }
public class CreateBookingWithTransaction : CreateBookingUseCaseImpl { /* 取引 */ }
public class CreateBookingWithValidationAndAuthorization : CreateBookingUseCaseImpl { /* ... */ }
public class CreateBookingWithValidationAndAudit : CreateBookingUseCaseImpl { /* ... */ }
public class CreateBookingWithAuthorizationAndAudit : CreateBookingUseCaseImpl { /* ... */ }
public class CreateBookingWithValidationAuthorizationAudit : CreateBookingUseCaseImpl { /* ... */ }
// さらに組み合わせが増えていく
この形だと、責務の組み合わせが増えるたびにクラスが増殖します。追加や差し替えが重くなり、どの組み合わせを使うべきかも分かりづらくなります。
Decoratorなら「必要なものだけを巻く」だけで済むため、変更に強い構成になります。
8. 実行方法
cd Decorator/DecoratorApp
dotnet run
出力例(正常/失敗の2回実行)を Program.cs で用意しています。
9. まとめ
- 本体ロジックをシンプルに保ちながら、周辺責務を外側で追加できる。
- 追加責務を増やしても、クラスを1つ足して巻くだけで済む。
- 実務の「入力検証・認可・監査・取引」をきれいに分離できる。
- 責務が分離されるため、Decorator単体のテストがしやすくなる。
- 本体ロジックが薄くなり、振る舞いの確認が容易になる。
- 将来的な追加・差し替えにも柔軟に対応できる。
Decoratorは、変更が多い周辺処理を安全に追加できる点で、実務と相性が良いパターンです。

