見出し画像

Art of Auto Software Development Deliverables Generation

ソフトウェア開発成果物自動生成教本

ソフトウェア開発において、ある情報が確定すれば、決まった手順、手続きに従って、ソフトウェア開発の成果物が作成可能な場合、それを自動化する方法を、具体的なサンプルを使って解説していきます。

はじめに

ビジネスでソフトウェア開発を行う場合、最終的に必要な実行ファイルだけでなく、ソースコードやソースコードをビルドする環境、ソフトウェア実行に関する各種設定ファイル、インストールスクリプト、開発プロセスで規定された様々な仕様書や資料、レポート、等々、膨大な中間成果物を作成しなければなりません。
これらの成果物は、それぞれ適切なタイミングで、元となる情報を使って、あるいは、それらを元に脳みそを振り絞ってアイデアを抽出し、あらかじめ定められた内容やフォームに従って、作成していきます。
ソースコードの作成においは、サードパーティーが提供(自社開発も含め)しているフレームワークライブラリを使う場合でさえ、様々なコードや設定ファイルを、フレームワークの規定に従って作成しなければなりません。
また、人が行う作業にはミスがつきものなので、間違いがないかチェックも必要です。加えて、予め定められた内容やフォームがあるということは、事前に社内の誰かがそれを作って、継続的にメインテナンスをしながら、開発部門に周知徹底を続けていることになります。これらを進化したIT技術を活用せずに、全て手動で行っていたとしたら、ソフトウェア開発には膨大な人と手間と時間がかかってしまいます。
ソフトウェア開発を効率化し、より早く変化を続けるビジネスニーズに対応していくためには、定型的なルールに従って作業が進められる部分はコンピュータに任せて自動化し、人が脳髄を絞らなければならない作業に専念させることが、有効な手段の一つとなります。
作成しなければならない成果物を注意深く観察すると、多くの成果物が、その成果物を作成するのに必要な情報を元に、必要な成果物を作成するための定型的なルールに則って作業が進められる部分と、実際に脳髄を絞って考えなければならい非定型な作業の部分の組合せであることがわかります。また、成果物は、中身と見栄え(書式)からできています。見栄えを整える部分は、ほとんどの場合、定型的なルールに則って作業を進めることができます。この状況は、ソースコードに限らず、様々な成果物も一緒です。
詳細は、「Practices of Software Engineering」で解説しているので、そちらを参照してもらうことにして、本稿では、具体的な例を使って、コード自動生成環境を構築していく手順を紹介し、それぞれのステップでのポイントを解説していきます。

コード自動生成環境構築の手順

コード自動生成環境は、以下の手順で作成していきます。

  1. 自動生成のスコープを決める

  2. 実際に動くサンプルコードを作成する

  3. サンプルコードを定型部分と非定型部分に分類する

  4. 定型部分をフレームワークライブラリ化する

  5. 非定型部分に対応するためのルールを決める

  6. ビルド環境を構成するファイル群の定型部分と非定型部分に分類し、ルールを決める

  7. コード自動生成アプリ化する

  8. 手動で行う作業を都度自動化していく

開発工程の自動化においては、全ての成果物が全てファイルとしてコンピュータ上で保存・共有可能で、かつ、なんらかの IT技術の手段で、そのファイルの中身を構造的にアクセスできることが大前提です。たとえ、Word や PDF で電子化されていても、自然言語で漫然と書かれたドキュメント群は構造的にはアクセスできないので、自動化の過程で改善していくとよいでしょう。
また、開発プロセスで規定されている各工程で作らなければならない成果物が、別の工程で作られた成果物の見栄えだけを変化させているだけの場合は、開発プロセス自体の見直しも必要です。
開発工程の自動化のそもそもの目的は、開発プロセスの効率化による時間短縮です。自動化環境を導入したことによって、従来より更に開発者の手間を増やしていしまうとしたら本末転倒です。下の項目の後者が前者より大幅に大きいという条件が成り立っていることを、常に意識しなければなりません。

  1. 自動化環境の開発・運用コスト + 現場側で生じる使うための追加コスト

  2. 自動化導入で削減されるコスト + 自動化により浮いた時間で見込める利益

ここで挙げた後者の項目の二番目の利益は、例えば、自動生成によって生み出された余剰時間で、開発者のスキルアップのための学習時間に充てられる、別の作業が行えるので、更に多くの機能の実装ができるなどを意味します。もちろん、日々忙しい時間で疲弊した頭と体をリフレッシュする時間に充てても構いません。これも開発組織の開発能力を高める重要なタスクです。いずれにせよ、高度なスキルを持つ開発者の貴重な時間を自動化できるようなダムな仕事で浪費するのは非常にもったいないことです。

ソフトウェア開発成果物自動生成環境の構築

以上、能書きはそれぐらいにしておいて、早速、具体的な事例を使って自動生成環境の構築を解説していきます。
本稿では、Azure IoT Hub に接続する IoT 機器上で動作するアプリケーションにおいて、DTDL で記述された IoT Plug & Play の定義を元に、定型的に生成可能なプロジェクトリソースとソースコード群を自動生成する環境を構築します。

自動生成のスコープを決める

先ずは、どの様な自動化を行うのか、概要を決めます。サンプルでは、Azure IoT Hub に接続する IoT 機器上で動作するアプリケーションを自動生成します。
IoT ソリューションと連動して運用される IoT 機器のアプリケーションは、以下のモジュールから構成されます。

  • 各 IoT 機器固有の制御ロジック

  • サービス側との通信ロジック

