13
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【C#】Interfaceをデリゲートのように扱って規約実装を伝播させる設計パターン

Last updated at Posted at 2025-11-09

英訳:
【C#】Design Pattern for Treating Interfaces Like Delegates to Propagate Contract Implementation
【C#】Design Pattern for Using Interfaces as Delegates to Cascade Contract Implementation

AdventCalendar2025 6日目の記事

編集履歴

いいねされない記事は削除されます。

この記事は制作に時間が掛かりすぎるため、ストックが3増えるごとに続きを書きます。

普通に派遣労働者なので週末での作業が限界です(笑)。


基礎研究的な記事。
技術的に特に目新しいものではなく、バックエンドやライブラリではよく使われるものです。

タイトルは分かりやすさとウケ狙いです。
sample Code付きの記事はなかなか無いのでという意味合いもあるでゲス。

ちなみに私はIT業界未経験なので、細かい業務的な案件を想定したものはだしませんでゲス。勘弁。

今回はGitも付けます。

Git

色んな記事見てるけどいちいち用意しない人が多い(笑)。
無駄な事してるんじゃないかとか思ったり

※原則として3Patternを同じGitリポジトリにぶちこみます。
pattern毎にプロジェクトとして分割するので、中で切り替えてください。

プラットフォームは例に依ってWPFでゲス。
Visual Studio2022,.net5.0 - 9.0(私の現行環境の場合は5.0まで指定出来る)
余談ですがVSCodeでも開けます。


目次

Pattern 1 : interface + DIによる“契約の伝播”
Pattern 2:abstract + ジェネリック型制約による“Templateの伝播”
Pattern 3 : Delegate Injectionを併用する


基本的な仕様

※私の敬虔な信仰心が反映されています。

  • ButtonA(仮称):
    処理A(仮称)に差し替える
    処理A:羊のText Ascie Artがloopするアニメーション

  • ButtonB:処理Bに差し替える

処理B : 文字がランダムに上から降って積もるアニメーション

  • ButtonC:処理Cに差し替える
    処理C:私が自作した聖書とひつじを関連付けたテキストを延々とテロップし続けるアニメーション(誰得)
  • ButtonD:PowerShellでファイルの列挙結果を出力

実行Buttonを押すとInjectionした処理を実行する


デモ動画


🔫Pattern1 : interfaceの挿入による“契約の伝播”

世に言うDependency Injection(DI)です。
実際、この実装はメソッドを変数として扱うDelegateから着想を得たそうです。

Interfaceの定義

IAnimationAction.cs



// CommonLib/IAnimation.cs
namespace CommonLib
{
    /// <summary>
    /// すべてのアニメーションが従う共通契約。
    /// WPFでもConsoleでも同一のインターフェイスを介して制御できる。
    /// </summary>
    public interface IAnimationAction
    {
        /// <summary>
        /// 初期化処理。必要なら外部リソースを確保する。
        /// </summary>
        Task InitializeAsync(CancellationToken token = default);

        /// <summary>
        /// 次のフレームを生成し、表示すべき文字列を返す。
        /// </summary>
        Task<string> NextFrameAsync(CancellationToken token);

        /// <summary>
        /// 終了処理。確保したリソースなどを解放する。
        /// </summary>
        Task FinalizeAsync(CancellationToken token = default);
    }
}


実装クラス

なぜかSealedクラスにしてきたんでそのままにしている。

diff_c_sharpのすゝめ。

折り畳み
AnimationExecutor.cs
// CommonLib/AnimationExecutor.cs
using System.Windows.Threading;

