C#におけるデザインパターンの実装と考察

本稿では、James W. Cooper著『C# Design Patterns : A Tutorial』で解説されている代表的なデザインパターンについて、自身の理解に基づき整理・解説します。解釈に偏りがある可能性もございますので、ご意見・ご感想をいただけると幸いです。

ビヘイビアーパターン (Behavioral Pattern)

オブジェクト間の連携やメッセージのやり取りに焦点を当てたパターン群です。

責任連鎖パターン (Chain of Responsibility)

オブジェクト間の結合度を低減させることを目的とします。リクエストを複数のオブジェクトに連鎖させ、各オブジェクトが処理するか、次のオブジェクトに渡すかを決定します。

適用場面:

  • 一つのリクエストを複数のクラスが処理する可能性があり、リクエストがクラス間を循環して、いずれかのクラスが処理を完了するような場合。
  • 相互作用のロジックを呼び出し元に集中させたくない場合。

実装例:

ハードウェアコマンドの送受信処理を例に、責任連鎖パターンを適用したコードを示します。この例では、文字列中の特定の文字をカウントしたり、特定の文字間の経過時間を計測したりする処理を想定しています(あくまで概念実証のためのコードです)。


using System;
using System.Collections.Generic;

namespace ConcurrencySolution
{
    #region 基底クラス
    /// <summary>
    /// メッセージ基底クラス
    /// </summary>
    public class MessageBase
    {
        public string MessageId { get; }
        protected MessageBase()
        {
            MessageId = Guid.NewGuid().ToString("N");
        }
    }

    /// <summary>
    /// メッセージ応答基底クラス
    /// </summary>
    public class MessageResponseBase
    {
        public string OriginalMessageId { get; }
        public bool Success { get; }
        public MessageResponseBase(string msgId, bool success)
        {
            OriginalMessageId = msgId;
            Success = success;
        }
    }

    /// <summary>
    /// チェーン処理の抽象基底クラス
    /// </summary>
    public abstract class HandlerChain
    {
        protected HandlerChain NextHandler { get; private set; }

        public void SetNext(HandlerChain handler)
        {
            NextHandler = handler;
        }

        public MessageResponseBase ProcessMessage(MessageBase msg)
        {
            MessageResponseBase response = HandleInternal(msg);

            if (response != null && response.Success)
            {
                return response;
            }

            if (NextHandler != null)
            {
                return NextHandler.ProcessMessage(msg);
            }

            return null; // どのハンドラも処理できなかった場合
        }

        protected abstract MessageResponseBase HandleInternal(MessageBase msg);
    }

    /// <summary>
    /// 特定のワーカー処理を行う抽象基底クラス
    /// </summary>
    public abstract class WorkerHandler : HandlerChain
    {
        public string WorkerIdentifier { get; }

        protected WorkerHandler(string identifier)
        {
            WorkerIdentifier = identifier;
        }

        protected override MessageResponseBase HandleInternal(MessageBase msg)
        {
            var workerMsg = msg as SpecificWorkerMessage;
            if (workerMsg == null)
            {
                throw new ArgumentException("Invalid message type for WorkerHandler.", nameof(msg));
            }
            return ExecuteWork(workerMsg);
        }

        protected abstract SpecificWorkerMessageResponse ExecuteWork(SpecificWorkerMessage msg);
    }
    #endregion

    /// <summary>
    /// ワーカー固有のメッセージクラス
    /// </summary>
    public class SpecificWorkerMessage : MessageBase
    {
        public string OperationCode { get; }
        public SpecificWorkerMessage(string operationCode)
        {
            OperationCode = operationCode;
        }
    }

    /// <summary>
    /// ワーカー固有の応答クラス
    /// </summary>
    public class SpecificWorkerMessageResponse : MessageResponseBase
    {
        public string HandlerIdentifier { get; }
        public SpecificWorkerMessageResponse(string msgId, bool success, string handlerId)
            : base(msgId, success)
        {
            HandlerIdentifier = handlerId;
        }
    }

    #region 具体的なワーカーハンドラ
    /// <summary>
    /// 'J' 文字を処理するハンドラ
    /// </summary>
    public class HandlerJ : WorkerHandler
    {
        public int JCount { get; private set; } = 0;

