0. まえがき
Youtubeで"sort algorithms visualized"などと検索すると、
ソートの様子を描いた綺麗なアニメーション動画がヒットします。
今回は、それを真似てコンソールでソートの様子が見えるプログラムを作りたいと思います。
言語はC#ですが、固有の機能(WPFでデータバインドが~とか)は使用せず、
枠組みからムニムニ作りますのでオブジェクト指向型言語であれば同じように作れるかと思います。
端的には、コンソールで動くこれ(↓)を作ります(バブルソートしてますね)
1. 開発環境
- OS
- Windows 10 Home
- IDE
- Visual Studio 2019 Community
- 言語
- C# 7.0以上 (System.ValueTapleが使えるやつ)
2. 設計(雑)
ソート処理にて要素の交換が発生した時、その時点の配列の内容を画面に表示、
また要素の交換が発生したら画面をクリアして再び配列の内容を表示・・・。
という処理をソート完了まで繰り返していけば上にあるようなアニメが出来そうです。
・・・こう書いてみると、ソートアルゴリズムの中に画面表示の処理を組み込んだら
それで一発完成しそうな雰囲気ですが、折角なのでオブジェクト指向の考え方で作っていきたいと思います。
(何が折角なのかは不明)
2.1. Observerパターンを検討してみる
2.1.1 そもそも👆ってなに?
これっす👇
https://ja.wikipedia.org/wiki/Observer_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3
またはQiitaで検索検索ゥ!!諸先輩方の大変分かりやすい記事がヒットしますヨ。
2.1.2 パターンに当てはめてみる
上のリンク内の説明に従うと、
ソートする人 → 通知する側(配列内容を交換する度にその状態をお知らせ)
画面表示する人 → 通知を受ける側(お知らせ内容に従い画面表示)
と置けそうです。
2.2 クラス図に起こしてみる
- 使用したツール
- まゐくろそふと ゑくせる
左側が通知を受ける側、右側が通知する側になります。
また水色が根幹の仕掛けとなる枠組み(フレームワーク部)で、
黄色が具体的な処理を書く部分(アプリケーション部)になります。
こうして俯瞰的に見てみると、通知する側・される側が相互参照していて
こんがらがりそうなんですがそれは・・・。
まぁ、フレームワーク部から順次作っていきます。。。
各クラスの説明は次章にて♨
クラス図初めて書いたんだけど合ってるんかなこれ
3. 実装
3.0 前準備
System.ValueTapuleを使用可能にする
すみません。冒頭でC#固有の機能は使わないと書きましたが微妙にウソつきました(ヘケッ!)
何かというと、ソートなので要素の交換は必須処理なのですが、
なぜか.NETの標準にはSwap<T>( ref T a, ref T b )のようなメソッドがなく、
自前で作ったことがある方も多いかと思います。
がしかし、C#7.0から使用可能になったSystem.ValueTapleを使えばSwap(ryを自前で用意しなくても簡単に要素の交換ができるので、
今回はこれを使って下のように書きたいと思います。
(a,b)=(b,a);// こんな風に値の交換を書ける使用するためにはNugetからゲトってインストールする必要があります。
画面表示用のメソッドを作成する
Observerパターンとは直接関係ないので切り離してここに記載しました。
アニメーションでの一コマ相当を画面に表示する静的クラス&メソッドです。
一旦画面をクリアしてから描きたいものを表示、それからちょっとタイム。
以降も呼び出される度にクリア→表示→タイムでアニメの再現・・・ってとこです。
タイムを設けているのは、これが無いと目視不可能な程の爆速でアニメが終了し
ポカーン( ゚д゚)となってしまうからです。
usingSystem.Threading;namespaceSortVisualizerCUI.Application{/// <summary>/// コンソールに対しアニメ表示を行う人/// </summary>publicstaticclassAnimator{privateconstintWaitTime_ms=100;// 画面の表示更新の間隔[ms]/// <summary>/// アニメでいうところの一コマ分を表示する/// </summary>publicstaticvoidDisplaySingleFrame(stringvalue){Console.Clear();Console.WriteLine(value);Thread.Sleep(WaitTime_ms);}}}3.1 枠組みを作る(フレームワーク部の作成)
クラス同士の関係を分かりやすくするため共通の絵文字をタイトルに付けました。
同じ絵文字がついているものは派生元・派生先同士、
インターフェースの定義・実装同士ってことです。
: 通知する側
: 通知を受ける側
3.1.1
Observableクラス(通知する側)
通知する側が備えておくべき機能を持たせた抽象クラスです。
ここから具象クラスをニョキニョキ派生させてそこへやりたい処理を書きます。
通知を受ける側への参照をリスト(observers)で複数持っているのは、
例えばGUIなら複数のコントロールに一斉に状態変更を知らせたい時があるためです。
今回は通知する相手が一人なので死に機能なんですけどね(ヘケッ!)
細けぇこたぁ(ryソースコード中のコメントをご参照くださいませ。m(_ _)m
usingSystem.Collections.Generic;namespaceSortVisualizerCUI.Framework{/// <summary>/// 通知する側の抽象クラス/// </summary>publicabstractclassObservable{/// <summary>/// 通知を受ける側への参照。/// </summary>privatereadonlyList<IObserver>observers=newList<IObserver>();/// <summary>/// 通知を受ける側を追加する。/// </summary>/// <param name="observer"></param>publicvoidAddObserver(IObserverobserver)=>observers.Add(observer);/// <summary>/// 通知を受ける側を削除する。/// </summary>/// <param name="observer"></param>publicvoidRemoveObserver(IObserverobserver)=>observers.Remove(observer);/// <summary>/// 通知を受ける側へ自身の状態変更を知らせる。/// </summary>protectedvoidNotifyObservers()=>observers.ForEach(observer=>observer?.Update(this));}}3.1.2
IObserverインターフェース(通知を受ける側)
インターフェースの定義なのでこれだけです。
読んでも「で?」って感じなので次ィ・・・。
namespaceSortVisualizerCUI.Framework{/// <summary>/// 通知を受ける側のインターフェース定義/// </summary>publicinterfaceIObserver{/// <summary>/// 通知する側から状態変更の知らせを受けた時の更新処理/// </summary>/// <param name="observable"></param>voidUpdate(Observableobservable);}}3.2 具体的な処理を書く(アプリケーション部の作成)
3.2.1
SortExecutableクラス(通知する側:抽象)
すみません、また微妙にウソついてます(ヘケッ!)具体的な処理を書くということで、
👆のObservableクラス(抽象)から直接バブルソートにあたるクラス(具象)を
派生させてもよかったのですが、例えばXXXソートを増やしたい場合、
同じ処理を何度も書くのはダルいし読んでいてうざいので共通化出来るプロパティ・
メソッドを抽出しここに抽象クラスとしてまとめました。
(抽象クラスにしたのはこいつをnew()!!でインスタンス化されてもしょーもない為)
ただ、単に処理を共通化したいからという理由で上位クラスに抽出し派生させるのは
良くない説もあり(is-a関係がとれているか?など継承は本当に難しい)、
この時点でほのかに糞の香りがしていますが、かといって
合成で実現するのも違和感があるためもうこれで押し通します。
これにて通知する側は三段階派生することになりました。
Observableクラス(抽象)
↓
SortExecutableクラス(抽象)←今ココ
↓
XXXSortExecutableクラス(具象)
んでソースコードです。例により細けぇこたぁ(ryコメントで書いていますが、
肝はItemsプロパティのSetにてNotifyObservers()を呼んでいる箇所で、
これが自分自身の状態変更を知らせる=通知を受け取る側に仕事をさせるトリガーです。NotifyObservers()内で登録している全IObserverにUpdate()(仕事しろ)の指示を出しています。
usingSortVisualizerCUI.Framework;usingSystem.Collections.Generic;usingSystem.Linq;namespaceSortVisualizerCUI.Application{/// <summary>/// ソート実行体抽象クラス/// </summary>publicabstractclassSortExecutable:Observable{/// <summary>/// ソート対象のデータ/// </summary>protectedint[]items;/// <summary>/// コンストラクタ。ソート対象のデータで初期化する。/// </summary>/// <param name="items"></param>protectedSortExecutable(IEnumerable<int>items){this.items=items.ToArray();}/// <summary>/// ソート対象のデータ。/// Setされるたびに更新した内容を通知を受ける側へ知らせる。/// </summary>publicIReadOnlyCollection<int>Items{get=>items;set{if(!value.SequenceEqual(items)){items=value.ToArray();NotifyObservers();// ★ココ!で通知を受ける側へ状態変更を知らせる}}}/// <summary>/// 実際のソート処理。派生先クラスで各アルゴリズムの実装を強制させるため抽象メソッドとしている。/// このメソッド内で要素を交換した場合、必ずItemsプロパティを設定することにより通知を受ける側へ状態変更を知らせること。/// </summary>publicabstractvoidSort();}}3.2.2
BubbleSortExecutable(通知する側:具象)
これが最終派生クラスになります。通知する仕組みは派生元に定義済みなので、
あとはSort()メソッド内にバブルソートのアルゴリズムを書くだけでおk。
要素の交換が発生した直後にItemプロパティを更新することにより交換直後の配列の状態を
通知を受け取る側へお知らせしています。
(★ココ!で通知を受ける側へ状態変更を知らせるのコメントの箇所)
usingSystem.Collections.Generic;namespaceSortVisualizerCUI.Application{/// <summary>/// バブルソート実行体/// </summary>publicclassBubbleSortExecutable:SortExecutable{/// <summary>/// コンストラクタ/// </summary>/// <param name="items"></param>publicBubbleSortExecutable(IEnumerable<int>items):base(items){}/// <summary>/// ソートの実行。要素を入れ替えるたびに通知を受ける側へ状態変更を知らせる。/// </summary>publicoverridevoidSort(){vararray=items.Clone()asint[];for(inti=0;i<array.Length-1;i++){for(intj=array.Length-1;i<j;j--){if(array[j]<array[j-1]){// System.ValueTapleの機能による要素の交換(array[j],array[j-1])=(array[j-1],array[j]);// ★ココ!で通知を受ける側へ状態変更を知らせるItems=array;}}}}}}3.2.3
SortObserverクラス(通知を受ける側)
👆のBubbleSortExecutableから間接的にUpdate()をぶっ叩かれて
画面表示をするクラスです。
これ自体は大したことをしていないので、通知側する側からの処理の流れをまとめると、
BubbleSortExecutableで配列要素を交換したらItemプロパティを更新。ItemプロパティのSet内でNotifyObservers()が呼ばれる。NotifyObservers()内から通知を受ける側のUpdate()を呼ぶ。- 通知を受ける側の
Update()内で交換済みの配列を画面表示する。(今回は各配列要素の値を棒グラフ状に変換)
…とこんな感じです。各メソッドの定義が色んなファイルに散っているので
パッと見で処理を追いづらいですね。
usingSortVisualizerCUI.Framework;usingSystem;usingSystem.Linq;namespaceSortVisualizerCUI.Application{/// <summary>/// ソートの状態変更の通知を受ける側/// </summary>publicclassSortObserver:IObserver{/// <summary>/// ソート実行体(通知する側)を設定し、自分自身を登録する。/// </summary>publicSortExecutableDataSource{set=>value.AddObserver(this);}/// <summary>/// ソート実行体から変更通知を受け取った時の更新処理。/// </summary>/// <param name="observable"></param>publicvoidUpdate(Observableobservable){if(observableisSortExecutablesortExecutable){varstr=string.Empty;foreach(varninsortExecutable.Items){// 現在のソートの状態を数値の並び→横棒グラフ状の文字列に変換する。str+=newstring(Enumerable.Repeat("■",n).SelectMany(x=>x).ToArray())+Environment.NewLine;}// アニメの一コマ分として画面に表示するAnimator.DisplaySingleFrame(str);}}}}3.3 呼び出し側を作って完成!
複雑な仕掛けは各クラスの裏側に隠れているので、呼び出し側はこんなあっさり風味です。
今回は直接オブジェクトをnew()!!して生成していますが、
通知を受ける側sortObserverのDataSourceに通知する側bubbleSortを設定し忘れると、
「何も動かねぇ・・・」状態になるので、一連の生成処理をファクトリメソッドにまとめても良いかもしれません。
usingSortVisualizerCUI.Application;usingSystem;usingSystem.Linq;namespaceSortVisualizerCUI{internalclassProgram{/// <summary>/// エントリーポイント/// </summary>sprivatestaticvoidMain(){// 1~20の数値を生成・シャッフルし、バブルソートの初期値とする。varbubbleSort=newBubbleSortExecutable(Enumerable.Range(1,20).OrderBy(x=>Guid.NewGuid()));// バブルソート実行体を通知を受ける側へ設定するvarsortObserver=newSortObserver(){DataSource=bubbleSort};// バブルソートの実行。この処理内でソート処理のアニメーションが実行される。bubbleSort.Sort();}}}4. 動作確認
「Ctrl」+「F5」で実行すると、冒頭でお見せしたバブルソートのアニメーションが
コンソールに表示されるハズです。
・・・が、けっこうチラつきます。調べたところConsole.ほにゃらら()を駆使すれば
回避出来るっぽいのですが、今回はこれで許し亭♨
GitHubにプロジェクトを丸ごと置きました。もしよければ弄ってやって下さい。
SortVisualizer
5. プラスα
〇〇〇ソートを追加するSortExecutableクラスから派生させた先でソートのアルゴリズムを書くだけで
容易に追加が出来そうです。
GUIアプリとして作る
コンソール表示からフォームへの表示になるため、通知を受け取る側は
ゴミ箱逝き不可避ですが、通知する側は再利用できそうです。
もっとも、C#には変更通知の機能が備わっているためそちらを使うのが正道でしょうが。。。
6. あとがき
記事が無駄に長くなってしまった感が。
今回はぷちプログラムのためやってる内容に対して
ソース全体が大げさになってしまいましたが、
ある程度の規模以上になるとオブジェクト指向の良さが表れてくると思いました。
小生は、実務経験はほぼほぼ素のC言語でやってきたので、
オブジェクト指向の考え方、実装としておかしい点、説明不足などあれば
ご指摘・アドバイス頂けると大変うれしく存じます。
それでは、アディオス!!