- 投稿日:2021-09-14T23:11:48+09:00
計量的な統計を扱うためのSystem.Diagnostics.Metrics API
はじめに dotnet-6.0 preview5より、System.Diagnostics.Metrics名前空間と、配下にAPIが追加された。 今後使うことになりそうかなあと思ったので、これについて解説しようと思う。 記事執筆時点での最新版はdotnet-6.0pre7なので、これをベースに解説する。 何をするためのものか 短く言うと、"プログラムにおける計量的な統計情報を扱うための仕組み"というべきだろうか。 ここでいう計量的な統計情報とは、"平均CPU使用率"とか、"平均秒間リクエスト数"とか、"秒間IO書き込みデータ量"とか、 とにかく数字で表せる統計情報を指す。 概念的には、PrometheusやMackerel的なものを扱っている人であれば、馴染みは深いと思う。 Windowsユーザーであれば、パフォーマンスモニター(perfmon.exe)で収集できるような情報と言えばわかりやすいだろうか。 概念的には、OpenTelemetryのMetricsをdotnetの中で実現するための基盤となる。 実際、OpenTelemetryのdotnetライブラリも、PrometheusのExporterは1.2.0-alpha1以降こちらを使うようになっている。 また、ConsoleExporterもこのバージョンからMetricsに対応している。 想定するシナリオ 登場人物としては、大雑把に言うと情報を発信する側(Instrument、Publisher)と、情報を受け取って処理する側(Collector)が存在する。 pushシナリオ こちらは、Instrumentが、Collectorに情報を発信するということを想定する。 仕組みとしては割と単純になる。 利点としては、 全体的な仕組み(特にInstrument側)が単純 事象の見逃しが起こりにくい 欠点としては、 エラー処理が大変 Collector側の処理オーバーフロー、システムダウン等 Collector側の不具合がInstrument側に影響を及ぼす場合がある(送信エラーのリトライ等による余分な負荷等) 間にBrokerを置く等、階層化することによって軽減は可能 pullシナリオ こちらは、 Collector側の情報取得の要求をトリガーとして値を取得する という方式である。 Prometheusはこの方式である。 利点としては Collector側の負荷制御がしやすい 問い合わせに来なければInstrument側の負荷は無い エラー処理が比較的容易 欠点としては 一つのやり取りだけ見ればInstrument側のオーバーヘッドはpush型よりも大きくなりがち 普通Collectorは高頻度で問い合わせはしないので、多くの場合全体的な負荷は減る Instrument側の仕組みが複雑になりがち Instrument側にCollectorを受け入れる口を作る必要がある(prometheusのexporterはHttpListenerかASP.NET Coreを使って口を作るようにしている) pullタイミングによっては事象を見逃す可能性がある CPUのスパイク現象等 Collector側が自重しないと結局Instrument側が過負荷になりやすい EventCounterとの関連 このような仕組みを実現する似たようなものとして、.NET Frameworkの時代からEventCounterというものが存在する(pullシナリオの場合はPollingCounter(要netstandard2.1以降))。 しかし、これはEventSourceに強く結びついたもので、opentelemetryの仕組みの実現としては多少不便なものがあった。 そこで、純粋にマネージドコードのみで実装し、opentelemetryで扱いやすく設計し直したのがSystem.Diagnostics.Metricsとなる。 また、MetricsのイベントをEventSourceで取り扱うための MetricsEventSource というものがあるが、 この記事で記述すると長大になってしまうため、今回は詳しい説明は省略する。 MetricsEventSourceの中では、Counter<T>、ObservableCounter<T>の集計を行っている等、各Instrumentの扱いが異なるので注意 DiagnosticSourceとの関連 立ち位置的には兄弟のようなもので、pushシナリオは実はDiagnosticSourceでも実現可能。 しかし、 DiagnosticSourceで扱うと想定されるオブジェクトはより汎用的なオブジェクトで、メトリックとして使うにはオーバーヘッドが大きくなることがある pullシナリオは実現が難しい という事情があるため、Metricsが実装された。 導入 対象とするTargetFrameworkがnet6.0であれば、特に追加パッケージは必要ない。 net5.0あるいはそれ以下で使いたい場合は、System.Diagnostics.DiagnosticSourceの"6.0.0-preview.5.21301.5"以降を追加すれば、System.Diagnostics.Metrics以下が使えるようになる。 Instrument側の流れ 情報発信を行う側で登場するのは以下で、全てSystem.Diagnostics.Metrics配下に存在する。 Meter Instrumentの親となるオブジェクト Counter<T> pushシナリオで、増分を記録するためのもの Histogram<T> pushシナリオで、その時の値を記録するためのもの ObservableCounter<T> pullシナリオで、増分を記録するためのもの ObservableGauge<T> pullシナリオで、その時の値を記録するためのもの 簡単に流れを書くと、 Meterオブジェクトの作成 各種Instrumentの作成 イベントの発生 Meter.Disposeで各種Instrumentオブジェクトの破棄 破棄は必須ではない Meterオブジェクトの作成 new System.Diagnostics.Metrics.Meter(string name, string? version)で、大元となるMeterオブジェクトを作成する。 注意点として、グローバルなリストに登録されるため、多くの場合でstatic readonlyにして、大量生成されないようにする 使い終わったらDisposeすれば、グローバルなリストからは外される(生存期間がアプリケーションのライフタイムと一緒ならば、Disposeは必ずしもしなくていい) Instrumentの作成 前項で作成したMeterオブジェクトから各種Instrumentを作成する。 6.0時点で作成可能なものは以下の通り Counter<T> where T: struct Histogram<T> where T: struct ObservableCounter<T> where T: struct ObservableGauge<T> where T: struct 上記の型は、Instrument<T> から派生している。 Tで取り得る型は以下の通り byte short int long float double decimal 要するに基本の数値型で、他の型を使おうとすると、Create時にInvalidOperationExceptionが発生する。 それぞれのInstrumentについての説明は後述する。 全てに共通して言えることだが、Createした時点で、プログラム内に作成イベントが通知されるため、シングルトンで運用するのが望ましい。 また、MeterがDisposeされた段階で、Instrumentも使えなくなるので、お互いの生存期間には注意すること。 Instrument 全てのカウンターのベースクラスとなる。 持っている公開プロパティとしては、 名前 型 説明 Name string 名前 Description string 説明(ユーザー任意) Enabled bool 監視しているリスナーがいるかどうか IsObservable bool Observableかどうか(pullシナリオ用かどうか) Meter Meter 親となるMeterのインスタンス Unit string 単位を表す文字列(req/s,KB等) がある。 Instrument<T> Instrumentクラスから派生した、pushシナリオ用の派生クラス。 イベント発生をさせるためのprotectedメソッドであるRecordMeasurementが追加されている。 このメソッドでは、第一引数に値を入れるが、それ以降はKeyValuePair<string, object?>なタグデータを入れることができる。 Counter<T> Instrument<T>から派生。 増分を記録していくためのメトリッククラス(総リクエスト数とか)。void Add(T measurement)と、追加でメタデータを与えるオーバーライドが公開されている。 Counterとはいうが、単体で総計値を保存しているわけではないので注意すること。 例: class C1 { static readonly Meter _M1 = new Meter("m1"); // unitとdescriptionは必須ではない static readonly Counter<int> _C1 = _M1.CreateCounter<int>("c1", "unit", "description"); public void Method1() { // processing if(_C1.Enabled) { // 有効ならば適当な値を入れる _C1.Add(1); } } } Histogram<T> Instrument<T>から派生。 単調増加ではない数値(req/sとか)を記録していくためのメトリッククラス。void Record(T measurement)と、追加でメタデータを与えるオーバーライドが公開されている。 Counterと何が違うのかという疑問を持つかもしれないが、単体で見ると、違いは公開メソッドの名前位である。 しかし、後述するMetricsEventSourceでは異なるイベント扱いされるので、可能ならば使い分けた方が良い。 例: class C1 { static readonly Meter _M1 = new Meter("m1"); // unitとdescriptionは必須ではない static readonly Histogram<int> _H1 = _M1.CreateHistogram<int>("h1", "unit", "description"); public void Method1() { // processing if(_H1.Enabled) { // 有効ならば適当な値を入れる _H1.Record(10); } } } ObservableInstrument<T> Instrumentクラスから派生した、pullシナリオ用の派生クラス。 Instrument.IsObservableがtrueになる他、protected abstract IEnumerable<Measurement<T>> Observe()が定義されている。 Measurement<T>は、T ValueとReadonlySpan<T> Tagsで構成される構造体となる。 ObservableCounter<T> ObservableInstrument<T>から派生。 増分を記録しておくためのメトリッククラス。Meter.CreateObservableCounter<T>()で作成され、この時値を返すためのコールバックを指定する。 こちらもCounter<T>同様、総計値を保存してくれるみたいなことはないので注意。 例: class C1 { static readonly Meter _M1 = new Meter("m1"); static int _CachedValue = 0; // unitとdescriptionは必須ではない static readonly ObservableCounter<int> _OC1 = _M1.CreateObservableCounter<int>("oc1", () => _CachedValue, "unit", "description"); public void Method1() { // メソッド呼び出し回数を想定 Interlocked.Increment(ref _CachedValue); } } ObservableGauge<T> ObservableInstrument<T>から派生。 観測時点の値を記録するためのメトリッククラス。Meter.CreateObservableGauge<T>()で作成され、この時値を返すためのコールバックを指定する。 Counter<T>とHistogram<T>の関係と同じく、構造上異なる点は名前位なものだが、MetricsEventSourceでは異なるイベント扱いされる。 例: class C1 { static readonly Meter _M1 = new Meter("m1"); static int _CachedValue = 0; static readonly Random _r = new Random(); // unitとdescriptionは必須ではない static readonly ObservableGauge<int> _OG1 = _M1.CreateObservableCounter<int>("og1", () => _CachedValue, "unit", "description"); public void Method1() { // ランダムな値を入れると想定 _CachedValue = _r.Next(100)); } } 推奨される運用 Meter及び各種Instrumentの名前は一意に 後述するCollector側で監視対象を判別するため Prometheusのガイドライン等、有名どころのドキュメントを参考にするのが吉 Instrumentオブジェクトはprivateないしinternalに 外部から勝手にイベントが追加されるのを防ぐため pushシナリオの場合、イベントを発生させる前に必ずEnabledをチェックする オーバーヘッド、過負荷の軽減のため Collector側の流れ 以下では、Collectorの流れを記述する。 ここで記述するのはインプロセスの話になるので、 実際はCollectorから更に他のCollectorにデータを流すことは十分に考えられることに注意。 MetricListenerの生成 new System.Diagnostics.Metrics.MetricListener()でリスナーオブジェクトを作成する。 MetricListenerも、後述するStart時点でプログラムグローバルなリストに登録されるため、シングルトンで管理するのが望ましい。 どのInstrumentを監視するかの設定 最初に、どのInstrumentを監視対象に入れるかの設定を行う。 MetricListenerにはAction<Instrument, MetricListener> InstrumentPublishedというメンバがあるので、 ここで監視対象に入れるならば、引数として渡ってきたMetricListenerのEnableMeasurementEvents(Instrument, object?)を実行する。 入れない場合はそのまま何もしないようにする。 第二引数には、イベント発生時のコールバックで渡したいオブジェクトを指定する(null可)。 例: var listener = new MetricListener(); listener.InstrumentPublished = (inst, l) => { if(inst.Name == "Abc" && inst.Meter.Name == "Meter1") { // 外側のlistenerは使わない l.EnableMeasurementEvents(inst, null); } }; イベント発生時の処理の仕方の設定 実際にイベントが来た時の処理を設定するには、MetricListenerのMetricListener.SetMeasurementEventCallback<T>(MeasurementCallback<T>)を使う。 MeasurementCallback<T>の型は、void MeasurementCallback<T>(Instrument inst, T measurement, ReadOnlySpan<KeyValuePair<string, object?>> tags, object? state)となる。 各引数の意味は、 Instrument inst: イベントを発生させたInstrumentインスタンス T measurement: イベント発生時で指定された値(Counterならdelta等) ReadOnlySpan<KeyValuePair<string, object?>> tags: イベント発生時に指定されたメタデータ object? state: EnableMeasurementEventsで指定されたstate 例: using var listener = new MetricListener(); listener.SetMeasurementEventCallback<long>((inst, measurement, tags, state) => Console.WriteLine($"{inst.Name}: {measurement}")); Observable*のコールバックでIEnumerable<Measurement<T>>を返している場合は、コールバックが複数呼ばれる。 注意点として、Instrument側のTの型と、SetMeasurementEventCallbackで指定させるTの型は完全に一致させなければならない。 一致しない場合はコールバックが無視される。 監視解除時の設定 Collectorが監視を停止したときに何らかのコールバックを行いたい場合は、 Action<Instrument, object?> MetricListener.MeasurementsCompleted に設定する。 第一引数は監視していたInstrumentインスタンスで、第二引数はEnableしたときに渡したstateインスタンスになる。 監視の開始 SetMeasurementEventCallbackしただけでは監視は始まらない。 監視をスタートさせるには、MetricListener.Start()を行う必要がある。 この時、登録されたInstrumentオブジェクトがあると、InstrumentPublishedで設定したコールバックが呼ばれ、 その中でEnableMeasurementEventsすると監視が開始される。 実際は、Start()しないで直接EnableMeasurementEventsしても監視は開始されるが、通常Instrumentはprivateないしはinternalな オブジェクトで運用することが多いので、Startからのコールバックで設定、というのが想定する使われ方と思われる。 ObservableInstrument系の取得(pullシナリオ) ObservableCounter<T>やObservableGauge<T>は、そのままでは値取得イベントは発生しない。 ではどうすればいいかというと、MetricListener側でvoid RecordObservableInstruments()を実行する。 これを実行すると、Observable生成時に指定したコールバックが呼ばれ、返された値を元にしてSetMeasurementEventCallbackで指定した処理が実行される。 例: using var m1 = new Meter("Meter1"); using var listener = new MetricListener(); listener.SetMeasurementEventCallback<int>((inst, measurement, tags, state) => Console.WriteLine($"{inst.Name}: {measurement}")); // Instrumentの設定等 var oc1 = m1.CreateObservableCounter<int>("observablecounter1", () => 1); // RecordObservableInstrumentsが呼ばれると、"() => 1"が呼ばれ、 // SetMeasurementEventCallbackで設定されたイベントが発生し、"observablecounter1: 1"が出力される listener.RecordObservableInstruments(); 監視の停止 監視を停止したい場合は、 MetricListener.DisableMeasurementEvents(Instrument)を呼ぶ MetricListenerをDisposeする の二種類の方法があるが、DisableするにはInstrumentオブジェクトが必要なので、普通はDisposeを使うことになるだろう。 Insturment側とは違い、Collector側は何らかの終了処理を行いたい場合が多い(接続の解除や各種ハンドルのクローズ等)ので、こちらは生存期間をなるべく 決めておいた方が良いと思う。 監視を停止すると、MeasurementsCompletedで設定したコールバックがInstrumentごとに呼ばれる。 まとめ Metricsについてまとめた。 個人的に気を付けたいのは Instrument側もCollector側も両方シングルトンで動かす 名前は一意に Instrument側でpullシナリオかpushシナリオか決める 扱う値の型を確定させておく 辺りだろうか。 MetricsEventSourceについては今回説明を省略したが、別の記事で書ければいいかなと思う。 dotnet-counterとかで観測するために多分必要になってくるし。 後、このAPI導入のきっかけとなったopentelemetryとの連携についても、気が向けば書くかもしれない。 参考リンク OpenTelemetryにおけるMetricsについての仕様 概念レベルでよくわからなくなってきたらここ OpenTelemetryのdotnet実装 Metricsを使うようになったのは1.2.0-alpha1から dotnet/runtimeのソース Prometheus pullシナリオをサポートする代表的OSSプロダクト
- 投稿日:2021-09-14T21:49:56+09:00
Blazor WebAssembly C#からJavaScriptを実行
はじめに この記事ではC#からJavaScriptを実行するまでの記事です。JavaScriptからC#コードを呼ぶ事も出来ますが別記事で記載します。 JavaScriptコードサンプル サンプルとしてメッセージ表示系の以下のコードをwwwrootの中に入れています。 //アラート export function displayAlert(message) { alert(message); } //Ok,キャンセルアラート export function okCancelAlert(message) { return confirm(message); } //入力アラート export function inputAlert(message, def) { return prompt(message, def); } 使い方 IJSRuntimeをInjectし、JSファイルを設定。 後は、JSの関数を指定して実行出来ます。 @page "/" <h1>Hello, world!</h1> Welcome to your new app. <SurveyPrompt Title="How is Blazor working for you?" /> @code{ [Inject] private IJSRuntime JSRuntime { get; set; } private IJSObjectReference JSCommand { get; set; } protected override async Task OnInitializedAsync() { this.JSCommand = await JSRuntime.InvokeAsync<IJSObjectReference>("import", "./JsCommand.js"); await this.JSCommand.InvokeVoidAsync("displayAlert", "メッセージを表示"); var result = await this.JSCommand.InvokeAsync<bool>("okCancelAlert", "Ok,Cancelメッセージを表示"); await this.JSCommand.InvokeVoidAsync("displayAlert", result); } } 動作
- 投稿日:2021-09-14T17:43:59+09:00
【雑談】あえて今、20年前のJavaを自由に語る
はじめに 「10年前の技術」に関する記事。 こんなテーマなら、さらに20年前の話から始めて、私の思い出話を語ろうかなと思う。 この記事では、私が知っている時代の「Java」を自由に語る。 語ろうとしている年代(1990年代後半)、私は当時小学生であり、当然SEなんて仕事はしていなかったが、密かに「システムエンジニアになりたい」という夢を抱いていた。 そんな私にとってJavaは、「プログラミング言語として学んだ2番目の言語」であり、 長い期間きちんと独学していた言語でもあるので、思い入れがたくさんある。 当時からしてみれば、オブジェクト指向やガベージコレクション、AWT/SwingなどのGUIライブラリ(なお今は死語)は非常に画期的な機能の一つであった。 当時は「Javaは文法的にもかっちょいいし、Cよりは遅いけど色々ソフトウエアが作れる言語なんだぜ~」なんて世間の風潮だったわけ。 実際、当時はJavaアプリケーションやJavaアプレット(これも死語だな)が当時人気だったFlash並みにWebで量産されていき、人気があった。 そんな今、そんな姿はどこへやら、既得権益(Oracle社)に飲まれた言語となっている。 ところで、当時あれほど「ポンコツ」と称されていた(Javaとなんとなく似ているけど全く別物の)スクリプト言語JavaScriptは、今や「開発者が好む言語No.1」になる時代。 まさかこんな時代が来るなんて想像もしていなかった。 注意 ここに出てくるコードは、「20年前~15年前くらいのJavaのコード(を何となく思い出しながら書いたもの)」であり、化石のようなものだ。まず現代の設計に追いつかないものであるので、決して使用してはならない。 歴史に関する詳しい部分は不正確なので、例えば以下の良記事を参照にすること。当時の私は中学生~大学時代で、リアルで仕事しているわけではない。ビジネスや規格競争のような難しいことはようわからずにやってたからな。 本人も当時の状況を他の資料(Wikipediaなど)を使って割と不正確に伝えていますので、「これおかしい」とか「ここもっと調べて欲しい」ってのがあったら是非コメントください。 旧き良きJavaの遺産 20年前、Javaの黎明期 Duke 読者は、こいつの存在「Duke」を知っているだろうか。 ちなみに最近まで名前を知らなかった このマスコットキャラクター、実は既にJDK Beta(1995)の頃には誕生していた。 当然のことながら、よく売れている書籍の中でこいつが描かれていたら、間違いなくJavaの書籍であることを確実に伝えている。 どうやってJavaを勉強したか 私がJavaを勉強したのは、丁度JDK 1.0~1.1くらいの内容をまとめてくれたプログラマーの書籍からである。 今でも間違いなく現存しているホームページに「浅煎り珈琲 Javaアプリケーション入門」がある。(なお化石) 今でこそ、無料有料問わず色んな分野に対する書籍・Webサイト・動画が多量にある時代であるが、 当時の私からしてみれば、プログラミングの基本がここまできっちり理解できるコンテンツは貴重で、 私にとってのデータアーキテクチャの基礎を今でも支えてくれている。 家電とJava 実は開発当初、JavaはIoTの先駆けだったことをご存知だろうか? Javaは、当時として堅牢性を考慮した設計であり、同時にWeb技術への拡張性を兼ね合わせているため、 これをWebSocketを使ってHTTP/HTTPS/FTPプロトコルなどに接続してしまえば、あら不思議IoTモジュールの完成、そんな未来を描いた言語だったのだ。 Web技術の中心となった現代のJavaであるが、Javaは、元々家電の制御のために作られた技術であった。 それは、当時C++はあまりにも柔軟性がありすぎて制御の安定性に問題があり、現代ほど電気製品の制御に向いている技術ではなかったためである。 そこで、JavaはC++の概念を整備して堅牢性を重視して設計された。 Javaの概念は型制約の厳しさを残してC#に引き継がれている。 Java仮想マシン(JVM)と互換性 Java仮想マシンという概念も、現在で言うところのクロスプラットフォーム開発やVirtual Desktop、Dockerなどの仮想化環境の考え方のベースとなる素晴らしいものであった。 一般に、プログラムの実行される方式はインタープリタ方式とコンパイラ方式がある。 インタープリタ方式は、人間の書いたコードをそのまま実行する方式 コンパイラ方式は、人間の書いたコードを機械語に変換する方式 インタープリタ方式が現在の多数の高級言語に該当し、 コンパイラ方式の代表格がC++であることは言うまでもない。 そして、そのJavaは、どちらの方式でもない中間コード方式を取っていた。 これはコンパイルするときに、Java仮想マシン(JVM)に理解できる形式で変換するものであり、 それで生成された中間言語は、JVMが責任を以て実行するので、どのような環境でも実行出来る強みがあったのだ。 当然、家電のような組込み用途においても、このような設計が活用できるのが有意義には違いない。 Swing GUIに関しても規格化が行なわれるような流れとなっていて、JavaにおいてはSwingと呼ばれるライブラリが並んだ。 当時としてはLook&Feelに配慮された綺麗なGUIで、JVMの性質上どのようなプラットフォームでも動くのが強みだった。 実際、Swingの画面とmfcの画面を比較してもらえれば分かると思う。 mfcはC++でWindowsでしか書けないのに、よくも悪くもWindows的なGUIであるのに対し、Swingはこれがあらゆるプラットフォームで動くんだからすごいすごいってなると思う。 Swing GUIの規格化とレイアウトマネージャー さらに、このSwingとはちょっと違う話題だが、レイアウトマネージャの概念も、Javaをユニークにする一つの概念であった。 実はレイアウトマネージャに関してはきちんと対応したC#フレームワークは、WPFまで存在しないのだ。 つまり、当時のWindowsアプリケーションはほぼ例外なく絶対座標系でGUIを表示していた。 現在でもこの絶対座標系を禁止するような正式な解決策はなく、私もWindowsアプリ捨ててブラウザ使いたいって気持ちになってしまう。WPFめんどくせえんだよなあ そしてそのWPFも難しすぎるってんで、それをHTMLとJavaScriptで記載してアプリケーションを作れるようにしたelectron.jsが割といま元気であるけれども、こうした時代に来るまでMicrosoftで作られた製品がユニバーサルデザインを意識した設計になることはなかったのである。 (tkinterやQtを見ていると、もしかすると当時でもユニバーサルデザインに対応したものはあったかもしれませんが。) 特に、以下のようなGridBagLayoutと呼ばれるレイアウトは難しかったが非常に便利であった。 HTMLで言うところの <table> <tr> <td colspan=2>AA</td><td>BB</td> </tr> <tr> <td>1</td><td>2</td><td>3</td> </tr> </table> に対応している機能なのだが、多少その仕組みが複雑とは言えどのようなレイアウトも再現出来たのには一定の効果があったように思える。 また、同じGUIをカードを切るように前後ろに配置するCardLayoutと呼ばれるレイアウトも便利であった。 これも、現在のHTML5ではインラインで代用される概念だと思うが、こういうのがデフォルトについていたJavaはレイアウトの考え方に対しても素晴らしい部分があったと思う。 アプレットとFlash この時代を象徴するJavaで出来たものと言えばアプレットであるだろう。 けれど、実はJavaの中でアプレットは初期の頃はJavaの目玉技術として採用されていたものの、2017年にはもはや表示されなくなるなどの憂き目に遭っている。 それでも、当時はすごかったのだ。個人のHPにJavaアプレットが大量に生成された。 今ではJavaScriptを使えば容易にゲームくらい作れる時代(私にそんな力はないが)だが、当時流行したFlash同様、JavaはプラグインをHTMLに埋め込む形式として流行した。 ↑のような本が山ほどあった時代だったわけです Javaアプレットの最大の強みは、JVMが責任もってブラウザの動きを管理してくれたことになる。 今でこそフロントエンド⇔バックエンドという用語は一般的なものになっているが、当時こうしたゲームを作ろうと思ったらプラグインを埋め込む方式が主流なのであった。つまり基本はフロントエンド側で完結するスタイルなのである。 そんなアプレットのHTMLは、どこでもこんな感じに書籍に書いてあったわけだ。 <html> <title>Java Applet</title> <body> <applet src="myapplet.class" width=640 height=480> </applet> </body> </html> この書き方自体、今のJavaScriptのHTML5に通ずるところがある。 もちろんこんな感じでプラグインのプログラムを1個だけ設置するなんてのはないのだが、idやname、classを使って上手いこと識別子を与えて、そこに表示したいプログラムをダイレクトに組み入れる考え方は似通っているのだ。 Javaはモバイル開発の元祖 Javaといえば、Androidのモバイルアプリケーションの分野で未だに現役であるが、既にもうこの時代から「携帯に使おうか」の流れはあったのだ。本当、Javaは恐ろしい子。 当時のモバイルフレームワークは多数あったが、有名どころは CLDC MIDP(Mobile Information Device Profile) : Vodafone, au ezアプリ用途 Doja : Docomo i-modeアプリ用途 などであろう。 Vodafoneという単語自体が懐かしいな。私は結構長年まで、Vodafoneユーザであった。 私の記憶では、自分自身の演習の目的でMIDPを頻繁に使っていたので、それを思い出すためにソースコードがどんな感じだったか紹介すると、 WebではAppletと言うように、MIDPではMIDletと呼ばれるオブジェクトクラスを定義する。 めっちゃ懐かしいコードだと本気で思った() Javaにおいて、結構Appletが基本で派生したモバイル開発は多いのかなと印象。 また、このことでAppletで学んだ知識がそのまま大体Javaアプリ開発に活かせるのも大きなメリットだった。 一方、下記にもあるようにMIDletからオーバーライドしたメソッドでしか処理が出来ないので、設計に対する自由度はさすがに現代と比べるとかなり劣る。 Appletは、ブラウザのプラグインに埋め込んでいたのでその範囲の中では自由度が高かったのですが、モバイルアプリは「モバイルアプリの一つのウィジェット」だけが切り出されているせいで、出来ることが少なくて「使いにくいな~」という印象はどうしても残った。 またご存知のように、昔のガラケーは画像容量が少ない!ゲームとか作ろうとすると、画像リソースの問題にも限界が・・・。 import javax.microedition.midlet.*; import javax.microedition.lcdui.*; public class HelloMIDlet extends MIDlet implements CommandListener { public HelloMIDlet() { ...(GUI初期化処理)... } public void startApp() { ...(動作start時の処理 MIDletのオーバーライド)... } public void pauseApp() { ...(Pause時の処理 MIDletのオーバーライド)... } public void destroyApp(boolean unconditional) { ...(終了時の処理 MIDletのオーバーライド)... } public void commandAction(Command c, Displayable s) { ...(GUIで登録したEventListenerの処理 CommandListenerから継承)... } } javadocでドキュメント作成を簡単化 この記事をまとめている最中、重要なトピックであるjavadocのことを触れるのを忘れていた。 本当、書けば書くほどJavaのネタが出てくる。それだけ将来性が期待されていた言語なんだろうなって思う Javaを少しでもやっていれば、このAPI仕様書には必ずお世話になっているはずだ。 しかし、私の過去においては若干これはトラウマだ。 継承の概念も少しずつ学んでいく時期だったので、分からないクラスはとにかく索引から探す!!!なんていう無謀な作業に取り組んでいた記憶がある。 当時はひたすら検索をかけまくったので、Ctrl+Fはショートカットキーとして覚えろと言わなくても体が覚えてしまった。 ↑こんなところから探せとか言われたらマジで発狂する(なお現実) さてこのjavadocも、ソースコード管理上、大変画期的な仕組みである。 Sun Microsystems社の社員は、この膨大なJavaのライブラリに一つ一つWebページを用意し、機能説明を準備しただろうか? しかもリリース期間が1~2年という状況で。 当然そんなことはしないわけで、ソースコードにコメントの型を決めてそれを自動生成していたのだ。 javadocでマニュアルに追記する場合、例えばこんな感じで書いたりする (外部サイトより引用) public class Sample01{ /** * サイズの設定 * @param width 幅 * @param height 高さ */ public void setSize(int width, int height){ } } そしてコンパイルするときに、javac -d Sample01.javaなどと書いて実行すると、先ほどのAPI仕様書が自動生成されるのだ。 この考え方は特にJavaのプログラマーにとって本気で感動させられる仕様であった。 (もちろん現在では割と日常的なものであるのだが) API仕様書の作成の作業が一つ減り、コードレビューする際もこのjavadocを吐き出せばレビューが出来るのが素晴らしい。 Collection Framework Collection FrameworkはC++のSTLを真似したものであるが、これもJavaでは独自の仕様となった。 実際、JavaのSTLで採用されているVectorはC++をベースに設定された可変長配列であり、基本になっているようである。 ところが、当時Javaはジェネリクスが存在しなかった。 一応、可変長配列として有名なArrayListなんか ArrayList list = new ArrayList(); と書いていたのだが、型指定の難しさは課題にあったのだ。それで2005年のJ2SE 5.0以降のアップデートでジェネリクスが生まれていた。 ちなみにそういう面では、他言語とは多少出遅れている面もあったと思われる・・・と思いきや、全く同じ時期の2005年にC# 2.0が出来たのだが、この時点でC#はジェネリクスに対応していたようだ。 私の中では、C#でジェネリクスを積極的に扱えるようになったのは、LINQが誕生するC# 3.0だったように思える。 革命的な文法 (1) インスタンスを渡す インスタンスの概念は、C++の左辺値参照が基本となっている。そしてその左辺値参照はどうも1988年に構想され導入されたもので、この概念自体はC++がベースとなっているようだ。 しかし、C++の左辺値参照には「インスタンス」の概念はなく、実体がどこかにいなければそのデータは無効となる。 参照渡し(C++) // originalはSomeClass型の実体のあるデータ // 参照は常に実体を持たないといけない。 SomeClass& obj_ref = original; またインスタンスに近い概念であるポインタは、nullptrを許容するのでそれが制御上の問題に繋がる。 ポインタ渡し(C++) // nullptrが許容されてしまう SomeClass* obj_ref = nullptr; // つまり、途中でこのような処理がなければ実行時例外が起きる if( obj_ref == nullptr) { ...(処理)... } // originalはSomeClass型の実体のあるデータ // 参照は常に実体を持たないといけない。 obj_ref = &original; ところで、Javaのルールに基づいてインスタンスを発明したことで、抜本的にこの問題が解決されているのだ。 つまり、インスタンスはそこでは常に参照を渡し続けると出来るのだ。 インスタンスを渡す SomeClass obj = new SomeClass(); // ☆ obj2を生成するときに、objを参照渡し SomeClass2 obj2 = new SomeClass2(obj); だが私の印象だが、これが原因で、当時、C言語/C++を中心に開発してきたエンジニアは、JavaやC#、objective-Cなどのオブジェクト指向型の言語で挫折する傾向が強かったように思う。 ☆の部分で、まさかobjが変更される可能性があるとは思うまい。C++の言語的には、 SomeClass2* obj2 = new SomeClass2(obj); と書かれりゃ、 class SomeClass2 { public: SomeClass2(SomeClass obj) { ...(処理)... } ...(以下省略) }; と勘違いする人と、 class SomeClass2 { public: SomeClass2(const SomeClass& obj) { ...(処理)... } ...(以下省略) }; と何となく意識づけ出来る人と分かれてしまうからだ。 その対策として、C#では値クラスが導入されたり、C++でも構造体とクラスは別の役割をするように調整されたりしたわけだが、私の印象ではバリバリポインタを使ってくるエンジニアほど、ここのところは弱いという印象があった。 一つの障壁になってしまっていたのが、悲しい。 革命的な文法 (2) インタフェースとポリモーフィズム また、Javaにおいてインタフェースクラスも重要な文法表現である。 C++では、多重継承がサポートされている。 class SubClass : public SuperClassA, public SuperClassB { ...(実装)... }; この多重継承の最大の利点は、様々なクラスの型変換を許容することにある。このように書けば、 // originalはSuperClassAで生成されたオブジェクト auto obj1 = dynamic_cast<SuperClassA*>(original); と書くことを許容するが、このdynamic_castはSuperClassB*に対してはnullptrを返却する。 それをJavaでは、 一般的なクラスの多重継承は禁止とする。 多重継承はインタフェースのみ許容し、インタフェースに存在するメソッドは全て抽象化され、オーバーライドしないといけない。 のルールを追加した。このようにしたことで、 interface IntarfaceA { ...(実装)... } interface IntarfaceB { ...(実装)... } public class SubClass extends SuperClass implements InterfaceA, interfaceB { ...(実装)... } と書けるようになり、さらにポリモーフィズムに破綻せずに、 // originalはSubClass型のインスタンス interfaceA obj_a = original; interfaceB obj_b = original; とする記載を許容することが出来たのだ。 こうすることでの最大のメリットは、オブジェクト指向の基本的な考え方であるis-a関係を破綻させていないところにある。 GoFのデザインパターンも、is-a関係あってこそ成立するものである。デザインパターンがオブジェクト指向の基礎教材となり、色々なデータ構造を表現できるようになったのは言うまでもない。 これは大きなポイントであり、古きJavaを語る上でマルチスレッドやEventListenerを使うときに重要な部分となった。 EventListener型の発明 ~ Delegate型との大きな違い ~ このEventListenerを聞くのは、現在では一部のJavaScriptネイティブでやっている人しかいないと思われるが、これも非常に画期的な仕組みだった。どう画期的かと言うと、イベントリスナークラスを定義してそのGUIに登録するだけでイベントの挙動を表せてしまうわけだ。 確かに、この仕組みはややこしいものであったが、このことによりイベントを共通のオブジェクト化する考え方が一般的になったのもある。 GUIにActionListenerと呼ばれるインタフェースを用意しておき、そのインタフェースを継承したクラスを開発者が作り、それを登録する。 例えば同期のC#ではこうした設計をするとき、delegateを採用するだろう。Windows Formsの例であるが public partial class FormMain : Form { public FormMain() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { MessageBox.Show("ボタンがクリックされました!"); } } という感じで、あくまでデザインビューワの中にbutton1_Clickを仕込ませて構成する。 ここで、フォームに存在するボタンGUIは別にメソッドOnClickを用意しておき、実際はdelegateでbutton1_Clickを実行するように仕事を投げる。 この仕組みは一見快適で、逆にWindowsアプリケーションの仕組みが楽すぎて、他のフレームワークに移行できない人が増えている原因になっているのだが、もう一つ大きな問題を抱えている。 それは「Formの実装の中に処理が潜んでしまうこと」だ。 つまりこうしたコードを書いてしまうと、MVPパターンで言うところのフォームのViewの中に、ボタンイベントであるPresenterの処理が混在してしまう。 技量がなくモジュール分割の習慣がついていないエンジニアなら、下手したらそのPresenterの中にModelとViewを一緒にごちゃ混ぜにして書く惨事すら招くのだ。もはやそんな状況では、MVPモデルなんて言うものは存在しないだろう。 それをJavaではEventListenerの形式で回避していて、 リスナークラス class MyActionListener implements ActionListener { MyApplet applet; public MyActionListener(MyApplet applet) { this.applet = applet; } //ActionListener インタフェースの実装 public void actionPerformed(ActionEvent e) { ...(Modelの処理やViewのプロパティに該当する値を変更する)... } } GUIクラス public class MyApplet extends Applet { private Button btnBlue; private MyActionListener actionListener; ... (略)... public void init() { btnBlue = new Button("blue"); actionListener = new MyActionListener(this); btnBlue.addActionListener(eh); } } こうすることで、少なくとも(Viewの一部+Modelの一部の処理)をListenerが、(Viewの処理)をAppletが担ってくれるようになるので、 設計が分かっていれば少しは上手く出来るようになる。 ただ、多分MVPモデルをきちっと意識して設計した人がいたかはかなり怪しい。解説書を読んでもそういうことには触れなかった。 実際、WPFが出てからMVVMに対する布教が進んできたという印象があるので、当時のプログラマーも要素分割の概念があったかはちょっと分からない。私は(当時)あくまでホビーユースだったから 10年前の自分に伝えたいこと 実は、こうした20年前の技術があったり、色々と画期的な設計を目指したJavaであったが、ある時期からそのJavaをやるのをやめたのであった。 その理由のきっかけは色々あるが、 Java 6からのアップデートがあまり行われなかったこと プラグインを使う方法がセキュリティセーフではなかったこと Oracle社の買収 などが挙げられる。 まあ、アルゴリズムの分野に関して言えばJavaで作られた成果物を結構見るし、それなりに上手く動くので、教育的な部分に関してはそれなりに使えた面はあったけれど、既に当時から限界な感じはちょっと見えていたのであった。 また、Javaを躍進させたモバイル開発の業界は確かに今も堅調だと思うのだけど、 少なくともPCを中心にやっていく私の世界だと、当面手を引かないといけない状況になっていたのも事実だ。 それが現在、安定期も終わり徐々に衰退期に近づいているような。 Oracle社が(アップデートを重ねることで)独自のDBシステムと共に既得権益化したこと、 openjdkはGPL化され、Javaで書いたコード以外のライセンス汚染が問題になること を誰が想像しただろうか? もし、私が10年前の自分にいえるとすれば、 「今すぐモバイル開発にさらに時間を集中させるか、C++の制御技術やパターン認識の知識をもっと深めろ」 と言っているかもしれません。 ちなみに 当時の記憶ですが、2008年頃、学校で勉強していたころ、Javaベースのモバイルウィジェット開発のフレームワークを作った「Jig.jp」さんというところがあります。 2008年といえば、Java SE 6が生まれた頃だったと思います。 今でこそJavaベースのモバイルアプリ開発はごく日常的なことだと思いますが、 よーく考えたら、2008年の時点でJavaがモバイルウィジェットの概念の発想が出来るかもと注目した人は、なかなか素晴らしい着眼点を持っていたかもしれません。
- 投稿日:2021-09-14T16:07:08+09:00
Unity ScalerPanelベースサイズに応じたサイズ変更
パネルのサイズを固定にして、その配下に固定で、画像やテキストを配置。 パネルのスケールを変更するのみで、比率に応じた倍率にする。 ここでは640x480に固定。倍率は小さい方に合わせる。画面いっぱいに黒画面などを置いて、縦の空きや横の空きをごまかす。 using System.Collections; using System.Collections.Generic; using UnityEngine; public class PanelScaler : MonoBehaviour { void Awake() { //Application.targetFrameRate=20; ScaleChange(); } void ScaleChange(){ const float fixed_width=640f; const float fixed_height=480f; var sw=Screen.width/fixed_width; var sh=Screen.height/fixed_height; var scale=Mathf.Min(sw,sh); Debug.Log(scale); gameObject.transform.localScale=new Vector3(scale,scale,1); } } パッケージ https://github.com/gnjo/UnitySamples
- 投稿日:2021-09-14T11:58:54+09:00
T4テンプレートでらくらくソースコード自動生成(類似クラス大量生成編)
あるインターフェイスを実装するクラスをたくさん作りた~い! 皆さんはあるインターフェイスや抽象クラスを実装するクラスを大量に作りたいとき、どうやっていますか? え、そんなことしない? 例えばこんなケースがあると思います! WPF,WinFormsなどのGUIで、共通の機能を持つ複数の画面部品を作る場合 下図は弊社で作成しているアプリのフッターにあるファンクションボタンです。各ボタンはIFunctionButtonインターフェイス(を実装する抽象クラス)を実装する個別のクラスとしています。 API名や定義から通信処理を自動実装したいとき Excel設計書から自動実装してもいい(宣伝)んですが、APIの定義とコードは直接作成/編集し、APIリファレンスをXMLドキュメントで公開するというやり方もありますよね。Excelをいじらなくてよいぶん、体感ではこちらのほうが開発体験はいいです。 2~3種類ならコピペで手っ取り早く作ってもいいのですが、こういった部品郡はだいたい10や20を超えてくるので手実装するのは大変なわけです。しかも後から機能を生やしたり数を増やしたりしようとするともう手がつけられません!大量のクラスにコピペで修正を反映していくなんて地獄の作業はやりたくないですよね? そこで、T4テンプレートを使って静的にソースコードを作ろう!というのが本稿の目的となります。 用意するもの Visual Studio どうやらVS2012くらいから使えるようですが、本稿ではVS2019で説明します。頑張ればVSCodeでも使えるらしいです。 デザイン時テキストテンプレート テンプレートファイルの追加 プロジェクトを右クリックし、「新しい項目の追加」からテキスト テンプレートを選択します。似た名前にランタイム テキスト テンプレートというものがありますが、これは文字列生成するクラスを作るテンプレートです。お間違えないようにしてください。 追加すると以下のファイルが作られます。 TextTemplate1.tt <#@ template debug="false" hostspecific="false" language="C#" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ output extension=".txt" #> このテンプレートを追加すると、ソリューションエクスプローラーで追加したテンプレートファイル(.tt)と同名の.txtファイルが生えます。これが生成先のファイルになります。試しに適当な文字列を.ttファイルに記述して保存すると、自動で.txtファイルに内容が反映されます。 テンプレートファイルの末尾にあるoutput extensionパラメータを変更すると、.csファイルや.xmlファイルなど任意の拡張子での出力が可能です。コードを生成するなら.csや.vb等の適切な拡張子に設定しましょう。.csファイルにすると、生成されたコードは普通に作ったコードと同じように使ったりブレークポイントを置いたりすることができます。 ちなみに/を指定するとファイルが作られないようです。ファイル名に/が使えないからでしょうか? なお、開発者が誤って自動生成したコードを編集しないように注意書きを書いておきましょう。バカは.ttファイルを無視して修正したりしやがります出力内容の先頭に<auto-generated>タグでコメントを書いておけばよいでしょう。 TextTemplate1.tt <#@ template debug="false" hostspecific="false" language="C#" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ output extension=".cs" #> // <auto-generated> // THIS (.cs) FILE IS GENERATED BY FunctionButtons.tt // DO NOT CHANGE IT. // </auto-generated> デザインの前に デザインファイルを編集する前に、自動生成したいコードがどんなコードかを具体化しておきましょう。ゴールを先にコード化しておかないと、テンプレートデザイン上でどこが動的に変わる部分かがわからなくなります。 今回のゴールは以下とします。 目的 WPFのMVVMアプリでファンクションボタンを個別にたくさん作る 前提条件 MVVMフレームワークにPrismを使用する ボタンの処理はIFunctionButtonを実装する抽象クラスFunctionButtonBase<T>に実装する TはPrismのPubSubEventを継承したクリックイベントのクラス。これも、共通処理を抽象クラスClickEventBaseに実装する T4で作るゴールのコード 抽象クラスFunctionButtonBase<T>を継承する各ボタンのクラス 各ボタンが使用するClickEventBaseを継承したイベントクラス 前提条件を満たすコードの実装 先に必要なコードを書いておかないと、T4で自動生成してもコンパイルエラーになります。必要なIFunctionButton, FunctionButtonBase<T>, ClickEventBase を実装しておきます。 IFunctionButton.cs namespace MyApp.FunctionButtons { /// <summary>フッターに配置されたファンクションボタンの機能を規定するインターフェイスです。</summary> public interface IFunctionButton { // 本当はViewを制御するためのIsEnabledプロパティ等を持っていますが省略 /// <summary> /// ボタンのクリックイベントを発行します。ただし、ボタンが押下可能な状態でなければ何もしません。 /// </summary> void Click(); } } FunctionButtonBase.cs using Prism.Events; using Prism.Mvvm; namespace MyApp.FunctionButtons { /// <summary> /// フッターのファンクションボタンの大まかな共通機能を提供する抽象クラスです。 /// </summary> public class FunctionButtonBase<TEvent> : BindableBase, IFunctionButton where TEvent : ClickedEventBase, new() { // 本当はClickに紐づくReactiveCommand等を持っていますが省略 private readonly IEventAggregator _eventAggregator; internal FunctionButtonBase(IEventAggregator eventAggregator) { _eventAggregator = eventAggregator; } public void Click() { _eventAggregator.GetEvent<TEvent>().Publish(); } } } ClickEventBaseにはボタン押下時にどのファンクションボタンが押されたのかログ出力する仕組みを仕込んでおきます。各ボタン用の具象クラスはFunctionNameForLogプロパティを実装してログ出力時のボタン名を管理するものとしました。 ClickEventBase.cs using Prism.Events; namespace MyApp.FunctionButtons { /// <summary>ファンクションボタンのクリックイベントを規定するクラスです。</summary> public abstract class ClickedEventBase : PubSubEvent { /// <summary>ロガー</summary> private static NLog.Logger Logger = NLog.LogManager.GetCurrentClassLogger(); /// <summary>ログ出力に使用する機能の名称を取得します。/summary> public abstract string FunctionNameForLog { get; } /// <summary>ボタンクリック時イベントを発行します。</summary> public override void Publish() { Logger.Info($"[ファンクションボタン押下] {FunctionNameForLog}"); base.Publish(); } } } 最終的に、各ボタンごとに以下のコードを作ることをゴールとします。 AnyButtons.cs using System; using Prism.Events; namespace MyApp.FunctionButtons { /// <summary>ほげボタン</summary> public class HogeButton : FunctionButtonBase<HogeButtonClickedEvent> { internal HogeButton (IEventAggregator eventAggregator) : base(eventAggregator) { } } /// <summary>ほげボタンクリックイベント</summary> public class HogeButtonClickedEvent : ClickedEventBase { public override string FunctionNameForLog => "ほげ"; } } デザイン 完成形のコードが出来たなら、後は完成形のうちどこが動的に変わるのかを抜き出してT4テンプレートのデザインに落とし込むだけです。 上のコードで考えると、以下が変わる内容になります。 ボタンクラスの<summary>タグ内の「ほげ」 ボタンクラス名のHoge FunctionButonBase<T>のTに指定するクリックイベントクラスのHoge コンストラクタのHoge クリックイベントクラスの<summary>タグ内の「ほげ」 クリックイベントクラス名のHoge FunctionNameForLogプロパティが返す値の「ほげ」 つまり、「ボタン名のHoge」と「ボタンを説明するテキストのほげ」が動的に変わる部分であることがわかります。この2パラメータをボタンの数だけループさせればよいことになります。 デザイナ上で変数等を定義したい場合、<# ~~ #>ブロックを使用可能です。また、関数や構造体など内部で使用したいコードはテンプレートファイルの末尾に<#+ ~~ #>ブロックを記述することで実現可能です。具体例を以下に示します。2パラメータを構造体とし、配列で作るボタンの数を管理しています。 なお、<# ~~ #>にはFunc<T>型で関数を定義したりLinQを使って変数をセットするなど大概のC#コードがかけるのですが、タプルが使えなかったりするなど、よくわからないことになっています。 FunctionButtonTemplate.tt <#@ template debug="false" hostspecific="false" language="C#" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ output extension=".cs" #> <# // ファンクションボタン定義。"更新"等はログに出す以外で使わないので日本語べた書きとする var buttons = new [] { new ButtonType("Reload","更新"), new ButtonType("Previous","1つ戻る"), new ButtonType("Next","1つ次へ"), ︙ new ButtonType("Exit","前画面に戻る/アプリケーション終了"), }; #> <#+ internal struct ButtonType { public string Name; public string LogText; public ButtonType(string a, string b) { Name = a; LogText = b; } } #> ここまでできたらbuttons配列をループで回してコード生成するのみです。先のコードと一部重複しますが、実際に作ったテンプレートファイルをお見せします。Qiitaではハイライトが効かないので一見するとわかりにくいですが、やっているのは単純にループしながら変わるところに値を埋め込んでいるだけです。なお、内部でIFunctionButtonの型制約をかけたジェネリックメソッドで使い倒しているところがあるため、空のコンストラクタもあわせて作っています。こういった機能拡張をデザイナの修正だけでできるのが自動生成の強みですね。 FunctionButtonTemplate.tt <#@ template debug="false" hostspecific="false" language="C#" #> <#@ assembly name="System.Core" #> <#@ import namespace="System.Linq" #> <#@ import namespace="System.Text" #> <#@ import namespace="System.Collections.Generic" #> <#@ output extension=".cs" #> <# // ファンクションボタン定義。"更新"等はログに出す以外で使わないので日本語べた書きとする var buttons = new [] { new ButtonType("Reload","更新"), new ButtonType("Previous","1つ戻る"), new ButtonType("Next","1つ次へ"), ︙ new ButtonType("Exit","前画面に戻る/アプリケーション終了"), }; #> // <auto-generated> // THIS (.cs) FILE IS GENERATED BY FunctionButtons.tt // DO NOT CHANGE IT. // </auto-generated> using System; using System.ComponentModel; using System.Windows; using System.Reactive.Linq; using Prism.Events; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace MyApp.FunctionButtons { <# foreach (var button in buttons) {#> /// <summary><#= button.LogText#>ボタン</summary> public partial class <#=button.Name#>Button : FunctionButtonBase<<#=button.Name#>ButtonClickedEvent> { internal <#=button.Name#>Button(IEventAggregator eventAggregator) : base(eventAggregator) { } /// <summary>このコンストラクタはジェネリック制約のために生成しています。使用しないでください。</summary> [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("このコンストラクタはジェネリック制約のために自動生成しています。使用しないでください。", true)] public <#=button.Name#>Button() : base(null!) { } } /// <summary><#= button.LogText#>ボタンクリックイベント</summary> public partial class <#=button.Name#>ButtonClickedEvent : FunctionButtonClickedEventBase { public override string FunctionNameForLog => "<#=button.LogText#>"; } <#}#> } <#+ internal struct ButtonType { public string Name; public string LogText; public ButtonType(string a, string b) { Name = a; LogText = b; } } #> できあがるのは以下になります。 FunctionButtonTemplate.cs // <auto-generated> // THIS (.cs) FILE IS GENERATED BY FunctionButtons.tt // DO NOT CHANGE IT. // </auto-generated> using System; using System.ComponentModel; using System.Windows; using System.Reactive.Linq; using Prism.Events; using Reactive.Bindings; using Reactive.Bindings.Extensions; namespace MyApp.FunctionButtons { /// <summary>更新ボタン</summary> public partial class ReloadButton : FunctionButtonBase<ReloadButtonClickedEvent> { internal ReloadButton(IEventAggregator eventAggregator) : base(eventAggregator) { } /// <summary>このコンストラクタはジェネリック制約のために生成しています。使用しないでください。</summary> [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("このコンストラクタはジェネリック制約のために自動生成しています。使用しないでください。", true)] public ReloadButton() : base(null!) { } } /// <summary>更新ボタンクリックイベント</summary> public partial class ReloadButtonClickedEvent : FunctionButtonClickedEventBase { public override string FunctionNameForLog => "更新"; } /// <summary>1つ戻るボタン</summary> public partial class PreviousButton : FunctionButtonBase<PreviousButtonClickedEvent> { internal PreviousButton(IEventAggregator eventAggregator) : base(eventAggregator) { } /// <summary>このコンストラクタはジェネリック制約のために生成しています。使用しないでください。</summary> [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("このコンストラクタはジェネリック制約のために自動生成しています。使用しないでください。", true)] public PreviousButton() : base(null!) { } } /// <summary>1つ戻るボタンクリックイベント</summary> public partial class PreviousButtonClickedEvent : FunctionButtonClickedEventBase { public override string FunctionNameForLog => "1つ戻る"; } /// <summary>1つ次へボタン</summary> public partial class NextButton : FunctionButtonBase<NextButtonClickedEvent> { internal NextButton(IEventAggregator eventAggregator) : base(eventAggregator) { } /// <summary>このコンストラクタはジェネリック制約のために生成しています。使用しないでください。</summary> [EditorBrowsable(EditorBrowsableState.Never)] [Obsolete("このコンストラクタはジェネリック制約のために自動生成しています。使用しないでください。", true)] public NextButton() : base(null!) { } } /* 以下略 実際にはこれが15クラスぶんあります */ } デザイン時生成したコードの修正について 生成されるのはあくまでファイルなので、生成したコードを直接編集することも可能です。これを活用すると、コードを編集して動きを確認してからT4のデザインを変更する、といったことができます。これはT4テンプレートが静的にコード生成するタイミングが以下になるためです。 .ttファイルを編集して保存したとき メニューバーから[ビルド]→[すべての T4テンプレートの変換]を実行したとき ソリューションエクスプローラーから.ttファイルを右クリックして[カスタム ツールの実行]を選択したとき 当たり前ですが.ttファイルを保存した瞬間に直接編集した内容は消えるので、自動生成したコードは原則いじらないようにしましょう。手実装するコードの雛形を作りたい、というようなケースなら自動生成後にリファクタリング機能でクラスごとに別ファイルに抜き出すことを推奨します。あるいは、自動生成するクラスやメソッドをpartialにして機能拡張を別ファイルで行う方法も考えられます。 自動生成したコードのテスト 自動生成したコードそのもののテストは私は行っていません。理由は以下によります。 生成したコードがいなくなることがありえる 本稿の例ではそのようなケースはないですが、buttons配列から消せば生成されなくなります。そのようなコードをテスト対象にすることは不適切です。 生成したコードそのものに機能をもたせることが少ない 本稿の例ではボタンが持つべき機能はすべてIFunctionButtonに規定されており、すべての実装はFunctionButtonBase<T>とTとなるClickEventBaseが担っています。ということはこれらのクラスをテストすればよいため、各コードのテストはしていません。 大量生成するコードに大量の実装をもたせても見通しが悪くなるのも理由の一つです。 まとめ Excel設計書から作る場合と比べて(宣伝)、すべての作業がVisual Studio内で完結するので、このテンプレートを作る作業は以外と短いです。T4の制御構文(<# for(~~~) { } #>とか<# if (~~~){ } #>)に慣れると1日程度で完成形まで持ってこれると思います。 コピペは便利ですが、一度修正があるとすべてのコードも修正内容をコピペして回らないといけないなど、すごく大変だと思います。正直私はコピペコーディングは悪くらいの気持ちでやっている1ので、テキストテンプレートを活用することで品質の担保とサボり作業の効率化を行っています。皆さんもぜひテキストテンプレートを活用して定時帰りしましょう! よいT4テンプレートライフを! コピペする量と回数にもよります。今回の例もボタンの数が十分少ないならコピペして回るほうが工数が短いです。何回も生成するかどうか/機能の修正・拡張がありそうかどうか/生成対象を増やすかどうか、でコピペコーディングするか自動生成するかを決めるとよいと思います。 ↩
- 投稿日:2021-09-14T07:14:11+09:00
App.config
asp.net coreだと、appsetting.jsonがテンプレートに入ってくるので、設定ファイルは、json一択かと思っていましたが XMLで書くApp.configもサポートとされているのですね。 今頃気づきました CofigrationManagerクラスが存在するので、同じように使えるのですね。
- 投稿日:2021-09-14T03:16:48+09:00
inheritdocを調査する
はじめに Visual Studio 2019 version 16.4 Release Notes によると, 継承関係にあるクラスやメソッドには, 親のコメントを引き継いでクイック情報窓に表示するようになりました. inheritdocタグで明示することもできます. inheritdocタグを明示した場合, 警告CS1591は出力されません. また, 16.4より前のリリースと出力されるXMLドキュメントは変わりません. いつからかはわかりませんが, inheritdocタグの補完がされるようになりました (Visual Studio 2017では候補にでない). ここから, コメントからドキュメントを生成することに興味のない方には関係がありません. inheritdocタグに対応, だけ見て"16.4より前だから対応していないらしい, コメントもそのままコピーしよう", とならない方がよいでしょう. Sandcastle Sandcastleは, C#コンパイラが生成したXMLからドキュメントを生成するシステムのひとつです. 継承先にコメントがない場合は取り込む, ある場合はリンク先を見てね, のような動作をするようです. コメントから直接イメージできるドキュメントにはならない印象です. namespace Inheritdoc { /// <summary> /// Something /// </summary> public interface ITweetAlert { /// <summary> /// This method does something0 /// </summary> void Method0(); /// <summary> /// This method does something1 /// </summary> void Method1(); } /// <inheritdoc /> public class TweetAlert : ITweetAlert { public virtual void Method0() { } /// <inheritdoc /> /// <summary> /// TweetAlert implements Method1 /// </summary> public virtual void Method1() { } } } Javadoc 何も考えなくても美しいドキュメントが生成される印象です. /** * Something */ interface ITweetAlert { /** * This method does something0 */ public abstract void Method0(); /** * This method does something1 */ public abstract void Method1(); } class TweetAlert implements ITweetAlert { @Override public void Method0() { } /** * {@inheritDoc} * TweetAlert implements Method1 */ @Override public void Method1() { } } Javadocは継承先にコメントがない場合, 親のコメントを継承しますが, 注釈をつけてくれます. Method0 public void Method0() インタフェースからコピーされた説明: ITweetAlert This method does something0 定義: Method0 インタフェース内 ITweetAlert Method1 public void Method1() This method does something1 TweetAlert implements Method1 定義: Method1 インタフェース内 ITweetAlert Doxygen Doxygenでは, 設定 INHERIT_DOCSをYESにすると, メソッドのコメントを親から引き継ぎます, クラスのコメントは引き継ぎません. デフォルトでは, コメントのない要素のドキュメントは生成しなかったりと設定を細かく行う必要があります. copydocコマンドで明示することもできます. copydocにメソッドを指定する場合, オーバーロード解決のために引数リストも指定します. /** @brief Something */ class ITweetAlert { public: /** @brief This method does something0 */ virtual void Method0() =0; /** @brief This method does something1 */ virtual void Method1() =0; }; /** @copydoc ITweetAlert */ class TweetAlert : public ITweetAlert { public: virtual void Method0() override { } /** @copydoc ITweetAlert::Method1() TweetAlert implements Method1 */ virtual void Method1() override { } }; TweetAlert::Method1()のドキュメントは次のようになります. copydocコマンドでコピーしたドキュメントの次に, ピリオドが追加されます. 理由は不明です. This method does something1. TweetAlert implements Method1 Implements ITweetAlert. まとめ コメントからドキュメントを生成するシステムのほとんどは, 継承も考慮して設計されています. オーバーライドしているかはナウい言語ではコードに書くことができますから, タグを明示的に書くかどうかは好みだと思います. 実装側にコメントを書かなければならないとしたら, 設計ミスの匂いがしますし. 複数個所に同じコメントを書くことはやめましょう.