        public HandlerJ(string identifier) : base(identifier) { }

        protected override SpecificWorkerMessageResponse ExecuteWork(SpecificWorkerMessage msg)
        {
            var response = new SpecificWorkerMessageResponse(msg.MessageId, false, WorkerIdentifier);
            if (msg.OperationCode == "J")
            {
                response.Success = true;
                JCount++;
            }
            return response;
        }
    }

    /// <summary>
    /// 'M' 文字を処理するハンドラ
    /// </summary>
    public class HandlerM : WorkerHandler
    {
        public HandlerM(string identifier) : base(identifier) { }

        protected override SpecificWorkerMessageResponse ExecuteWork(SpecificWorkerMessage msg)
        {
            var response = new SpecificWorkerMessageResponse(msg.MessageId, false, WorkerIdentifier);
            if (msg.OperationCode == "M")
            {
                response.Success = true;
            }
            return response;
        }
    }

    /// <summary>
    /// 'B' 文字を処理し、時間を記録するハンドラ
    /// </summary>
    public class HandlerB : WorkerHandler
    {
        public List<DateTime> TimestampWhenB { get; } = new List<DateTime>();

        public HandlerB(string identifier) : base(identifier) { }

        protected override SpecificWorkerMessageResponse ExecuteWork(SpecificWorkerMessage msg)
        {
            var response = new SpecificWorkerMessageResponse(msg.MessageId, false, WorkerIdentifier);
            if (msg.OperationCode == "B")
            {
                response.Success = true;
                TimestampWhenB.Add(DateTime.Now);
            }
            return response;
        }
    }
    #endregion
}

使用例:


public void ProcessStringOperations()
{
    string inputSequence = "Bb1123lkiJMoBp";
    var handlerJ = new HandlerJ("J_Handler");
    var handlerM = new HandlerM("M_Handler");
    var handlerB = new HandlerB("B_Handler");

    handlerJ.SetNext(handlerM);
    handlerM.SetNext(handlerB);

    int jmbSequenceCount = 0;
    foreach (char c in inputSequence)
    {
        var message = new SpecificWorkerMessage(c.ToString());
        var response = handlerJ.ProcessMessage(message) as SpecificWorkerMessageResponse;
        if (response != null && response.Success)
        {
            jmbSequenceCount++;
        }
    }

    Console.WriteLine($"Total J, M, B sequence count: {jmbSequenceCount}");
    if (handlerB.TimestampWhenB.Count >= 2)
    {
        TimeSpan diff = handlerB.TimestampWhenB[1] - handlerB.TimestampWhenB[0];
        Console.WriteLine($"Time between first two 'B's: {diff.TotalMilliseconds} ms");
    }
    Console.WriteLine($"'J' occurrences: {handlerJ.JCount}");
}

別の実装例 (曜日判定):


using System;

public interface IDayHandler
{
    void SetNext(IDayHandler nextHandler);
    void HandleDay(DayOfWeek day);
}

public class SundayHandler : IDayHandler
{
    private IDayHandler _nextHandler;
    public DayOfWeek HandledDay { get; } = DayOfWeek.Sunday;

    public void SetNext(IDayHandler nextHandler)
    {
        _nextHandler = nextHandler;
    }

    public void HandleDay(DayOfWeek day)
    {
        if (day == HandledDay)
        {
            Console.WriteLine($"It's {HandledDay}! Time to relax and study.");
        }
        else if (_nextHandler != null)
        {
            _nextHandler.HandleDay(day);
        }
        else
        {
            throw new InvalidOperationException($"No handler found for day: {day}");
        }
    }
}

public class DefaultDayHandler : IDayHandler
{
    private IDayHandler _nextHandler; // 末尾なので未使用だがインターフェース定義のため

    public void SetNext(IDayHandler nextHandler)
    {
        throw new NotSupportedException("This is the end of the chain.");
    }

    public void HandleDay(DayOfWeek day)
    {
        Console.WriteLine($"Processing day: {day}. Default handling.");
    }
}

public class DayProcessingChain
{
    private readonly IDayHandler _firstHandler;

