23
22

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

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

(25/11/16)Template Method Patternを反映しました。

※原則として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から着想を得て書いた記事です。

着想について

https://martinfowler.com/articles/injection.html

Martin Fowlerの記事 "Inversion of Control Containers and the Dependency Injection pattern" (2004年): DIの命名と詳細な説明。IoCとの関係を掘り下げ、DIの着想をサービスロケーターとの比較で論じています。

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ストックで更新。

ライブラリではよく使われるそうです

実行結果

ジェネリック型制約を使用する

共通化する部分
Animation: 右から左へ歩き、左端到達したら右端に戻る
Pause/Resume処理をIAsyncEnumerableで実装する

で共通処理(Template)を伝播させます。

詳細

IAsyncEnumerable を用いてアニメーションを非同期ストリーム化し、Pause/Resume の制御を行います。
また、Pause/Resume の振る舞いを表すインターフェイス(または抽象クラス)を Generic 型制約で縛ることで、すべてのアニメーションが共通のテンプレート(Pause/Resume の制御構造)を必ず備えるように保証します。
これにより実装の簡略化と、フレーム制御の一貫性を両立できます。

実装

AnimationTemplate.cs

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

namespace CommonLib.Animations
{
    public abstract class AnimationTemplate<T> : IAnimationAction, IAsyncEnumerable<string>
+        where T : IAnimationAction
    {
        protected List<string> _frames = new();
        protected int _frameIndex = 0;

        protected readonly TextBox _output;
        protected bool _isRunning;
        private bool _isPaused;
        protected int _offset { get; set; }

        protected virtual void AdvanceFrame()
        {
            _frameIndex++;

            if (_frameIndex >= _frames.Count)
                _frameIndex = 0;
        }

        protected async Task<string> RenderFrameAsync(string rawFrame, CancellationToken token)
        {
            var sb = new StringBuilder();
            string[] lines = rawFrame.Split("\n");

            int pad = Math.Max(0, _offset);

            foreach (var line in lines)
                sb.AppendLine(new string(' ', pad) + line);

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

            // offset を左に移動
            _offset--;
            if (_offset < 0)
                _offset = returnOfset();

            // 描画間隔
            await Task.Delay(150, token);

            return sb.ToString();
        }


        protected AnimationTemplate(TextBox output)
        {
            _output = output;

            _offset = returnOfset();
        }

        public int returnOfset()
        {
            int result = (int)_output.ActualWidth / 12; // 初期位置を右端に
            return result;
        }

        public virtual Task InitializeAsync(CancellationToken token = default)
        {
            _isRunning = true;
            _isPaused = false;
            //_offset = 0;


            _offset = returnOfset(); // 初期位置を右端に
            return Task.CompletedTask;
        }

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

        // 派生クラスで AA を返す
        protected abstract List<string> GetFrames();

        // 次のフレーム生成
        protected virtual async Task<string> GenerateFrameAsync(CancellationToken token)
        {
            var frames = GetFrames();
            var sb = new StringBuilder();

            double width = 0;
            await _output.Dispatcher.InvokeAsync(() => width = _output.ActualWidth / 7);

            var currentFrame = frames[0].Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);

            foreach (var line in currentFrame)
            {
                int pad = Math.Max(0, _offset);
                sb.AppendLine(new string(' ', pad) + line);
            }

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

            // オフセット更新
            _offset--;
            if (_offset < 0) // 左端まで行ったら戻す
                _offset = returnOfset(); // 右端の位置にリセット

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

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

            while (_isPaused)
                await Task.Delay(50, token);



            string current = _frames[_frameIndex];

            // 共通描画
            string rendered = await RenderFrameAsync(current, token);

            // フレームを次に進める
            AdvanceFrame();

            return rendered;
        }


        public void Pause() => _isPaused = true;
        public void Resume() => _isPaused = false;

        public async IAsyncEnumerator<string> GetAsyncEnumerator(CancellationToken token = default)
        {
            while (_isRunning && !token.IsCancellationRequested)
                yield return await NextFrameAsync(token);
        }
    }
}

Animationクラス

Gneeric型制約のおかげでIAnimationActionをいちいち実装しなくてよくなります。Pause/Resumeもtemplate継承のおかげで自動実装みたいな。

折り畳み
  • ひつじめぇション .cs
    力作。
using CommonLib;
using CommonLib.Animations;
using System.Windows.Controls;

public class SheepAnimation : AnimationTemplate<IAnimationAction>
{
    public SheepAnimation(TextBox output) : base(output)
    {


        _frames = GetFrames();
    }