namespace CommonLib
{
    /// <summary>
    /// DIで受け取った IAnimation 実装を実行する共通制御クラス。
    /// 出力先(Action<string>)を注入することで、UI・Consoleの両対応を実現する。
    /// </summary>
    public sealed class AnimationExecutor
    {
        private readonly IAnimationAction _animation;
     private readonly Action<string> _outputAction;
        private readonly int _frameDelayMillis;

      
  public AnimationExecutor(IAnimationAction animation, Action<string> outputAction, int frameDelayMiallis = 100)
  {
   //AIはerrorハンドリングやってくれて便利でゲスねえ
+      _animation = animation ?? throw new ArgumentNullException(nameof(animation));
      _outputAction = outputAction ?? throw new ArgumentNullException(nameof(outputAction));
      _frameDelayMillis = frameDelayMiallis;
  }




        /// <summary>
        /// 非同期でアニメーションを実行する。
        /// </summary>
        public async Task RunAsync(CancellationToken token = default)
        {
            await _animation.InitializeAsync(token);

            try
            {
                while (!token.IsCancellationRequested)
                {
                    var frame = await _animation.NextFrameAsync(token);
+                    Dispatcher _outputDispatcher = Dispatcher.CurrentDispatcher;

+                     _outputDispatcher.Invoke(() => _outputAction(frame));


                    await Task.Delay(_frameDelayMillis, token);
                }
            }
+            // 最後に必ず開放(例外が発生しても確実に実行される)
            finally
            {
+                await _animation.FinalizeAsync(token);
            }
        }

    }
}
-----

処理A:Sheepアニメェション。

AI製でゲス。自力でやったら1週間は缶詰でゲスね。

折り畳み
SheepAnimation .cs
using CommonLib;
using System.Text;
using System.Windows.Controls;

public sealed class SheepAnimation : IAnimationAction
{
    private CancellationTokenSource? _cts;
    private bool _isRunning;
    private int _frameIndex;
    private int _offset;
    private readonly TextBox _output;
    private bool _isMovingRight = true; // ← 右方向フラグを追加
    string lastOutput = string.Empty;

    private readonly List<string> _frames = new()
    {
@"
  __  __  
 (oo)\____
 (__)\    )\
     ||--|| *",
@"
  __  __  
 (oo)\____
 (__)\    )\
     ||--||Z",
@"
  __  __  
 (oo)\____
 (__)\    )\
     ||--||z",
@"
  __  __  
 (oo)\____
 (__)\    )\
     ||--||"
    };

    public SheepAnimation(TextBox output)
    {
        _output = output;
    }

    public Task InitializeAsync(CancellationToken token = default)
    {
+        _cts = CancellationTokenSource.CreateLinkedTokenSource(token);
        _isRunning = true;
        _frameIndex = 0;

        double textBoxWidth = 0;
        _output.Dispatcher.Invoke(() =>
        {
+            textBoxWidth = _output.ActualWidth / 7; // 文字換算
        });
+        _offset = (int)(textBoxWidth) - 50; // 右端寄りから開始
        return Task.CompletedTask;
    }

    public async Task<string> NextFrameAsync(CancellationToken token)
    {
        if (!_isRunning)
            return lastOutput;

        string frame = _frames[_frameIndex];
        var lines = frame.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);

        double textBoxWidth = 0;
        await _output.Dispatcher.InvokeAsync(() =>
        {
            textBoxWidth = _output.ActualWidth / 7; // 文字幅換算
        });

        var sb = new StringBuilder();

        // 安全にオフセットを制御(負数はゼロ扱い)
        int safeOffset = Math.Max(0, _offset);

        foreach (var line in lines)
        {
            string spaces = new string(' ', safeOffset);
            sb.AppendLine(spaces + line);
        }

        await _output.Dispatcher.InvokeAsync(() =>
        {
            _output.Text = sb.ToString();
            _output.ScrollToEnd();
            lastOutput = _output.Text;
        });

        // フレーム進行
        _frameIndex = (_frameIndex + 1) % _frames.Count;

        // 移動処理:左右に往復する
        if (!_isMovingRight)
        {
            _offset++;
            if (_offset > textBoxWidth - 50)
                _isMovingRight = true; // 右端到達で反転
        }
        else
        {
            _offset--;
            if (_offset <= 0)
                _isMovingRight = false; // 左端到達で反転
        }

        await Task.Delay(120, token);
        return sb.ToString();
    }

    public Task FinalizeAsync(CancellationToken token = default)
    {
        _isRunning = false;
        _cts?.Cancel();
        return Task.CompletedTask;
    }
}
-----