前者は、例えば、IoT 機器が工作機械なら、予め設定されたコマンドに従った、センサーや画像から収集した情報を元にしたアクチュエータのフィードバック制御や、また、IoT 機器が自動車であれば、運転状況のセンシングや自動運転制御等、IoT 機器の種別に応じた固有の役割を担う部分です。この部分は、それぞれのドメイン固有の主題領域の実装なので、今回の自動化の部分には含めません。
一方、後者は、接続先のサービスが Azure IoT Hub の場合は、Azure IoT Hub へのセキュアな接続、Azure IoT Hub と IoT 機器間の双方向通信、それらの処理の実行状況の補足等を担う部分です。この部分は、接続先のサービスが決まっていて、かつ、この部分の実装では、マイクロソフトがリリースしている Azure IoT Device SDK を使った実装が一般的なので、パターン化されたロジックが相当数含まれるので、自動化対象の候補になります。
更に後者の部分は、完全にパターン化できる部分と、IoT 機器毎の要件で変わる部分から構成されます。冗長なので以下、Azure IoT Hub を”クラウド”、IoT 機器を”デバイス”と省略します。Azure IoT Hub との接続と双方向通信では、

  • クラウド への接続

  • デバイスからクラウドへのテレメトリーデータ送信 (D2C Message)

  • クラウドからデバイスへのメッセージ送信 (C2D Message)

  • クラウドからデバイスへの設定情報通知(Device Twins Desired Properties)

  • クラウドへのデバイスからの状態・設定通知(Device Twins Reported Properties)

  • クラウドからのデバイスのコマンド起動 (Direct Method)

  • デバイスからの大きなデータの一括アップロード

の、7種類の機能があります。一番最初の項目は、それぞれのデバイスで接続に必要なセキュリティ情報は変わりますが、それを用いた接続ロジックは同一です。
同様に、一番最後の項目も、アップロードするデータ自体は、アップロードの時々で変わりますが、アップロード自体の処理ロジックは同一です。
それ以外の項目のうち、デバイスからクラウドへの通信である、2番目と5番目は、送るためのデータの収集ロジックの部分はそれぞれのデバイスで異なりますが、クラウドへの送信処理ロジックの部分は同一です。デバイスから送信する D2C Message と Reported Properties のデータ構造は DTDL による IoT Plug & Play で定義された  Telemetry と Read Only の Property で規定されるので、データ構造定義や、通信の為のシリアライズに関わるコードロジックは自動生成の対象になります。
また、クラウドからデバイスへの通信である、3、4、6番目は、通信を受信するための処理ロジックは同一ですが、受信後の処理ロジックは、それぞれのデバイス毎固有の実装になります。デバイスからクラウドへの通信の場合と同様に、Desired Properties と Direct Method は、IoT Plug & Play で定義されたWritable な Property と Command で規定されるので、データのデシリアライズやデバイス固有の処理ロジックへのつなぎの部分は自動生成の対象になります。
以上、上に挙げた、同一の処理ロジックの部分は、フレームワークライブラリ化し、自動化対象の部分は、IoT Plug & Play の定義から自動生成することにします。 
ビジネスとしてのソフトウェア開発の場合、複数の開発者が参加してソフトウェアを開発していきますが、上に挙げた項目の実装においては、各開発者が思うままにバラバラに実装を行うことは、リリース後に永遠に続く機能拡張と保守の観点から望ましくありません。合理的な観点から設計実装方針を用意して、それに従って開発がすすめられれなければなりません。経験上、自動化しないスタイルの場合、開発に参加する人達の忍耐と、よほどの余計なコストをかけない限り、この様な開発は、まぁ不可能でしょう。
 ホビーや開発トレーニングを目的とした IoT ソリューションは別として、Digital Transformation や Digital Twins を目指す、実際の IoT ソリューションでは、様々な機器が IoT 機器として IoT ソリューションに接続されます。
接続する機器の OS やアプリケーションロジックの開発で用いるプログラミング言語は、それぞれの機器の特性や要件で決まります。結果として、IoT 機器のアプリケーションは、機器毎に異なる OS、異なるプログラミング言語を使った開発になります。

クラウドとデバイスの接続と双方向通信は、どの OS、どのプログラミング言語においても中身は一緒なので、自動生成も、様々な OS、プログラミング言語で可能とします。とはいえ、プログラミング言語は星の数ほどあるので、C#、C/C++、Python の3つをサポートすることにします。OS やプログラミング言語の選択は、使用する IoT 機器のハードウェアリソースを元に選択するのが原則です。また、ロジックの実行方法にはいくつか種類があるので、その選択も考慮に入れておきます。
コンピューティングリソースが少ない小型組込み機器の場合は、C/C++ が最適です。C/C++ の場合は、更に、Linux や Windows が使えない超小型組込み機器の場合には、Azure RTOS や Free RTOS、Embedが選択肢になります。この選択では、利用する SDK は、Azure IoT SDK for Embedded C になり、言語は、C言語一択になります。この様な機器では、電源が入って動き出すと、一つのアプリが自動的に起動されるという実行方法になります。
次に、コンピューティングリソースが比較的潤沢で、Linux や Windows が使える機器の場合は、どちらの OS でも使えて、プログラム自動生成にも親和性の高い C# の採用が妥当です。 アプリケーションの実行方法は、Shell によるアプリ実行と、バックグラウンドでの実行(Linux の場合は Daemon、Windows の場合は、Windows Service)の二種類があります。
ちょっと違った観点からになりますが、IoT 機器では、画像や音、センシングデータを元にした AI 処理を行うものもあります。AI 処理は何といっても Python がデファクトであり、最適でしょう。
アプリケーションロジックの実行方法には、Docker 等の技術を使ってコンテナーとして実行する方法もあります。Azure IoT Hub の場合は、IoT Edge を使う IoT 機器で利用可能な実行方法です。将来的にはどうなるかは微妙ですが、Docker が動くくらいの潤沢な HW リソースがある機器では、敢えて、C/C++を採用する理由はないと思われます。