    protected override List<string> GetFrames() => new List<string>
        {
@"
+  __  __       ┌────────────────────┐
+ (oo)\___      │                    │  
+ (__)\    )\   │All Woman Are sheep!│
+     ||--||   │                │   
+            ⎿___________⏌
",
@"
+  __  __       ┌────────────────────┐
+ (oo)\___      │                    │  
+ (__)\    )\   │All Woman Are sheep!│
+     /\--/| │                │   
+            ⎿___________⏌
"


        };
}
  • BirdAnimation .cs

using System.Windows.Controls;


namespace CommonLib.Animations
{

    public class BirdAnimation : AnimationTemplate<IAnimationAction>
    {
       
        public BirdAnimation(TextBox output) : base(output)
        {
            _frames = GetFrames();
        }

        protected override List<string> GetFrames() => new List<string>
        {

@"\```\        \ ``\ 
 ,,,\, \        \ ``  \ ,
_''"",e,""゛ヽ \ 、、\ 
 ̄ヽ, `ヽ: : : : : : : : : :゙ヽ
     ヽ, ヽ、、_,.,.,.,.-''"" 
      ヽ、、、___,,,,.,.-''"" 
  

",
@"
      ,,,,,
        _'"",e,゛ヽ、、、、、   \ 
         ̄ヽ, `ヽ: : : : :゙ヽ\ \
             ヽ,  ヽ、、_,.,.-'""  \ 
              ヽ、、、___,,.-'"" \  ``   ``\ 
                \   /           \\    \
                 \ /                \\
                   \↑/"
        };
    }
}
  • DolphinAnimation .cs
namespace PropagationByAbstractTemplate.Implements
{
    using global::CommonLib;
    using global::CommonLib.Animations;
    using System.Collections.Generic;
    using System.Windows.Controls;

    namespace CommonLib.Animations
    {


        public class DolphinAnimation : AnimationTemplate<IAnimationAction>
        {
            public DolphinAnimation(TextBox output) : base(output)
            {


                _frames = GetFrames();
            }

            protected override List<string> GetFrames() => new List<string>
    {
        @"       
                    -─-‘- 、i
        __, ‘´       ヽ、
       ’,ー– ●       ヽ、
        `""’ゝ、_          ‘,
          〈`’ー;==  ヽ、〈ー- 、 !! /  
           `ー´        ヽi   `ヽ iノ            
    " ,

        { @"    
                    -─-‘- 、i        
        __, ‘´       ヽ、         ´_‘´        _  
       ’,ー– ●       ヽ、! ,,,,     7
        `""’ゝ、_              ‘,
          〈`’ー;==ヽ、〈ー- 、         /
           `ー´    ヽi`ヽ ,,,・ 
          "
    }
        };
        }
    }
}

Generic 型制約の役割

単なるクラスを指定するのではなく、Interfaceを利用することも重要です。

  • 単純にCode量の削減
  • T が必ずアニメ契約(IAnimationAction)を満たすことを保証し、設計ミスをコンパイル時に排除する
  • テンプレートが T を“アニメーションとして扱える”ようにする(安全な内部利用)
  • 差し替え可能なアニメーションを IAnimationAction 実装に限定し、柔軟性と安全性を両立させる

この実装だと内部で型パラメータTを使っていないので、単なる自己満足の側面が強い(笑)

MVVM化と呼び出し

この際なので書いておく。
実装の都合でTextBoxをぶち込んでいるが多めに見てほしい。

ポリモーフィズムの例としてよくあるAnimal.Bark() Animal.Loud()みたいなのよりは分かりやすいのではないかと思っています

ViewModel作ろうか迷いましたが、めんどいし、これなら要らんだろってことで

↦やっぱり作っておいた。

めんどいですねこれ。コードビハインドと分割しようと思ったんですが。Pauseのクリックイベントが利かないので諦めました。(UI要素依存のやつなら利くだろうなと)

Prism.WPFを使っています。

折り畳み
AnimationViewModel.cs

using CommonLib;
using CommonLib.Animations;
using PropagationByAbstractTemplate.Implements.CommonLib.Animations;
using System.Windows.Controls;

namespace PropagationByAbstractTemplate
{
    internal class AnimationViewModel : BindableBase
    {

        private IAnimationAction? _selectedAnimation;
        private CancellationTokenSource? _cts;


        public DelegateCommand ACommand { get; }
        public DelegateCommand BCommand { get; }

        public DelegateCommand CCommand { get; }

        public DelegateCommand ExecuteCommand { get; }

        public DelegateCommand PauseCommand { get; }
        public DelegateCommand ResumeCommand { get; }


        TextBox _outputBox;

