0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoFをモダンで実務アプリケーションよりにサンプル実装する試み~Decoratorパターン編~

0
Last updated at Posted at 2026-01-14

GitHubリポジトリ: https://github.com/cardamon04/GoF

  • (Decoratorパターンどんなやったっけって本を開いたものの、いまいちピンとこなかったので復習がてら)。
  • 本記事では、予約作成ユースケースを題材にして、Decorator(デコレータ:責務を外側に追加する設計)をC#で実装した例を紹介します。
  • 目的は「本体ロジックを太らせずに、周辺の責務を後付けできる」点を、実務の流れに近い形で理解することです。

1. Decoratorパターンとは

Decoratorパターンは、中身(本体)の処理を変えずに、外側で機能を追加する設計手法です。継承で機能を盛るのではなく、合成(オブジェクトを包む)で「前後処理」を差し込めるのがポイントです。

今回の例では、予約作成の処理に以下の周辺責務を追加します。

  • 入力チェック
  • 認可チェック
  • 監査ログ
  • 取引(トランザクション)

習うより慣れろ、サンプルコードを理解するとこの説明が腑に落ちると思います。


2. サンプルの構成

主要クラス

  • ICreateBookingUseCase:ユースケースの口
  • CreateBookingUseCaseImpl:本体ロジック(予約作成)
  • CreateBookingUseCaseDecorator:Decoratorの土台
  • ValidationDecorator / AuthorizationDecorator / AuditDecorator / TransactionDecorator

クラス図

以下は本サンプルの最小構成のクラス図です。

IMG_1294.png


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. 正常系のシーケンス図

実行の流れは以下のようになります。

IMG_1295.jpeg


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は、変更が多い周辺処理を安全に追加できる点で、実務と相性が良いパターンです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?