接続に必要なセキュリティ情報の供給、プロトコル選択等、IoT 機器アプリ実行時に必要な情報は、Yamlフォーマットによる設定ファイルの雛形を生成し、ユーザーが設定できるようにします。設定項目は、IoT Edge と それ以外の実行方式で異なるので、生成時の選択に応じて雛形を生成することにします。
また、自動生成されたファイル一式のビルド、デバッグの簡便性を考慮して、Visual Studio で扱えるよう、プロジェクト定義ファイル等も生成対象とします。実際にやってみると判るのですが、フレームワークライブラリを分離した時の、生成コードからの参照の追加なども必要となり、それらも適宜対応することとします。

以上を勘案して、サンプルの自動生成環境の機能をまとめたものを
dtdl-iot-app-generator/README.md at main · kae-made/dtdl-iot-app-generator (github.com)
から、公開しているのでそちらを参照してください。見かけ上、結構なレベルで厳密に書かれていますが、自動生成環境構築の初めの時点では、不確定な部分を多数含むため、厳密性や正確性はあまり追い求めなくて構いません。いろいろな事実が判明した時点で、都度都度修正や書き加えを行っていけば十分です。

自動生成の仕組みや原理は、あまりプログラミング言語に関係ないので、本稿では、C# を使った場合を解説していきます。

実際に動くサンプルコードを作成する

さて、自動化のスコープが決まった後は、自動生成環境の雛形となるアプリを作成して、ちゃんと動くこと、自動生成されたファイル一式への固有要件の追加の容易性等を検証します。この過程で、固定部分のフレームワークライブラリ化も並行して行っていきます。
コーディングの際、「Essence of Software Design」に記載の事項を参考にしてください。
この工程は試行錯誤の連続です。DTDL による IoT Plug & Play で定義可能な範囲を最低限包含するようなサンプルコードになるよう心掛けつつ、あーでもこーでもないを繰り返してください。この段階でのフレームワークライブラリの出来は完璧を目指さなくてもよいです。
参考までに、作成したサンプルアプリは、azure-iot-hub-device-app-framework/csharp at main · kae-made/azure-iot-hub-device-app-framework (github.com) から公開しているので、参照してください。
このファイル一式は、一応完成したフレームワークライブラリに合わせて修正してしまっているので、実際にはもっとラフなコードのレベルで、この工程では問題ありません。

実際に筆者が、あーでもないこーでもないを繰り返して、作成したフレームワークライブラリを、azure-iot-hub-device-app-framework/csharp at main · kae-made/azure-iot-hub-device-app-framework (github.com)
から公開しています。
また、サンプルコードは、まず一番テストしやすいアプリ形式を作成しながら、フレームワークライブラリも並行して開発し、何が変わるのかを注意しながら、サービス形式のサンプルコードを作成、最後に IoT Edge 形式のサンプルコードを作成しています。それぞれ、以下のURLから公開しているので、ソースコードとプロジェクトファイルの違いを眺めてみてください。

フレームワークライブラリは、自動生成だけでなく、通常の手書きによる開発でも無理なく使えるように考慮しています。

サンプルコードを定型部分と非定型部分に分類する

DTDL による IoT Plug & Play の定義の幅を考慮したバリエーションへの検討や、実装時によく使われるであろうフレーズ等を検討して、サンプルコードを充実させていきます。例えば、このサンプルでは、ObjectType や Enum 等が使われている事を想定したり、IoT 機器としてはよくある、周期的なセンシングデータの送信も試みて、定型化していたりします。
アプリ形式の実行方式での検証が大体終わった後、サービス形式や IoT Edge 形式での実行用サンプルを作成し、定型の部分と非定型の部分を確認していきます。それぞれのサンプルは、以下のURLで公開しています。

詳細に観察し、ソースコードとプロジェクトファイルの違いを確認してみてください。

定型部分をフレームワークライブラリ化する

フレームワーク構築については、「Essence of Software Design - フレームワーク」をご一読ください。
実際に筆者が、この記事で説明している事を忠実に守って、あーでもないこーでもないを繰り返して、作成したフレームワークライブラリを、azure-iot-hub-device-app-framework/csharp at main · kae-made/azure-iot-hub-device-app-framework (github.com)
から公開しています。
この工程で作成されたフレームワークライブラリが最終形態ではないことに注意していください。自動生成環境構築中、更には、構築が終わって第三者が使い始めた以降も、様々な要因で変更は生じ、変化し続けるモノだと思ってください。変更を加えた結果、ちょっと気持ち悪いとか座りが悪いな(プログラミングの途中で、この様な感覚を持つことは非常に重要)と感じたら、躊躇わずにリファクタリングを行いましょう。適切に設計され、ある程度成熟したフレームワークライブラリは、フレームワークライブラリの内部構造をリファクタリングしても、ユーザーコードへの影響は小さいものです。また、自動生成を活用している場合は、再生成すれば済む部分も多いので、リファクタリングをよりしやすくなります。