        public AnimationViewModel(TextBox textBox)
        {
            ACommand = new DelegateCommand(() => CallSheepAnimetion());
            BCommand = new DelegateCommand(() => CallBirdAnimation());
            CCommand = new DelegateCommand(() => CallDolphineAnimation());
            ExecuteCommand = new DelegateCommand(() => ExecuteMethod());
            PauseCommand = new DelegateCommand(() => PauseMethod());
            ResumeCommand = new DelegateCommand(() => ResumeMethod());

            _outputBox = textBox;
        }

        private void ResumeMethod()
        {
            if (_selectedAnimation is AnimationTemplate<IAnimationAction> template)
            {
                template.Resume();
                StatusString = "アニメーションを再開しました。";
            }
        }

        private void PauseMethod()
        {
            if (_selectedAnimation is AnimationTemplate<IAnimationAction> template)
            {
                template.Pause();
                StatusString = "アニメーションを一時停止しました。";
            }
        }

        private async void ExecuteMethod()
        {
            if (_selectedAnimation == null)
            {
                StatusString = "先にアニメーションを選択してください。";
                return;
            }

            // 前回実行中ならキャンセル
            _cts?.Cancel();
            _cts = new CancellationTokenSource();

            try
            {
                await _selectedAnimation.InitializeAsync(_cts.Token);

                // IAsyncEnumerable を活用してフレームを逐次表示
                await foreach (var frame in (_selectedAnimation as IAsyncEnumerable<string>)!.WithCancellation(_cts.Token))
                {
                    // UI スレッドで TextBox 更新
                    _outputBox.Dispatcher.Invoke(() =>
                      {
                          _outputBox.Text = frame;
                          _outputBox.ScrollToEnd();
                      });
                }
            }
            catch (OperationCanceledException)
            {
                StatusString = "アニメーションが停止しました。";
            }
            finally
            {
                await _selectedAnimation.FinalizeAsync(_cts.Token);
            }
        }

        private void CallDolphineAnimation()
        {        // 前回実行中ならキャンセル
            _cts?.Cancel();
            _cts = new CancellationTokenSource();


            _selectedAnimation = new DolphinAnimation(_outputBox);
            StatusString = "🐬 イルカアニメが選択されました";
        }

        private void CallBirdAnimation()
        {

            // 前回実行中ならキャンセル
            _cts?.Cancel();
            _cts = new CancellationTokenSource();


            _selectedAnimation = new BirdAnimation(_outputBox);
            StatusString = "🐤 鳥のアニメが選択されました";
        }

        string _statausStromg = "処理未選択";

        public string StatusString
        {
            get => _statausStromg;
            set => SetProperty(ref _statausStromg, value);
        }



        private void CallSheepAnimetion()
        {

            // 前回実行中ならキャンセル
            _cts?.Cancel();
            _cts = new CancellationTokenSource();

            _selectedAnimation = new SheepAnimation(_outputBox);
            StatusString = "🐑 羊のアニメが選択されました";



        }
    }
}


MainWindow

using CommonLib;
using System.Windows;

namespace PropagationByAbstractTemplate
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();


            amimationViewModel = new AnimationViewModel(TextOutput);
            DataContext = amimationViewModel;
        }
        private AnimationViewModel amimationViewModel;
    }
}
command登録が案外、煩雑なんですよね。どうにかならんかな。 私は運用論としてコードビハインドとViewModelでうまく責務を分割しようと考えているのですが、このケースだとスコープが違うから分割するとうまく動作しないかもしれない。

後日、このテーマで記事を書こうと考えています。

実装に苦労した点

$\color{Black}{\Large \textsf{AAの調整}}$
AAの調整にとても苦労致しました。
派生クラスでOfsetを維持したままアニメーションを差し替えようとしてもなかなかうまく行かなかったのでTemplateにした。(最終的には成功したが没にした)

Pause/Resumeを組み入れたのは良い趣向だったなと
これでTemplate MethodとGeneric型制約の利点を最大限発揮出来た、かもしれない


デメリット

ガチガチにTemplateで固めてしまうとアニメーションを差し替えるのにかなり苦労する、というか最悪、基底クラスは殆ど使えなくなることもある。

今回の例ではoffsetを維持したままアニメーションをさせるのはかなり難しかったので、Templateとして組み入れました。


...気に食わないがここだけAIが書いた。
テンプレート : 動きの流れ(開始→繰り返し→終端動作)を固定
派生 : いや、その途中に“offset の蓄積”という追加状態を入れたい
テンプレート「それは骨格を書き換えないと無理です」
派生「ほぼ全部オーバーライドする羽目に」

実際、ofsetが破棄されてしまい状態維持が難しいのを例によってstaticだの、Publicプロパティだのでうんぬん

🃏Pattern 3 : Delegate Injectionを併用する

※6ストックで更新。 → 更新済み