    public DayProcessingChain()
    {
        var sundayHandler = new SundayHandler();
        var defaultHandler = new DefaultDayHandler();

        sundayHandler.SetNext(defaultHandler);
        _firstHandler = sundayHandler;
    }

    public void Execute(DayOfWeek currentDay)
    {
        _firstHandler.HandleDay(currentDay);
    }
}

戦略パターン (Strategy Pattern)

アルゴリズム(戦略)を個別のクラスにカプセル化し、実行時にそれらを交換可能にします。

適用場面:

  • 複数のアルゴリズムが存在し、それらを状況に応じて切り替えたい場合。

実装アプローチ:

  • アプローチ1: 各戦略を独立したクラスとして実装する。
  • アプローチ2: 戦略をデリゲート(例: Func, Action)として扱う。より柔軟で軽量ですが、デリゲートが属するクラスとの結合度が高くなる可能性があります。

アプローチ2の実装例:


using System;

public class ContextStrategy
{
    private Delegate _currentStrategy;

    public ContextStrategy() { }

    public ContextStrategy(Delegate initialStrategy)
    {
        _currentStrategy = initialStrategy ?? throw new ArgumentNullException(nameof(initialStrategy));
    }

    public object ExecuteStrategy(params object[] arguments)
    {
        if (_currentStrategy == null)
        {
            throw new InvalidOperationException("No strategy has been set.");
        }
        try
        {
            return _currentStrategy.DynamicInvoke(arguments);
        }
        catch (Exception ex)
        {
            // エラーハンドリング
            Console.WriteLine($"Strategy execution failed: {ex.Message}");
            throw;
        }
    }

    public void SetNewStrategy(Delegate newStrategy)
    {
        _currentStrategy = newStrategy ?? throw new ArgumentNullException(nameof(newStrategy));
    }
}

アプローチ2の使用例:


// 加算戦略
var additionStrategy = new Func<int, int, int>((a, b) => a + b);
var context = new ContextStrategy(additionStrategy);
var result = context.ExecuteStrategy(10, 5); // result = 15
Console.WriteLine($"Result of addition: {result}");

// 減算戦略に変更
var subtractionStrategy = new Func<int, int, int>((a, b) => a - b);
context.SetNewStrategy(subtractionStrategy);
result = context.ExecuteStrategy(10, 5); // result = 5
Console.WriteLine($"Result of subtraction: {result}");

生成パターン (Creational Pattern)

オブジェクトの生成プロセスに焦点を当てたパターン群です。オブジェクト生成の責務を専門の「ファクトリ」クラスに委譲することで、コードの柔軟性と再利用性を高めます。

シングルトンパターン (Singleton Pattern)

クラスのインスタンスが一つしか存在しないことを保証し、その唯一のインスタンスへのグローバルなアクセスポイントを提供します。リソース管理など、アプリケーション全体で共有されるべき単一のインスタンスが必要な場合に有効です。

実装例 (静的コンストラクタを使用):


public sealed class ResourceManager // sealed で継承を防ぐ
{
    private static readonly ResourceManager _instance = new ResourceManager();

    // 静的コンストラクタは、クラス型が初めて参照されたときに一度だけ実行される
    static ResourceManager() { }

    // プライベートコンストラクタで外部からのインスタンス化を禁止
    private ResourceManager()
    {
        // リソースの初期化処理など
        Console.WriteLine("ResourceManager instance created.");
    }

    public static ResourceManager Instance
    {
        get { return _instance; }
    }

    public void LoadConfiguration()
    {
        Console.WriteLine("Configuration loaded.");
    }
}

注記: 上記の実装は静的コンストラクタを利用しています。一般的に知られる `lock` を使用した実装(スレッドセーフを考慮したもの)と比較して、初期化時のオーバーヘッドが少なく、また、`lock` 実装で稀に発生しうるインスタンス取得失敗(null返却など)のリスクがないという利点があります。静的コンストラクタは.NETランタイムによって管理され、スレッドセーフであることが保証されています。

タグ: C# デザインパターン 責任連鎖パターン 戦略パターン シングルトンパターン

9月29日 16:40 投稿