モダンRPG、実世界のデータ構造
モダンRPGでのデータ構造の基本はすでにご存知のとおりですが、本稿はアプリケーションの例を通してデータ構造の全ての概念を結びつけます。
サンプル・アプリケーションを探る
コードを見ていく前にサンプル・アプリケーションについて説明します。DS_EXAMPLEライブラリのSAVFはiprodeveloper.com/codeからダウンロード可能です。このライブラリには本稿で使用するDB2テーブル、インデックス、プログラム・オブジェクトが全て含まれています。このアプリケーションはIBM i 6.1以後のバージョンだけで利用可能な機能を使用しています。具体的には、EXFMT付のデータ構造、LIKEREC(record:*ALL)サブファイルとEXFMTで定義されたデータ構造、データ構造テンプレートを定義する際のTEMPLATEキーワードを使用するにはIBM i 6.1を稼動していなければなりません。こうした機能がすべて正しく動作することを保証するには、tinyurl.com/75wlfzxからPTFを読み込んでください。IBM i 6.1より前のバージョンをお使いの場合は、上記の足りない機能に対処するようにアプリケーションにコードを追加して修正することができます。
ここで説明するサンプル・アプリケーションは非常に簡素化された注文(PO: Purchase Order)システムです。図1に示したテーブル構造は複数のテーブルで構成され、注文アプリケーションとしてはごく標準的で、異なるテーブル中の列が同じ名前であることが多いものです。またここで説明するアプリケーションで使用することができるPOデータ構造を作成する役目も果たします。この例は純粋なILEアプリケーションで、サービス・プログラムPO_SERVICEを使用してすべてのデータベースとのやり取りやビジネス・ルールを取り扱います。PO_SERVICEへのインタフェース用のサブ・プロシージャとデータ構造テンプレートのプロトタイプは,ソース・ファイルSOURCE中のコピー・ソース・メンバーPO_PROTO中にあります。
SOURCEにはPO_SERVICEサービス・プログラムのシグネチャを管理するのに使用するバインダ言語も含まれています。このバインダ・ソースはPO_BINDERにあります。PO_PROGRAMは表示ファイルPO_SCREENSを使ってここで説明するアプリケーションを実行します。単純化するために私は意図的に一部の機能を省きましたので、アプリケーション例を自由に機能強化して今回学んだ概念に馴染んでください。(最後に緑色画面アプリケーションを書いたのは1年以上も前ですので、サブファイルの処理に誤りがあったらお許しください。)
このサンプル・アプリケーションにはエラー処理や検証の機能は実質上全く含まれていない点に注意してください。これはデータ構造とその使用方法について焦点を当てたかったからです。新しいアプリケーションを構築する際の基礎としてこのコードを使用されることは歓迎しますが、実運用アプリケーションに組み入れるには追加の作業が必要です。コードを見て行く前に、このアプリケーションがどのように動作するのか少し実行してみてください。
覆面の下を覗く
UIの観点から何が起こっているのかはご覧いただけたと思いますので、覆面の下を見てみましょう。この例には800行強のコードが含まれていますので、全ての行を説明することはしません。その代わりに重要な点だけ、特にデータ構造に関するところに触れます。ただしお気づきになると思いますが、この例はスタンドアローンのフィールドをほとんど使っておらず、コード中のほとんど全ての操作はデータ構造を使用しています。
ほとんどの作業はサービス・プログラム上で行われるので、そこから見始めるのが合理的に思えますから、プロトタイプ・コピーブックPO_PROTOを見てみましょう。コードの最初の部分は、PO構造を構築しサービス・プログラムとインタフェースするのに使用するデータ構造テンプレートの全てを定義しています。なぜこのデータ構造をサービス・プログラム中ではなくコピーブック中で使用するのでしょうか。それはPO_SERVICEサービス・プログラムを使用する各プログラムはこの定義を使用してPO構造のそれぞれのインスタンスを作成し、PO_SERVICEとの間でやり取りする必要があるからです。データベースのテーブル中でこの定義を参照する代わりに各サブフィールドを明示的に定義している点にお気づきになりましたでしょうか。PO_SERVICEと同じようにデータベース操作をカプセル化するときは、endプログラムはF仕様にこのテーブルを含めません。つまり、定義の列はテンプレート中の参照には利用可能となりません。
今回のアプリケーションを通じて使用する最終的なPO構造のレイアウトを図2に示します。このPO構造は、注文を扱ったことがあればその意味がわかることでしょう。お気づきかもしれませんが、この「マスター」POテンプレートを構築するには数個のデータ構造テンプレートが必要になります。PO_PROTOにもPO_SERVICE内のサブ・プロシージャの各プロトタイプが含まれています。
それではPO_SERVICEのソースコードを見てみましょう。注文に関連する全てのテーブルはF仕様に定義されています。典型的な環境においてはこのサービス・プログラム中のCUSTOMER、ITEM_MAST、ADDRESSのテーブルは使用しない場合がほとんどです。理想的には、これらのテーブルは独自のサービス・プログラムを有してそのデータおよび関連するビジネス・プロセスとのやり取りを処理するのですが、今回の例においては少し省略して入れておくことにしました。/INCLUDEパラメータはプロトタイプとテンプレートの定義を読み込みます。これによりファイル入出力中に使用するために定義された各レコード・フォーマットのデータ構造を確認することができます。このデータ構造はPO_PROTOメンバーには含めませんでした。なぜならendプログラム・オブジェクトがデータベースのテーブルと直接やり取りすることは意図していないからです。全てのファイル入出力はPO_SERVICE中のサブ・プロシージャを介して行われなければなりません。なぜならこの方法により全てのプログラムに対しての入出力をこの1点で行うことができるからです。ビジネス・ルールとデータに対する要件が変化した場合、1つのオブジェクトに対して修正を行って、それを全てのプログラムに適用することができます。これにより修正に要する時間を大幅に削減できます。
PO_SERVICE中の最初のサブ・プロシージャGET_POが一番頻繁に使用されます。GET_POサブ・プロシージャはその名前が示す通りの動作をします。つまりPO番号をパラメータとして受け取って、POテンプレートに基づいたデータ構造を介して関連する全てのPOデータを返します。DS_EXAMPLEライブラリ中のソースコードをご覧ください(図3)。プロシージャのモジュール性が高く、またデータ構造を使用しているため、コードが実際非常にシンプルになっています。POデータ構造内に保存されている比較的大量の情報を読み出すのにはそれほどたくさんの行のコードは必要ありません。
まずGET_POが、このサブルーチンだけで使用するローカルのスコープを持つデータ構造をPO_PROTOから必要なテンプレートを用いて定義します(呼び出しA)。これにより、呼び出す可能性のある他のプロシージャをうっかり修正してもこのデータが影響を受けないように保護することができます。次にGET_POはPOHEADERレコードをデータ構造内に置きます(呼び出しB)。データ構造POHEADER_INにレコードを置くことで、EVAL-CORRを使ってデータをヘッダー・レコードから私たちのORDER構造に1行でコピーすることができます。次にORDERデータ構造中の4つのサブ構造がそれぞれ対応するGETプロシージャを使用して読み込まれます。最初の3つのサブ構造は単純なCHAIN操作を遂行し、EVAL-CORRを使用して戻り値用の適切なフォーマットにデータを読み込みます。最後のGET_PO_DETAILSサブ構造は残りの3つとは少し異なります。
この時点でその他のGETプロシージャはデータ構造をすでに戻しています。GET_PO_DETAILSプロシージャはデータ構造配列と戻す配列の要素の数も戻す必要があります。データ構造配列を戻り値として戻し、行数の合計を通常のパラメータとして返すべきだったのかもしれませんが、あることを説明するためにこの逆の方法を選択しました。GET_PO_DETAILSはパラメータとしてPO構造全体を受け取らず、LINEサブ構造しか受け取らないという点に注意してください。その代わりにORDER.LINEを使用しなければなりません。
もう一つ注意しなければいけないプロシージャがGET_PO_LISTです。このプロシージャはヘッダー・レベルのPOデータだけを持つデータ構造配列を返します。私たちのアプリケーションにおいて注文のサブファイル・リストを構築するときにこのプロシージャが便利であることがわかります。確かにGET_POを各サブファイル行に使用すべきでしたが、この場合、全ての詳細情報を必要としていたわけではありませんでした。忘れてはいけない重要なことは、同じデータの集合に対してデータ構造に基づいた複数のインタフェースを持つことができるという点です。各プロシージャに対して異なるデータ構造インタフェースを持つことはお勧めしませんが、いくつか異なるインタフェースを用意してもサービス・プログラムには影響ありません。
>PO_PROGRAMの流れを見る
サービス・プログラムについてはこれで十分でしょう。ではPO_PROGRAMを見てみましょう。まず、PO_PROGRAM中のファイル仕様はPO_SCREENSの表示ファイルだけという点に注意してください。前述しました通り、データベースとのやり取りは全てPO_SERVICEにデータ構造をパラメータとして渡して呼び出すことで発生します。全体的にみれば、データの定義に何も変わったところはないように見えるかもしれませんが、仕様の最後の部分のところで表示ファイル中のレコード・フォーマットに基づいたデータ構造を定義しています。そうです。データ構造を画面入出力用のデータ構造として使用するのです。この定義の唯一の違いはLIKERECのタイプ・パラメータとして*ALLを使用している点です。最新バージョンのRPGでは画面入出力操作は入力と出力に同じデータ構造を使用することができます。いつかIBM Rationalチームの仲間がデータベース入出力にもこの機能強化をしてくれると願っています。
PO_PROGRAMはきわめて標準的なサブファイル・アプリケーションです。通常は、関連する画面ごとに別々のプログラムに分けるのですが、今回の例では全て一塊にまとめました。LOAD_LISTプロシージャを詳しく見てみると、その処理はサブファイルの読み込みの場合と同じであることにお気づきになるでしょう。唯一の違いはDOWループがデータベースの行を読む代わりにこのプロシージャではGET_PO_LISTプロシージャを呼び出してforループを使って結果を繰り返し処理し、EVAL-CORRがEVAL操作の複数行を切り出してWRITE操作の前にデータを読み込んでいるという点です。
EDIT_POプロシージャ(図4)は、通常私は別のプログラムに入れる基本的な主流となるコードで、これを使ってPOヘッダー情報とサブファイルの詳細行を加えて画面を構築しています。ロジックはPO_PROGRAMの主流となるコードと似ています。LOAD_PO_SCREENプロシージャは、LOAD_LISTプロシージャと同様のロジックとなっていることがわかります。大きく異なる点は、1つのグループのフィールドが個別に設定されている点です。これは、複数のアドレスがPO_CTL画面上に現れ、アドレス・フィールドの1つの集合がADDRESSデータ構造テンプレートと異なる名前を必要とするからです。このサブフィールドにはそれぞれ異なる名前がついているので、EVAL-CORRはこれを読み込むことができません。
図5に示したのはEDIT_DETAILで、これはPO_PROGRAM中の最後のプロシージャです。このプロシージャはPO行の詳細情報を画面に読み込み、変更を受け取ります。EVAL-CORR操作をいくつか使って画面を読み込みます(呼び出しA)。この方法を使用すると個々のEVAL文でデータを読み込むよりずっと簡単で、特にフィールドが多数ある画面では有効です。このプロシージャはDTL_EDIT画面を急いで構築し、1つのEVAL-CORRでデータをORDER構造に戻します(呼び出しB)。これ以上簡単にはならないでしょう。ではコーディングを楽しんでください。