抽象データ型とRPG
抽象データ型(ADT)は、データのタイプと、そのデータのタイプに対して定義される操作のセットです。ADTを使用することにより、プログラマーは、物理表現ではなく、機能の観点から、データを扱うことができるようになります。ADTは、オブジェクト指向プログラミングの基礎となっているものです。だとすると、抽象データ型はRPGのような手続き型言語には適用されないということになるのでしょうか。そんなことはありません。まったく逆です。
RPGベースのアプリケーションで抽象データ型をどのように使用することができるかを説明する前に、別の非OOオブジェクト、すなわち、ユーザー プロファイルを例に用いて、抽象データ型について詳しく説明してみようと思います。ユーザー プロファイルは、データのタイプです。ユーザー プロファイルには、プロファイル名、パスワード、作成日、状況、現行ライブラリーなどの属性があります。そして、ユーザー プロファイルに対しては、実行できる限定された操作のセットがあります。たとえば、ユーザー プロファイルに対しては、作成、削除、変更、ダンプなどの操作を行うことができます。行うことができる操作のすべてのリストを確認するには、IBM iのコマンド ラインからGO CMDUSRPRFを実行します。
ユーザー プロファイルがシステムでどのように格納されているかご存じでしょうか。私は知りませんし、気にしてもいません。重要ではないからです。
ユーザー プロファイルは、初めてAS/400が出荷されたときにそうであった通りに今も格納されているのでしょうか。おそらく、そうではありませんが、それも重要ではありません。インターフェースは、当時そうしていたように今でも機能しているからです。
IBM iはオブジェクトベースのシステムであるので、今述べたことは、他のオブジェクトのタイプ(プログラム、ファイル、データ待ち行列、データ域、メッセージ待ち行列など)にも当てはまります。すべてのオブジェクト タイプには、定義された属性と操作があります。
それでは、アプリケーション プログラミングについて考えてみましょう。組織がどのような業務を行っている場合でも、その組織にはオブジェクトがあります。たとえば、どの組織にも、その組織のサービスの利用者がいます。そのようなサービス利用者が何と呼ばれているかは、それぞれ異なります。顧客、クライアント、患者、消費者、市民、納税者など、様々です。
顧客データに対しては、実行できる限定された操作のセットがあります。たとえば、顧客を作成する、データベースから顧客を除去する、顧客の属性(フィールド)を変更する、顧客を一時停止する、などです。したがって、顧客というのは、コンピューターにおいて抽象データ型を用いて表現することができるということになります。
それを行うには、どのようにしたらよいのでしょうか。簡単です。ここでは、例として在庫品(inventory)を使用します。この例の目的では、在庫品を、倉庫に保管されている品物と定義します。
在庫品に対しては、どのようなことを行うことができるでしょうか。たくさんのことがあります。ほんの少しの例を以下に示します。
- 入庫: 送られてきた品目を倉庫に保管する。
- 返品: 品目を購入先の個人または組織へ送り返す。
- 転送: 倉庫内のある場所から同じ倉庫内の別の場所または別の倉庫へ在庫品を移動する。
- 出庫: 顧客へ品目を発送する。
こうした、行うことができる「たくさんのこと」は トランザクションと呼ばれているため、在庫品に対しては、実行できる一定数のトランザクションがあることになります。言い換えれば、データのタイプと操作のセットがあるということです。つまりは、抽象データ型です。
最初に行うことは、データと、そのデータに対して定義される操作を記述することです。これは、コピーブックで簡単に行えます。以下は、ソース物理ファイルPROTOTYPESのメンバーINVENTORYです。
**free
dcl-s InventoryStatus_t char(2) template;
dcl-c INV_ALL_OK '00';
dcl-c INV_ITEM_NOT_FOUND '01';
dcl-c INV_INVALID_LOCATION '02';
dcl-c INV_INVALID_ITEM '03';
dcl-c INV_MISC_ERROR '99';
dcl-s ItemNumber_t packed(5) template;
dcl-s Quantity_t packed(5) template;
dcl-s Warehouse_t packed(3) template;
dcl-s Aisle_t packed(3) template;
dcl-s Bay_t packed(3) template;
dcl-s Level_t packed(1) template;
dcl-ds InventoryRecord_t qualified template;
ItemNumber like(ItemNumber_t);
Warehouse like(Warehouse_t);
Aisle like(Aisle_t);
Bay like(Bay_t);
Level like(Level_t);
Quantity like(Quantity_t);
end-ds InventoryRecord_t;
dcl-pr AdjustItem;
ouStatus like(InventoryStatus_t);
inItemNumber like(ItemNumber_t) const;
inWarehouse like(Warehouse_t) const;
inAisle like(Aisle_t) const;
inBay like(Bay_t) const;
inLevel like(Level_t) const;
inQuantity like(Quantity_t) const;
end-pr AdjustItem;
dcl-pr MoveItem;
ouStatus like(InventoryStatus_t);
inItemNumber like(ItemNumber_t) const;
inFromWarehouse like(Warehouse_t) const;
inFromAisle like(Aisle_t) const;
inFromBay like(Bay_t) const;
inFromLevel like(Level_t) const;
inToWarehouse like(Warehouse_t) const;
inToAisle like(Aisle_t) const;
inToBay like(Bay_t) const;
inToLevel like(Level_t) const;
inQuantity like(Quantity_t) const;
end-pr MoveItem;
コピーブックの先頭は、ステータス コードの定義です。これは、ルーチンがトランザクションの成否をレポートするのに使用する 列挙データ型 です。これは、在庫品データ タイプの属性ではありません。
その後は属性の記述であり、3つの属性があります。すなわち、品目ID、品目の倉庫内でのロケーション(保管場所)、そのロケーションに保管されているその品目の数量の3つです。これらの属性の定義は、表の列(物理ファイルのフィールド)の定義と、必ずしも一致するわけではないことに注意してください。
ここまでのすべては、テンプレートとして定義されます。つまり、これらの変数にはデータを格納することができないということです。代わりに、LIKEキーワードを使用して作業変数を定義する必要があります。ここでは、それぞれの変数の名前の最後を「_t」としました。それらがテンプレート コードであることを示すためです。
私は、Inventory_tというレコード タイプを定義しました。もっとも、2つのサブプロシージャーではそれを使用しませんでした。このタイプの構造は、プロシージャー インターフェースで多くのパラメーターを定義することなく、多くの属性を渡すのに便利です。
記述の後は、サービス プログラムのプロシージャーのプロトタイプです。この短い例では、調整(adjustment)と転送(transfer)という2種類の在庫品トランザクションのみを定義しています。在庫調整は、手持数量に値を当てはめるだけです。たとえば、コンピューターからは、ある品目が1ダースあると報告されていて、倉庫の中に入って数えてみると、10点しかなかったとします。その場合は、調整トランザクションを使用してデータベースを修正することができます。もう一方のトランザクションは転送です。私は、よりシンプルな「 移動(move)」という用語の方が好みなので、そちらを用いています。
それらの操作を実行する演算を入れるためのもうひとつのソース メンバーが必要です。以下は、ソース物理ファイルQRPGLESRCのメンバーINVENTORYです。
**free
ctl-opt nomain option(*srcstmt: *nodebugio);
/include prototypes,inventory
dcl-s DB_ItemNumber_t char(15) template;
dcl-s DB_Location_t char(16) template;
dcl-s DB_Quantity_t packed(11: 3) template;
dcl-proc AdjustItem export;
dcl-pi *n;
ouStatus like(InventoryStatus_t);
inItemNumber like(ItemNumber_t) const;
inWarehouse like(Warehouse_t) const;
inAisle like(Aisle_t) const;
inBay like(Bay_t) const;
inLevel like(Level_t) const;
inQuantity like(Quantity_t) const;
end-pi;
dcl-s Quantity like(DB_Quantity_t);
dcl-s ItemNumber like(DB_ItemNumber_t);
dcl-s Warehouse like(Warehouse_t);
dcl-s Aisle like(Aisle_t);
dcl-s Bay like(Bay_t);
dcl-s Level like(Level_t);
dcl-s Location like(DB_Location_t);
ouStatus = INV_ALL_OK;
EncodeLocation (Location: inWarehouse: inAisle: inBay: inLevel);
Quantity = inQuantity;
ItemNumber = %editc(inItemNumber: 'X');
exec sql
update inventory
set Qty = :Quantity
where Item = :ItemNumber
and Loc = :Location;
select;
when SqlState = '02000';
ouStatus = INV_ITEM_NOT_FOUND;
when SqlState > '02000';
ouStatus = INV_MISC_ERROR;
endsl;
end-proc AdjustItem;
dcl-proc MoveItem export;
dcl-pi *n;
ouStatus like(InventoryStatus_t);
inItemNumber like(ItemNumber_t) const;
inFromWarehouse like(Warehouse_t) const;
inFromAisle like(Aisle_t) const;
inFromBay like(Bay_t) const;
inFromLevel like(Level_t) const;
inToWarehouse like(Warehouse_t) const;
inToAisle like(Aisle_t) const;
inToBay like(Bay_t) const;
inToLevel like(Level_t) const;
inQuantity like(Quantity_t) const;
end-pi;
dcl-s Quantity like(DB_Quantity_t);
dcl-s ItemNumber like(DB_ItemNumber_t);
dcl-s FromLocation like(DB_Location_t);
dcl-s ToLocation like(DB_Location_t);
ouStatus = INV_ALL_OK;
Quantity = inQuantity;
ItemNumber = %editc(inItemNumber: 'X');
EncodeLocation (FromLocation:
inFromWarehouse: inFromAisle: inFromBay: inFromLevel);
EncodeLocation (ToLocation:
inToWarehouse: inToAisle: inToBay: inToLevel);
exec sql
update inventory
set Qty = Qty - :Quantity
where Item = :ItemNumber
and Loc = :FromLocation;
// fixme -- check SQLSTATE, set ouStatus if error
exec sql
update inventory
set Qty = Qty + :Quantity
where Item = :ItemNumber
and Loc = :ToLocation;
// fixme -- check SQLSTATE, set ouStatus if error
end-proc MoveItem;
dcl-proc EncodeLocation;
dcl-pi *n;
ouLocation like(DB_Location_t);
inWarehouse like(Warehouse_t) const;
inAisle like(Aisle_t) const;
inBay like(Bay_t) const;
inLevel like(Level_t) const;
end-pi;
ouLocation = %editc(inWarehouse : 'X') +
%editc(inAisle : 'X') +
%editc(inBay : 'X') +
%editc(inLevel : 'X');
end-proc EncodeLocation;
これは堅牢なコードでありません。この例を機能させるのに十分な程度に、ササッと作ってみたものだからです。実際のアプリケーションなら、最低でも適切なエラー チェック機能およびコミットメント制御も備えている必要があります。
このモジュールは、すべての在庫品トランザクションが定義される場所です。これが実際の本番アプリケーションであるとしたら、コンパイルしてモジュールを作成し、そのモジュールからサービス プログラムを作成し、在庫品トランザクションを実行する必要があるすべてのプログラムがそのサービス プログラムにバインドすることになります。つまり、在庫品を移動する必要があるプログラムが1ダースあるとしたら、1ダースのプログラムがすべてMoveItemルーチンを呼び出します。INVENTORY表と直接対話するプログラムは1つもありません。
繰り返した方がよいでしょうか。在庫品を移動する必要があるプログラムが1ダースあるとしたら、1ダースのプログラムがすべてMoveItemルーチンを呼び出します。INVENTORY表と直接対話するプログラムは1つもありません。
以下は、調整ルーチンの呼び出しの例です。
/include prototypes,inventory
dcl-s Status like(InventoryStatus_t);
AdjustItem (Status: Item: Whs: Aisle: Bay: Lvl: NewQty);
1つ、指摘しておきたい大事なことがあります。前述の通り、属性のデータ タイプは、データベースでのデータ定義と一致している必要はありません。この例を作成するにあたって、私はその点を強調するために、わざわざ面倒を背負い込んでみました。以下が、その面倒の結晶である、データ定義の比較表です。
属性 | Inventory抽象データ型 | INVENTORY表 |
---|---|---|
Item | Packed 5,0 | Character 15 |
Location | 4つのフィールド
|
1つのフィールド, Character 16 |
Quantity | Packed 5,0 | Packed 11,3 |
ここで出てくるのが、抽象化です。大事なのは、データをどのように認識するかです。データがどのようにデータベースに格納されているかは、トランザクション処理には無関係だからです。何となれば、サービス プログラムを変更して別の表を使用することもできますが、これらのサブプロシージャーにバインドしていたプログラムには、違いが分かりません。
データベースの照会など、非トランザクション処理についてはどうでしょうか。これは別個のプロセスであり、抽象データ型に適合しません。表を照会するときは、サービス プログラムで定義されているデータではなく、生のデータで作業する必要があるでしょう。
そうする必要があるのでしょうか。
必ずしもそうであるとは限りません。自分の好きなようにデータを認識させてくれるSQLビューを定義することができます。ただし、これについては、この記事の対象範囲を超えているため、そうした SQLビューの使い方については、Paul Tuohyの手による素晴らしい記事を参照してください。
RPGは、長い間にわたって、様々な形で存在してきたことで、いろいろ言われているようです。私は、プログラミング言語も含め、新しくて良いものに反対するものはでありません。しかし、RPGは評価されている以上のことを行うことができることを私は知っています。抽象データ型を使用することで、ソフトウェア アプリケーションの構築のしかたに大きな違いが生まれます。そして、RPGは、他のどのプログラミング言語にも劣ることなく、それらを処理することができるのです。