ここで扱っているサンプル向けに作成したフレームワークライブラリは、Kae.IoT.Framework という名前空間で提供されており、このフレームワークを使う場合、ユーザーは、以下のインターフェイスやクラスを定義することになります。azure-iot-hub-device-app-framework/csharp/SampleIoTFWDeviceApp at main · kae-made/azure-iot-hub-device-app-framework (github.com) から公開されているサンプルファイルとの対応付けも明記しておきます。 

  • D2C Message、Desired Properties、Reported Properties、Direct Method の Request と Response のデータ型定義 - AppIoTData.cs

  • IoTAppConnector クラスを継承する各 IoT 機器アプリ用のクラス - SampleIoTAppConnector.cs 

  • フレームワークが規定する、初期化、動作、終了処理の順番を規定するロジック - Program.cs

  • 各 IoT 機器固有の、初期化・終了処理、センシングデータ収集・送信処理、C2D Message受信・Desired Properies更新・Direct Method起動時の処理、Reported Properties 更新処理、大きなデータのアップロード処理など - IoTApp.cs、IoTApp.code.cs、IIoTApp.cs

最後のファイル群が定義している要素は、フレームワーク本来の要求というよりも、ユーザーが各 IoT 機器のアプリロジックを構築する時の設計・実装方針を規定する意味合いからのもになっており、IIoTApp.cs ファイルで、IoT 機器アプリが実装するべき全てのメソッド群を宣言する IIoTAppというインターフェイスを宣言し、IoTApp.cs、IoTApp.code.cs で、それらメソッドを実装するように規定しています。IIoTApp インターフェイスにより、アプリロジック起動の仕組みと、アプリロジックの結合を疎にし、実行方法の多様性への対応を考慮しています。

非定型部分に対応するためのルールを決める

サンプルコードを作成し、フレームワークライブラリもある程度出来上がり、定型部分と非定型部分が判明したら、いよいよ、ファイル自動生成の仕組みづくりの開始です。
筆者は、1990年代後半から2000年代初頭にかけて、実に多くの自動生成ツールを作成した経験を持っています。BridgePointのarchtype、Java、Perl、Visual Basic 等様々な言語で試みています。いろいろ使った中で、一番使い勝手の良いコード生成(というより変換)の仕組みが、T4 Template です。本稿でもこの仕組みを使って解説を行います。
ここで、簡単にコード生成の基本を、typedef 付きの C言語の構造体の定義を例に説明しておきます。
typedef 付きの C言語の構造体定義は、

typedef struct Foo {
    int x;
    bool flag;
    …
} Foo;

こんな感じですね。"typedef struct " と "{"、"}"、及び、最後の ";" が定型部分です。これらは、C言語の文法で決まっています。非定型の部分は、構造体の名前を示す "Foo" と、構造体のメンバー変数です。メンバー変数は、
data_type_name member_variable_name ;
という形式で、定義する構造体に持たせたいメンバー変数を全て並べる事で定義します。data_type_name と member_variable_name の部分は非定型ですが、この構造は定型です。「Art of Conceptual Modeling」で解説している概念情報モデルを、構造体の定義に対して作成すると、
”構造体(struct_name)” 1 - member_of - * ”メンバー変数(data_type_name, member_variable_name)”
というモデルが出来上がります。この概念情報モデルを使うと、C言語の文法とは無関係に、構造体的な概念を定義していくことができます。
 概念情報モデルのインスタンス定義から、順番に"構造体"のインスタンスを取り出し、Foo の部分を struct_name で置き換え、更に、member_of のリンクをたどって、メンバー変数を順に取り出して、データ型の部分をdata_type_name に、メンバー変数名の部分を membar_variable_name に置き換えていけば、概念情報モデルのインスタンスで定義された構造体的な概念が全て C言語の構造体として定義されます。紹介した概念情報モデルを C# を使って実装すると、

class StructDecl {
    public string struct_name { get; set; }
    IEnumerable<StructMemberDef> memberOf { get; set; }
}
class StructMemberDef {
    public string data_type_name { get; set; }
    public string member_variable_name { get ;set; }
}

こんな感じになります。これらを使って、C言語の typedef 付きの struct 定義を生成する T4 Template を作ると、

<#
    foreach (var s in structDecls) {
#>
typedef struct <#= s.struct_name #> {
<#
        foreach (var m in s.memberOf) {
#>
        <#= m.data_type_name #> <#= m.member_variable_name> ;
<#
        }
#>
} <#= s.struct_name #>;
<#
    }
#>