処理B:文字が降ってくるアニメーション

折り畳み

using CommonLib;
using System.Text;
using System.Windows.Controls;

namespace WpfApp.Animations
{
    public class RainTextAnimation : IAnimationAction
    {
        private bool _isRunning;
        private CancellationTokenSource? _cts;

        public Task FinalizeAsync(CancellationToken token = default)
        {
            return Task.CompletedTask;
        }



        public async Task InitializeAsync(CancellationToken token = default)
        {
            _cts = CancellationTokenSource.CreateLinkedTokenSource(token);
            _isRunning = true;
            await Task.CompletedTask;
        }

        public async Task<string> NextFrameAsync(CancellationToken token)
        {
            var random = new Random();
            var width = 50;      // 表示幅(文字数)
            var height = 20;     // 表示高さ(行数)
            var buffer = new char[height, width];
            var sb = new StringBuilder();

            // 初期化(前フレームを考慮しない単発フレーム)
            for (int y = 0; y < height; y++)
                for (int x = 0; x < width; x++)
                    buffer[y, x] = ' ';

            // ランダムな文字を降らせる
            for (int i = 0; i < width / 3; i++)
            {
                int x = random.Next(width);
                int y = random.Next(height);
                buffer[y, x] = (char)random.Next(33, 126); // ASCII可視文字
            }

            // 出力組み立て
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                    sb.Append(buffer[y, x]);
                sb.AppendLine();
            }

            await Task.Delay(100, token);
            return sb.ToString();
        }

        public async Task RunAsync(TextBlock output, CancellationToken token)
        {
            _isRunning = true;
            var random = new Random();
            var width = 50;      // 表示幅(文字数)
            var height = 20;     // 表示高さ(行数)
            var buffer = new char[height, width];

            // 初期化
            for (int y = 0; y < height; y++)
                for (int x = 0; x < width; x++)
                    buffer[y, x] = ' ';

            var sb = new StringBuilder();

            while (!token.IsCancellationRequested && _isRunning)
            {
                // 新しい文字を一列分落とす
                for (int x = 0; x < width; x++)
                {
                    char c = (char)random.Next(33, 126); // 可視文字範囲
                    buffer[0, x] = c;
                }

                // 下に1行ずつずらす
                for (int y = height - 1; y > 0; y--)
                {
                    for (int x = 0; x < width; x++)
                        buffer[y, x] = buffer[y - 1, x];
                }

                // 上の行をクリア
                for (int x = 0; x < width; x++)
                    buffer[0, x] = ' ';

                // ランダムな位置に新しい文字を降らせる
                for (int i = 0; i < width / 3; i++)
                {
                    int x = random.Next(width);
                    buffer[0, x] = (char)random.Next(33, 126);
                }

                // 出力組み立て
                sb.Clear();
                for (int y = 0; y < height; y++)
                {
                    for (int x = 0; x < width; x++)
                        sb.Append(buffer[y, x]);
                    sb.AppendLine();
                }

                await output.Dispatcher.InvokeAsync(() =>
                {
                    output.Text = sb.ToString();
                });

                await Task.Delay(100, token);
            }
        }

    
    }
}


処理C:ひつじバイブルテロップ

折り畳み
using CommonLib;
using System.Windows.Controls;