Interfaceではなく、DelegateをInjectionします。
これによりオーバーライドや実装の強制を伴うことなく処理の移譲が可能になります。

Template Method Patten(とジェネリック型制約) これがなかなか厄介でして、派生クラスでジグザグに動かしたりしようとしたらうまく行かないんですね。まず変な挙動になる。

→ そんなことしなくてもうまく行くという方は教えてください。

Delegateを使うこの手法はこういったインスタンスの齟齬を簡単に解決し、派生先でメソッドの合成がうまく出来ます。

Git

今回は大した変更ではないので、そのままPropagationByAbstractTemplateプロジェクトに追加しておきました。
image.png

デモ動画

ひつじメェションクラスにDelegateを書く


using CommonLib;
using CommonLib.Animations;
using System.Windows.Controls;


public class SheepAnimation : AnimationTemplate<IAnimationAction>
{
+   private readonly Func<string, int, int, string>? _frameModifier;

    public SheepAnimation(TextBox output, Func<string, int, int, string>? frameModifier = null)
+          : base(output, frameModifier) // ←ここで基底クラスのコンストラクタを呼ぶ
    {
        _frames = GetFrames();
    }

}
     protected override List<string> GetFrames() => new List<string>
     {
     //AAは割愛
       }
}

基底クラス: AnimetionTemplateにDelegateを差し込む

前のコンストラクタは残しますのでオーバーロードでゲスね。

AnimationTemplateクラス内
   protected List<string> _frames = new();
   protected int _frameIndex = 0;
   protected int _offset { get; set; }

+  protected Func<string, int, int, string>? FrameModifier { get; }
+  protected AnimationTemplate(TextBox output, Func<string, int, int, string>? frameModifier = null)
  {
      _output = output;
      _offset = returnOfset();
      FrameModifier = frameModifier;
  }  
  • RenderFrameAsyncメソッド
AnimationTemplateクラス内
 protected async Task<string> RenderFrameAsync(string rawFrame, CancellationToken token)
 {
     var sb = new StringBuilder();
     string[] lines = rawFrame.Split("\n");

     int pad = Math.Max(0, _offset);


     // Delegate があれば適用
+     string modifiedFrame = FrameModifier != null ? FrameModifier(rawFrame, _offset, _frameIndex) : rawFrame;
+     lines = modifiedFrame.Split("\n");

     foreach (var line in lines)
         sb.AppendLine(new string(' ', pad) + line);

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

     // offset を左に移動
     _offset--;
     if (_offset < 0)
         _offset = returnOfset();

     // 描画間隔
     await Task.Delay(150, token);

     return sb.ToString();
 }

これをViewModel側で実行するだけです。

AnimationViewModel内

  private void CallGigzagSheep()
  {
      // 前回実行中ならキャンセル
      _cts?.Cancel();
      _cts = new CancellationTokenSource();

      _selectedAnimation = new SheepAnimation(_outputBox, (frame, index, offset) =>
      {
          // 横オフセット(基底クラスの offset をそのまま使う)
          int horizontal = Math.Max(0, offset);

          // 縦方向ジグザグ(偶数で上、奇数で下)
          int vertical = (index % 2 == 0) ? 1 : 0;

          string verticalPad = string.Concat(Enumerable.Repeat("\n", vertical));

          var sb = new StringBuilder();
          sb.Append(verticalPad);

          foreach (var line in frame.Split("\n"))
              sb.AppendLine(new string(' ', horizontal) + line);

          return sb.ToString();
      });

      StatusString = "🐑 ジグザグ羊アニメが選択されました";
  }

なんでViewModelでこの書き方が成立するのかというと、SheepAnimationクラスで基底クラスのoffsetを渡しているからです。あくまで呼び出し元はViewModelで、offsetを基底クラスからリレーしてるような感覚です。

初心者向け講座(実は最近知った)

  • Func<string, int, int, string>? _frameModifier;について

Func<引数1の型, 引数2の型, 引数3の型, 戻り値の型>

左側がデリゲートとして渡す際に代入するパラメータです。
右側が戻り値という構造になっています。

この場合だと

_selectedAnimation = new SheepAnimation(_outputBox, 
(frame, index, offset) =>
//渡すパラメータ(Funcの左3つ)

ちなみにSheepAnimationのシグネチャはこうなってる


  public SheepAnimation(TextBox output, 
+  Func<string, int, int, string>?
   frameModifier = null)
   : base(output, frameModifier)

https://qiita.com/simoyama2323/items/11ec93a130c07e23de68

本稿は以上で終了となります。お疲れさまでした。

大量に実際呼ばないメソッドがあるようなので、今度整理しておきます。


参考Web文献

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

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


あとがき

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

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


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

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

image.png

23
22
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
23
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?