こんな感じになります。<# … #> で囲まれた部分が、C#のロジックコードとして実行される部分で、<#= #> で囲まれた部分に書いてある変数の値で置き換わります。
※ structDecls は、IEnumerable<StructDecl> の変数とします。
Visual Studio では、”ランタイムテキストテンプレート”というアイテムテンプレートを使って、T4 Template を作成します。T4 Template ファイルの拡張子は、"tt"です。プロジェクトにT4 Template ファイルを追加すると、そのファイルを元に Visual Studio が自動生成する、拡張子が "cs" のファイルが出来上がります。例えば、"StructGen.tt" という T4 Template ファイルを追加すると、自動的に、"StructGen.cs" というファイル名になります。
このような形式のC#ソースコードファイルは、”コードビハインド”と呼ばれていて、T4 Template に書かれた変換ルールをC#コードに変換したロジック群が記述されています。"StructGen.cs" を開くと、”StructGen”という名前のクラスが確認できます。このファイルは .tt ファイルを修正するたびに自動生成されて全て書き換わるので、ユーザーによる編集はできません。
T4 Template ファイルの追加当初は、このファイルをソリューションエクスプローラで選択して表示される ”カスタムツール”プロパティは、TextTemplatingFileGenerator になっているので、TextTemplatingFilePreprocessor に書き換えます。
そのままでは、"StructGen.tt"  に書かれている、structDecls は宣言されていないので、所望の情報を、T4 Template に供給する仕組みを追加します。名前はなんでも良いのですが、"StructGenCode"という名前で、プロジェクトにクラスを追加します。出来上がった"StructGenCode.cs" ファイルを開いて、クラス名を "StructGenCode” から "StructGen" に変更し、"class" の前に、"partial" キーワードを追加します。

partial class StructGen
{
     IEnumerable<StructDecl> structDecls;
    public StructGen(IEnumerable<StructDecl> structs)
    {
        this.structDecls = structs;
    }
}

更に、上に示したようなメンバー変数を追加してやれば、T4 Template 側で参照可能になります。
これで、typedef 付きの C言語定義用ジェネレータは完成です。後は、