namespace WpfApp.Animations
{
    public class SheepGospelAnimation : IAnimationAction
    {
        private bool _isRunning;
        private int _lineIndex = 0;
        private readonly string[] _verses =
        {
            "羊は主の声を聞き、その名を呼ばれて応える。",
            "主は羊を青草の野に伏させ、いこいの水のほとりに伴われる。",
            "我が魂を生き返らせ、義の道に導かれる。",
            "ひつじは光の方へ進み、闇を恐れない。",
            "EndOfData — ひつじはプログラムの中にも息づく。"
        };

        private readonly TextBox _output;

        public SheepGospelAnimation(TextBox output)
        {
            _output = output;
        }

        public Task InitializeAsync(CancellationToken token = default)
        {
            _isRunning = true;
            _lineIndex = 0;
            return Task.CompletedTask;
        }

        public async Task<string> NextFrameAsync(CancellationToken token)
        {
            if (!_isRunning)
                return string.Empty;

            // TextBox の行バッファを保持
            var lines = new List<string>();

            await _output.Dispatcher.InvokeAsync(() =>
            {
                var existing = _output.Text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
                lines.AddRange(existing);
            });

            // 新しい行を追加
            lines.Add(_verses[_lineIndex]);

            // 行数が多すぎたら上から削除(縦スクロール効果)
            int maxLines = 20;
            if (lines.Count > maxLines)
                lines.RemoveRange(0, lines.Count - maxLines);

            // 出力更新
            var text = string.Join(Environment.NewLine, lines);

            await _output.Dispatcher.InvokeAsync(() =>
            {
                _output.Text = text;
                _output.ScrollToEnd(); // 下に自動スクロール
            });

            // 次の行へ
            _lineIndex = (_lineIndex + 1) % _verses.Length;

            await Task.Delay(500, token); // スクロール速度調整
            return text;
        }

        public Task FinalizeAsync(CancellationToken token = default)
        {
            _isRunning = false;
            return Task.CompletedTask;
        }
    }
}



処理D:ファイル列挙アクション

PowerShellを呼んでます。蛇足だったかも。

折り畳み
ファイル列挙アクション.cs

using CommonLib;
using System.Diagnostics;
using System.Text;
using System.Windows.Controls;

public class ファイル列挙アクション : IAnimationAction
{
    private bool _isRunning;
    private Process? _process { get; set; }
    private readonly StringBuilder _buffer = new();
    private readonly TextBox _output;
    private readonly string _folderPath;

    public ファイル列挙アクション(TextBox output, string folderPath)
    {
        _output = output;
        _folderPath = folderPath;
    }


    string lastOutput = string.Empty;
    public Task InitializeAsync(CancellationToken token = default)
    {

        _isRunning = true;
        _buffer.Clear();

        var psCommand = $"Get-ChildItem -Path '{_folderPath}'| " +
                        "Select-Object LastWriteTime, Extension, Name";

        var startInfo = new ProcessStartInfo
        {
            FileName = "powershell.exe",
            Arguments = $"-NoProfile -Command \"{psCommand}\"",
            RedirectStandardOutput = true,
            UseShellExecute = false,
            CreateNoWindow = true
        };

        _process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
        _process.Start();

        _ = Task.Run(async () =>
        {
            if (_process == null) return;

            var lines = new List<string>();
            const int MaxLines = 1000;

            while (!_process.StandardOutput.EndOfStream &&
                   !_process.HasExited &&
                   _isRunning &&
                   !token.IsCancellationRequested)
            {
                var line = await _process.StandardOutput.ReadLineAsync();
                if (line == null) continue;

                lines.Add(line);

                // 200件取得で止める
                if (lines.Count >= MaxLines)
                {
                    _isRunning = false;
                    break;
                }

                // UIに反映(5件ごとに間引き)
                if (lines.Count % 5 == 0)
                {
                    var snapshot = string.Join(Environment.NewLine, lines);
                    await _output.Dispatcher.InvokeAsync(() =>
                    {
                        _output.Text = snapshot;
                        _output.ScrollToEnd();
+                        lastOutput = snapshot; 
+                        // 最後の出力結果を保存して返す
                    }, System.Windows.Threading.DispatcherPriority.Background);
                }
            }

            // 最終出力
            var finalText = string.Join(Environment.NewLine, lines);
            await _output.Dispatcher.InvokeAsync(() =>
            {
                _output.Text = finalText + Environment.NewLine + "[完了: 200件取得]";
                _output.ScrollToEnd();
            });

            FinalizeAsync(token).Wait();

        }, token);

        return Task.CompletedTask;
    }