var decls = new List<StructDecl>
    { new StructDecl
        { struct_name = "Foo", new List<StructMemberDecl> {…},
        …
    };
….
var generator = new StructGen(decls);
var content = generator.TransformText();

の様に、StructGen のインスタンスを生成して、TransformText()をコールすれば、生成結果が得られるので、ファイルに保存するなり、更に加工したりと必要な生成処理を行えます。
以上が、自動生成の基本中の基本です。
このやり方で、非定型部分の生成ルールを定義して、自動生成環境を構築していきます。
残念ながら T4 Template の中で <# … #> で書いたコードロジックは、通常のプログラムの様に、ステップ実行によるデバッグができません。「マイクロソフトさん、是非、ステップ実行できる機能を提供してください!」と言いたいところですが、10年以上そんな機能は提供されていないので、現状を追認したうえでの Tips を紹介しておきます。データ供給用に追加したクラスに、メソッドを追加して、データ供給、ループ処理、条件判断、文字列変換など、T4 Template 内で使う予定の制御構造を実装して実行テストを行い、検証されたコードロジックを .tt ファイルにコピーし、テンプレート化していく、これがスムーズに Generator を作るコツです。Github から公開しているサンプルを見ていくと、prototype() という名前の、実際には使われていないメソッドが散見されます。これは、正にこの Tips を実践した名残です。例として敢えて残してあるので、.tt と見比べてみてください。

ここまで読んできて「なんだかめんどくさそ…」と思う読者もいるかと思いますが、この様な自動生成テクニックを使わずに、自然言語でフレームワークの利用方法を正確に、見ず知らずの開発者に伝える説明文を書く方が、よほど難しいと、筆者は考えています。正確な記述をするためには、何らかの正式なシンタックスやフォームが必要です。そんなものを何もないところから作り上げるのは車輪の再発明であり、そんなものを敢えて作るなら、より使い勝手の良い自動生成用ツールまで作った方が、世の中のためになるでしょう。

それはさておき、フレームワークライブラリの利用パターンを元に、T4 Template の使い方も交えながら、それぞれの要素を解説していきます。

D2C Message、Desired Properties、Reported Properties、Direct Method の Request と Response のデータ型

これらは、IoT Plug & Play で定義された Telemetry、Property、Command の Request payload、Response payload から自動生成可能です。
D2C Message の場合は、D2CData という名前のクラスを、フレームワークの IoTDataWithProperties を継承して宣言し、そのクラスに、定義された Telemetry を全てプロパティとして宣言します。
Desired Properties と Reported Properties は、それぞれ、AppDTDesiredProperties 、AppDTReportedProperties という名前のクラスを、フレームワークの IoTData を継承して宣言し、AppDTDesiredPropertiesには Writable な Property を、AppDTReportedProperties には、Writable ではない Property を全てプロパティとして宣言します。
Command の Request payload、Response payload は、各 Command 毎にフレームワークの IoTData を継承したクラスを宣言し、それぞれの schema で宣言された内容をプロパティとして宣言します。
Azure IoT Hub と IoT 機器間の通信では、JSONフォーマットでデータをやり取りされるのが基本なので、JSONテキストから対応するデータ型への変換(デシリアライズ)と、その反対方向の変換(シリアライズ)の処理ロジックが、IoT 機器アプリには、そのどちらか、または両方が必要になります。加えて、アプリ側でデータを扱うためのデータ構造も必要です。これらは、必ずロジックとして作らなければならないものであり、かつ、しょせん、同じフレーズでの実装なので、そこは自動生成してやろうということです。フレームワークの IoTData は、前述の双方向変換に対応するために、継承したクラスが、Deserialize()、Serialize() の二つのメソッドを実装するように宣言されているので、その実装を自動生成してやります。
Telemetry、Property、Command の Request と Response の schema(データ型)は、bool や string、integer 等、プリミティブなデータ型と、複数の子要素を持つ、オブジェクト型(Object Type)、及び、列挙型(Enum Type)があります。プリミティブ型の場合は、データクラスのプロパティを追加する際に、対応する C# 上の基本型をデータ型として使えば、宣言が出来上がります。オブジェクト型の場合は、データクラスのインナークラスとして、オブジェクト型で定義されたフィールドをそのインナークラスのプロパティとして定義するという変換ルールになります。オブジェクト型は階層化が可能なので、この変換ルールは再帰的に利用可能にします。列挙型の場合は、データクラス内に enum を宣言します。列挙型には、Value Schema が string と integer の二通りあるので、integer の場合には、定義されている数を列挙子の値として宣言する部分を追加します。
以上、これらを実際に行って生成するファイルが、AppIoTData.cs であり、変換ルールは、AppIoTData_cs.tt  で記述しています。この T4 Template ファイルに必要なデータや処理を供給するロジックは、AppIoTData_csCode.cs に書かれています。

サンプルでは、T4 Template ファイルに対応する、データや制御を供給するロジックを記述するファイル名を、T4 Template の名前に Code をつけるというネーミングルールを使用しています。以降、特記事項が無い限り、T4 Template には、この様なC#ファイルが付随するものと思ってください。
T4 Template ファイルの名前が "Foo.tt" なら、"FooCode.cs" です。

AppIoTData_cs.tt では、Telemetry、Writable な Property、Writable でない Property、Direct Method 毎の Request、Response の順で前述の変換を行うルールが記載されています。データクラスに追加するプロパティ宣言は、どれも同じフレーズと変換ルールが使えるので、FieldDecl.tt にまとめています。プリミティブなデータ型の場合の変換ルールはこの T4 Template ファイルに記載されていて、オブジェクト型の場合の変換ルールは、ObjectTypeDecl.tt  で記載しています。前述のとおり、この T4 Template で定義されたジェネレーターは、再帰的に使う事が出来ます。列挙型の場合の変換ルールは、EnumTypeDecl.tt  に記載されています。

参考までにですが、これらの変換ルールには、生成されたコードの見栄えに関する二つのテクニックが入っています。
一つは、インデントです。クラスの中にインナークラスを宣言する時、enum を宣言する時も同様ですが、通常は見やすいように宣言の深さに応じてインデントを下げていくと見栄えの良いコードが出来上がります。
二つ目は、enum の 列挙子宣言の際の ”,” の位置です。最後の列挙子の後ろには、なるべくなら ","はつけたくありません。この変換ルールでは、つける位置と、最後には付けない工夫をしています。
まぁ、C# の場合、Visual Studio の Editor で開いて、最後の ”}”を書き直せばインデントは全て揃うし、気の利いた整形ツールもあるので、それを使っても良いのですが。。。
一般的に、「一つのクラスあたり一つのファイルで宣言する」というクラス宣言に関する指針はありますが、このサンプルでは、めんどくさいのと、「まぁ、データ宣言だけ集めて一つのファイルを作っても実用上問題なかろ」ということで一つのファイルにまとめることにしました。分けたければ、AppIoTData_cs.tt を分割すればいいだけの話なので、本質的な問題ではありません。

IoTAppConnector クラスを継承する各 IoT 機器アプリ用のクラス

フレームワーク側で定義されている IoTAppConnector クラスの IoT 機器アプリ側の実装クラスを生成します。この実装クラスの名前は、プロジェクト名をプレフィックスにしたものにします。結果として、ファイルは、そのクラス名に、拡張子の".cs"を付けたものになります。このクラスの実装の非定型部分は二つあります。
一つ目は、IoT Plug & Play で定義された Command 群への振り分けの部分、具体的には、InvokeDirectMethodAsync() の実装です。
二つ目は、実行方式が、アプリ形式、サービス形式の場合と IoT Edge Module 形式の場合の違いです。前者は C2D Message を受信し、後者は Module の外側からの Message の受信を名前付きのアウトプットポートからするという違いがあるので、それぞれの場合で、使わない方のメソッドを明確にしています。他のメソッドの実装は定型になるようにフレームワークを設計しているので、定型コードをそのまま記述すれば問題ありません。以上の変換ルールを、MyAppConnector_cs.tt で定義しています。

フレームワークが規定する、初期化、動作、終了処理の順番を規定するロジック

このサンプルでは、アプリ実行方式が、アプリ形式と IoT Edge Module 形式の場合は、Program クラスの Main メソッドで、IoTApp クラスをインスタンスを生成して順番にコールする、という形にしています。フレームワークの使い方として、これは定義されたものなので定型です。
この変換ルールは、ProgramDeviceApp_cs.tt で定義しています。
アプリ実行形式が、サービス形式の場合は、一段、バックグラウンドサービス化するための処理が Program クラスに仕組まれるので、Worker クラスで定義しています。サービス形式の場合は、システム用のログ機構が用意されているので、それへの対応も行っています。変換ルールは、Worker_cs.tt です。

各 IoT 機器固有の、初期化・終了処理、センシングデータ収集・送信処理、C2D Message受信・Desired Properies更新・Direct Method起動時の処理、Reported Properties 更新処理、大きなデータのアップロード処理など

実装ロジックへの変換は、あくまでも、入力として使えるデータセットから、一定のルールに基づいたものだけです。IoT Plug & Play の定義は、クラウドとデバイス間の約束が定義されているだけで、その約束を守る具体的な処理はないので、機器固有の初期化や終了で必要な具体的な処理内容や、センシングデータの計測・収集の具体的な方法、クラウドからのデータ受信や Direct Method 起動時に何をやるか、といったことは、変換だけで作り出すことは原理的に不可能です。
自動生成の仕組みとしては、これら、ユーザーが記述するべき事を括りだして、書く場所を、実用上問題ない形で提供する方法を考えます。
サンプルでは、IIoTApp インターフェイス、IoTApp クラス、及び、IoTApp クラスの実装方法を工夫することによって、この課題に対応しています。具体的には、IIoTApp インターフェイスで、IoT 機器アプリが用意するべきメソッド群を定義し、そのインターフェイスを実装する IoTApp で必要なロジックを実装するよう設計しています。
IIoTApp インターフェイスはほぼ定型的ですが、IoT Plug & Play で定義された Command それぞれが起動された時の処理を行うメソッドの定義が非定型になっています。各 Command は、"string コマンド名(string payload)”という形式でメソッド定義することになっています。変換ルールは、IIoTApp_cs.tt  で記述されています。
次に IoTApp クラスです。このクラスは、IIoTApp で宣言されているメソッド群を全て実装するクラスですが、フレームワークの規定上、定型的に実装できるメソッド群と、ユーザーが独自にロジックを記述しなければならないメソッド群に大別されます。C# のpartial キーワードを使って、この二つを別々のファイルに分割するようになっています。定型的なコードは、IoTApp.cs、ユーザーがロジックを加える部分を、IoTApp.code.cs  に用意するようにしています。IoTApp.csファイルは定型なのでユーザー編集は禁止です。変換ルールは、IoTApp_cs.tt  です。
IoTApp.code.cs  は、結果として、

  • クラウドあるいは外側からのメッセージ受信時の処理

  • Desired Properties 更新時の処理

  • Direct Method 起動時の処理

  • Azure IoT Hub への接続前に行う初期化処理

  • Azure IoT Hub への接続後に行う初期化処理

  • 初期化完了後の様々な処理

  • Azure IoT Hub への接続を切る前に行う終了処理

  • Azure IoT Hub への接続が切れた後に行う終了処理

を記述するためのメソッドの雛形が生成されます。変換ルールは、IoTAppCode_cs.tt です。この様に、自動生成後に、ユーザーが手を加えることを前提としたファイルは、再生成時に、既にファイルがある場合は、更新しない、というような処理を自動生成環境に組み込むなどの工夫が必要です。

以上、ソースコードの変換ルールについて解説してきました。基本的に、ソースコードをビルドする時は、前回のビルド成功時点より後に更新されたソースファイルのみが再コンパイルされます。更新されたソースコードが少なければ、それだけビルドにかかる時間は短くなります。一つのファイルのビルド時間は問題ないぐらい短くても、塵も積もればなんとやらです。ユーザーが更新する必要なソースコードファイルはなるべく少なくなるように自動生成環境を設計しましょう。

ビルド環境を構成するファイル群の定型部分と非定型部分に分類し、ルールを決める

ソフトウェア開発においては、ソースコードだけでなく、ソースコード一式をビルドする環境、起動時の設定ファイル等が必要です。
Visual Studio による開発では、伝統的に、プログラム開発の単位を”プロジェクト”と呼び、".xxproj"という拡張子のファイル(xxはプログラムの種類によって異なり、C# の場合は、”cs”)で定義されます。
サンプルでは、ソースコードの生成だけでなく、このプロジェクトファイルも生成しています。
プロジェクトファイルは、通常の開発では、Visual Studio で、プロジェクトテンプレートを使って、何かのプロジェクトを作った時に、プロジェクトの種類等に応じて、Visual Studio が自動的に生成してくれるものです。
サンプルでは、

  • アプリ形式 - C# Console Application

  • サービス形式 - C# Worker Service

  • IoT Edge Module 形式 - C# Azure IoT Edge Module

で、それぞれプロジェクトを作った時に生成されるプロジェクトファイルを元にして自動化を行っています。変換ルールは、ProjectFile.tt に記載されています。
他に、生成されたコードを実行する時に参照する、接続セキュリティ情報等を記述するための、Yaml ファイルを "iot-app-config.yaml" という名前で生成します。この生成に当たっては、IoT Edge Module 形式の場合とそれ以外で、設定内容が異なるので、必要な設定項目のみをテンプレートとして生成するようになっています。変換ルールは、IoTAppConfig_yaml.tt です。
IoT Edge Module 形式の場合は、Docker Image のビルドも必要なので、そのための仕組みも、Dockerfile.tt や Module_json.tt  等で用意されています。

コード自動生成アプリ化する

変換ルールを T4 Template で作成していくのと並行して、生成したアーティファクト一式を Visual Studio でそのままプロジェクトで扱えるようにするディレクトリ作成や必要なファイルの作成・コピー、変換ルールを起動するアプリケーションを開発していきます。
サンプルでは、将来的に変換の仕組みだけを、例えば、DevOps 環境に組み込むなど、自動生成機構の再利用ができるように、ライブラリ化し、そのライブラリに必要な GUI を提供するアプリというように、分離して開発が成されています。
ライブラリは、dtdl-iot-app-generator/Kae.IoT.PnP.Generator at main · kae-made/dtdl-iot-app-generator (github.com) で公開しています。
このライブラリは、Visual Studio でプロジェクトとして開き、開発行為が行えるようなディレクトリ構成とファイル構成を整え、これまで説明してきた変換ルールを順番に起動して、ソースコード一式を生成する機能としてまとめられています。また、将来的には、C/C++ や Python のサポートも予定しているので、それらの拡張が可能なように考慮されて作られています。
ソースコードを生成するための情報として、DTDL で記述された IoT Plug & Play の定義を使っていますが、「IoT プラグアンドプレイモデリングガイド」の記載内容を元に、下図の様な概念情報モデルを作成し、この構造に沿って、DTDLの記述から概念クラスのインスタンス群を作成し、構造に沿って情報を取り出し、逐次、ソースコードに変換するようにジェネレータは作られています。

このモデルは、概念モデリングの視点から見ると、Azure IoT Hub から見れば、IoT 機器の抽象化したビュー(ドメイン)であり、IoT 機器から見れば、デバイスが IoT 機器としてクラウド側に提供するべき情報の一般化したビュー(ドメイン)であるといえるでしょう。
概念モデリングについては、「Art of Conceptual Modeling」をご一読ください。
GUI付きのアプリは、dtdl-iot-app-generator/Kae.IoT.Tools.IoTAppGenerator at main · kae-made/dtdl-iot-app-generator (github.com)
です。このアプリは、生成対象のDTDLファイルの選択、生成するプログラミング言語や関連する選択可能なバリエーション設定、ジェネレータの起動に関する機能を提供します。使い方は、dtdl-iot-app-generator/HowToUse.md at main · kae-made/dtdl-iot-app-generator (github.com) を参照してください。

この二つのプロジェクトには、自動生成環境を構築する為のテクニックがちりばめられているので、じっくりと中身を眺めて、理解を深めてください。

手動で行う作業を都度自動化していく

自動生成環境は、一度作れば終わりというものではありません。使い勝手の良し悪しやスコープの妥当性は、第三者が使ってみて初めて分かることが多々あります。やってみたら想像していたものと違ったというようなことは日常茶飯事です。また、自動生成環境の開発を行えば行うほど、技が増えていく事も多いので、改善・改良の余地があれば、積極的かつ、継続的に、(生成後のコードにユーザーが手を加えた場所には極力影響を与えないように)修正を加えていく事をお勧めします。

kae-made/dtdl-iot-app-generator: This application generates IoT application framework which can collaborate with Azure IoT Hub from IoT PnP DTDL file (github.com) では、各ファイルの履歴を見ることも可能です。
公開後に手を加えて例を、一つだけ紹介しておきます。
生成されたプロジェクト一式をビルドするには、Kae.IoT.Framework  の DLL が必要です。IoT Edge Module 形式の場合は、Docker Image の Build で DLL のファイルが必要なので、Kae.IoT.Framework  プロジェクトを手動で Build して、必要な DLL ファイルを手動コピーするという方法をとっていました。
アプリ形式、サービス形式の場合は、それらのプロジェクトに、Kae.IoT.Framework  のプロジェクトを参照プロジェクトとして追加する、という方法をとっていました。しかし、実際やってみると、参照プロジェクトとして追加できるのは、同じソリューションに属するプロジェクトだけであることが判明し、あえなく失敗(苦笑)。解決策として、Kae.IoT.Framework  プロジェクトで Build して、必要な DLL ファイルを参照追加する、という形式を採用しました。
この時点で、.csproj ファイル上での Kae.IoT.Framework  の参照に関する記述は、3つの実行方式で全て同じ形式に統一されました。
それに合わせて、ProjectFile.tt  を修正し、一段落。
しかし、開発活動を続けていくうちに、Kae.IoT.Framework 側での修正も発生し、その都度、IoT Edge Module の場合は、ビルド済みの DLL ファイルを手動コピーしなければならない事になり、めんどくさいのと、ビルドされた DLL ファイルのありかと名前は同じなので、プロジェクト環境を生成する時に、自動的に DLL ファイルをコピーする機能を追加して一段落。更に一段進んで、最新の DLL ファイルがコピーされるように、Kae.IoT.Framework ライブラリの Build もプロジェクト環境を生成する時にやってしまおうということで、その機能を組み込み、今に至る、です。最新かどうかの問題は、当然、アプリ形式、サービス形式でも問題だったので、それも解決、と一見落着。

Github の更新履歴を探ると、もっと色々な更新(中には、アホか?!というものもあり)があるので、是非、眺めてみてください。

最後に

以上、IoT Plug & Play の定義から、C# をターゲットにした、IoT 機器アプリプロジェクトの自動生成をサンプルに使って、ソフトウェア開発成果物自動生成の基本を解説してきました。2022年5月9日時点で、実際に公開しているサンプルは、ターゲットのプログラミング言語としてC#しかできていませんが、今後、C/C++ や Python についても開発していく予定です。出来上がった段階で、それぞれの言語に特化した自動生成に関するテクニックや概念をドキュメント化する予定です。
また、本稿はソースコード生成が主となっていますが、IoT Plug & Play の DTDLファイル生成や、様々なソフトウェア開発における中間成果物の自動化についても、機会があれば解説しようと考えています。

本稿を読破して、「自分もソフトウェア成果物の自動生成をチャレンジしてみよう!」という読者が一人でもいれば幸いです。

この記事が気に入ったらサポートをしてみませんか?