    public async Task<string> NextFrameAsync(CancellationToken token)
    {
        if (!_isRunning)
+            return lastOutput;
+//ここで最終出力を返す。そうしないと再描画で出力が消える。

        // フレーム単位で返すだけ
        await Task.Delay(100, token);
        return _buffer.ToString();
    }

    public Task FinalizeAsync(CancellationToken token = default)
    {
        _isRunning = false;
        try
        {
            if (_process != null && !_process.HasExited)
            {
                _process.Kill();
                _process.Dispose();
            }
        }
        catch { }

        return Task.CompletedTask;
    }
}

StrategyPatternとの違い

Dependency InjectionはStrategyPatternとよく似ていますが、最大の違いは

  • StrategyPatternではどのクラス(戦略)を採用するかをCodeで提示
  • DIではInterfaceでクラス実装(規約)を一般化・抽象化し、それを注入する

というものです。つまり、DIはStrategyPatternを一歩進めた設計思想というわけです。

前回の記事を参照:高度な抽象化設計によるSnapShot UndoRedo
どっちかというと両者を併用しているかも。


実装に苦労した点

まあ殆どAIにやらせたんで何とか最小限の時間消費で済んだ。
処理Aはofset位置の調整とか。
処理Dは出力結果が消える原因がいまいち分からなかった件。String.EmptyをRetternしてたから

あとはどのツールを使うべきかとか(chkdiskは時間が掛かりすぎて例としてイマイチだった)、Consoleアプリとして出来るかとか。よく考えたら/cオプション付けなければTerminal出力だった(笑)


💀Pattern 2:abstract + ジェネリック型制約による“Templateの伝播”

※3ストックで更新。

あまり見かけない実装ですが、ライブラリではよく使われるそうです。処理内容は設計パターンの性質の問題があって少々変えてます。手間が掛かるでげす。


🃏Pattern 3 : Delegate Injectionを併用する

※6ストックで更新。

Interfaceではなく、DelegateをInjectionします。
これによりオーバーライドや実装の強制を伴うことなく処理の移譲が可能になります。
また、前者2パターンの組み合わせにより実装の柔軟性を維持することが可能でゲス。Very Coolでゲスね🃏。


参考Web文献

どんな記事でも必ず依拠してるドキュメントがあるのでゲス。
初心者向け順。

1.DI (依存性注入) って何のためにするのかわからない人向けに頑張って説明してみる
2.Dependency Injection: 依存性の注入 のお役立ち例
3.What is DI?
4. .NET の依存関係の挿入
5.【C#】Dependency Injection(依存性の注入)とは
6.なぜ依存を注入するのか DIの原理・原則とパターンを読んだ感想


あとがき

ごちゃごちゃ頑張って説明するより実際に書いた方がよほど早いと思うんだけど、どうなんだろうという。僕は実際、あんま頭良くないので動くCodeを追いたい派です。まあCodeから動作までイメージ出来るようになって半人前ぐらいでしょうか、感覚的に。

処理内容は即興で考えました。
ちょっと頑張れば自力で実装も出来るようになるでしょう。まあひな形は作ったんで。


余談ですが、突然口調が変わるというのはアルツハイマーの予兆なのだそうでゲス。歳でゲスかね。

※キャラクター(語尾)は「Diner」のギデオンを参考にしたでゲスよ。

image.png

13
14
2

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
13
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?