TryIT - 試してみれば気に入るはず
子供たちが モグラ叩きゲームで遊んでいるのを見掛けたことはありますが、自分ではプレイしたことはありません。たぶん、モグラ叩きを見ると、自分が関わったプログラムを思い出して不愉快な気分になるからでしょう。バグを1つ修正しても、別のバグが見つかるだけです。人生は短いのだから、そのような無意味なことを耐え忍んでいる暇などないのです。さらに言えば、修正したばかりのはずのプログラムが、まだ壊れていると誰かに言われたりしたら良い気はしません。
たとえば、4,000行のRPGプログラムで作業していて、650~660行目をコメント アウトするとします。そして、この時点で知らないことがあります。それは、2755行目で使用される変数の値は、654行目でそれに割り当てられた値でなければならないということです。代わりに、その変数の値は、1435行目で割り当てられた値になっています。さあ、モグラ叩きの始まりです。
1つ確かなことがあります。すなわち、「意図せざる結果の法則」からは逃れられない、ということです。そうした理由から、私は2つの疑問についてよく考えてみました。(1) プログラムの保守によって、望ましくない副作用が引き起こされることがあるのはなぜか。(2) バグを発生させないようにするために、どのようなプログラミング作法を身に付けたらよいのか。
1つ目の疑問に関しては、望ましくない副作用の主な原因は、グローバル変数の使用にあると私は確信しています。プログラムの一部でのみ利用可能なローカル変数とは対照的に、グローバル変数は、プログラム全体の至る所で参照できる変数です。私が使用していた初期のRPGおよびCOBOLコンパイラーがサポートしていたのは、グローバル変数のみでした。
私の推理が正しいとすれば、2つ目の疑問に対する回答は、グローバル変数の使用を避けることです。では、そうするには、どうすればよいのでしょうか。それは、サブプロシージャーを書くことです。それが、プログラムを書く際に私が行っているやり方です。私のプログラムは、メイン ドライバー ルーチンの制御下にあるサブプロシージャーの集まりです。データは、サブプロシージャー間でパラメーターを通じて渡され、作業データはサブプロシージャー内で定義されるため、プログラムの残りの部分はそれについて知りません。
すべてのサブプロシージャーに対して、厳しいテストの実施が必要です。そして、そうしたことは、大規模なプログラムでやり遂げるのは難しいことです。使用したテスト データで、可能性のあるすべての入力データの組み合わせをテストできているということは、どのようにしたら確信できるのでしょうか。私の場合は、「TryIt」というサブプロシージャーを使うことで、そうした確信を得ています。
以下は、TryItテンプレート ソース メンバー、TRYIT_Tです。
**free
// This program . . . (does what?) . . .
ctl-opt option(*srcstmt: *nodebugio) dftactgrp(*no) actgrp(*new);
dcl-c LINE_LENGTH 132;
dcl-f qsysprt printer(LINE_LENGTH);
dcl-ds PrintLine len(LINE_LENGTH) qualified inz;
xxx char ( 1);
*n char ( 1);
yyy char ( 1);
*n char ( 1);
end-ds;
*inlr = *on;
// print column headings
PrintLine.xxx = 'xxx';
evalr PrintLine.yyy = 'yyy';
writeln (PrintLine);
TryIt ( 'x' : 0 );
return;
dcl-proc TryIt;
dcl-pi *n;
inxxx char (1) const;
inyyy packed (1) const;
end-pi;
SomeRoutine ( inxxx: inyyy );
// print results of call to the subprocedure
clear Printline;
PrintLine.xxx = inxxx ;
evalr PrintLine.yyy = %editc( inyyy : 'X' );
writeln (PrintLine);
end-proc;
dcl-proc SomeRoutine;
dcl-pi *n;
inxxx char ( 1 ) const;
inyyy packed ( 1 ) const;
end-pi;
end-proc SomeRoutine;
dcl-proc writeln;
dcl-pi *n;
inString varchar(LINE_LENGTH) const;
inPosition uns(3) const options(*nopass);
end-pi;
dcl-ds ReportLine len(LINE_LENGTH) end-ds;
dcl-s Position uns(3);
if %parms() >= %ParmNum(inPosition);
Position = inPosition;
else;
Position = 1;
endif;
%subst(ReportLine: Position) = inString;
write qsysprt ReportLine;
end-proc writeln;
SomeRoutineは、私が本番環境向けに開発しているプロシージャーのスタブです。適当な言葉が思い浮かばなかったので、私は「ターゲット サブプロシージャー」と呼んでいます。もっと良い名前があったら、ぜひ教えてください。
たとえば、商品を販売する会社の仕事をしているとします。それぞれの商品にはカタログ表示価格(リスト価格)がありますが、その価格は、必ずしも顧客が支払うことになる価格というわけではありません。商品を販売することが承諾される価格は、3つの基準によって決められます。
- 顧客は誰か。「タイプA」の顧客(収益の大半を占めている顧客)は、10%の割引を適用。「タイプB」の顧客(その顧客のビジネスも評価でき、取引を続けたい顧客)は、5%の割引を適用。「タイプC」の顧客(購入頻度の低い顧客または取引上好ましくない顧客)は、割引適用なし。
- 顧客が何点購入するか。1点、2点の購入の場合は割引なし。大量購入の場合は大量購入割引を適用。
- 顧客がどれくらい早い商品配達を希望しているか。翌日配達を希望の場合は、割増料金。
これは、サブプロシージャーにうってつけのタスクです。入力パラメーターは明らかです。すなわち、顧客ID、商品ID、注文数量、および期限です。このサブプロシージャーは、価格を返す必要がありますが、それは、パラメーターまたは戻り値を通じて行うことができます。また、このサブプロシージャーには、エラーが発生したことを示す方法も必要となります。これは、パラメーター、戻り値、またはエスケープ メッセージを通じて行えます。簡潔にするために、ここでは、すべてパラメーターを使用します。このサブプロシージャーの名前は、「CalculateUnitPrice」にしたいと思います。
dcl-proc CalculateUnitPrice;
dcl-pi *n;
inCustomerID like(CustomerID_t) const;
inProductID like(ProductID_t) const;
inOrderQuantity packed (7) const;
inDueDate date const;
ouPrice packed (7: 2);
ouErrorCode uns (3);
end-pi;
最初の2つのパラメーターは、コピーブックDATADEFS内のテンプレートがベースになっています。
dcl-s CustomerID_t packed (5) template;
dcl-s ProductID_t char (6) template;
したがって、それが、ターゲット サブプロシージャーのプロシージャー インターフェースです。このサブプロシージャーをテストするために、TryItサブプロシージャーから呼び出します。TryItには、通常、ターゲット サブプロシージャーと同じ入力パラメーターがあります。ターゲット サブプロシージャーの出力パラメーターは、TryIt内のローカル変数になります。
dcl-proc TryIt;
dcl-pi *n;
inCustomerID like(CustomerID_t) const;
inProductID like(ProductID_t) const;
inOrderQuantity packed (7) const;
inDueDate date const;
end-pi;
dcl-s Price packed (7: 2);
dcl-s ErrorCode uns (3);
CalculateUnitPrice ( inCustomerID: inProductID: inOrderQuantity: inDueDate:
Price: ErrorCode);
メイン ルーチンは、可能性のあるすべてのテスト ケースをカバーするパラメーターでTryItを呼び出す必要があります。通常は、最初からすべてのケースをテストするわけではありません。私は、 テスト駆動開発 (TDD: test-driven development)のような方式を採用しようと思います。つまり、ターゲット サブプロシージャーを一度に1部分ずつ開発するということです。まずは、入力パラメーターを検証するコードを書くことから始めます。
TryIt ( 0 : *blank : 0 : d'2000-01-01' );
TryIt ( 100 : *blank : 0 : d'2000-01-01' );
TryIt ( 100 : 'AB-101' : 0 : d'2000-01-01' );
return;
dcl-proc TryIt;
dcl-pi *n;
inCustomerID like(CustomerID_t) const;
inProductID like(ProductID_t) const;
inOrderQuantity packed (7) const;
inDueDate date const;
end-pi;
dcl-s Price packed (7: 2);
dcl-s ErrorCode uns (3);
CalculateUnitPrice ( inCustomerID: inProductID: inOrderQuantity: inDueDate:
Price: ErrorCode);
end-proc;
dcl-proc CalculateUnitPrice;
dcl-pi *n;
inCustomerID like(CustomerID_t) const;
inProductID like(ProductID_t) const;
inOrderQuantity packed (7) const;
inDueDate date const;
ouPrice packed (7: 2);
ouErrorCode uns (3);
end-pi;
clear ouPrice;
clear ouErrorCode;
// validate the parameters
if inCustomerID <= *zero;
ouErrorCode = 1;
return;
endif;
if inProductID = *blanks;
ouErrorCode = 2;
return;
endif;
end-proc CalculateUnitPrice;
これら2つのテストを実行するのに十分なコードです。
TryItの呼び出しの後、戻りコードは、1、2、および0となるはずです。価格は常に0になります。価格を設定するためのコードを書いていないからです。
本当にその通りだったかどうかは、どのようにして分かるのでしょうか。そこで、お気に入りのインタラクティブ デバッガーを立ち上げることもできます。それで何も問題はありません。私は、レポートを作成しようと思います。その手法については、2年前に記事を書いています(「 Guru: Quick And Handy RPG Output, Take 2」を参照)。
**free
dcl-c LINE_LENGTH 132;
dcl-f qsysprt printer(LINE_LENGTH);
dcl-ds PrintLine len(LINE_LENGTH) template;
CustomerID char ( 5);
*n char ( 1);
ProductID char ( 6);
*n char ( 1);
OrderQuantity char ( 8);
*n char ( 1);
DueDate char (10);
*n char ( 1);
Price char ( 9);
*n char ( 1);
ErrorCode char ( 3);
end-ds;
/copy copybooks,datadefs
*inlr = *on;
// print column headings
PrintLine.CustomerID = 'Cus';
PrintLine.ProductID = 'Prod';
evalr PrintLine.OrderQuantity = 'Qty ';
PrintLine.DueDate = 'Due';
evalr PrintLine.Price = 'Price ';
PrintLine.ErrorCode = 'Err';
writeln (PrintLine);
TryIt ( 0 : *blank : 0 : d'2000-01-01' );
TryIt ( 100 : *blank : 0 : d'2000-01-01' );
TryIt ( 100 : 'AB-101' : 1 : d'2000-01-01' );
return;
dcl-proc TryIt;
dcl-pi *n;
inCustomerID like(CustomerID_t) const;
inProductID like(ProductID_t) const;
inOrderQuantity packed (7) const;
inDueDate date const;
end-pi;
dcl-s Price packed (7: 2);
dcl-s ErrorCode uns (3);
CalculateUnitPrice ( inCustomerID: inProductID: inOrderQuantity: inDueDate:
Price: ErrorCode);
// Print the results of calling the subprocedure.
clear Printline;
evalr PrintLine.CustomerID = %editc( inCustomerID : 'X');
PrintLine.ProductID = inProductID ;
evalr PrintLine.OrderQuantity = %editc( inOrderQuantity : 'L');
PrintLine.DueDate = %char ( inDueDate );
evalr PrintLine.Price = %editc( Price : 'L');
PrintLine.ErrorCode = %char ( ErrorCode );
writeln (PrintLine);
end-proc;
dcl-proc CalculateUnitPrice;
dcl-pi *n;
inCustomerID like(CustomerID_t) const;
inProductID like(ProductID_t) const;
inOrderQuantity packed (7) const;
inDueDate date const;
ouPrice packed (7: 2);
ouErrorCode uns (3);
end-pi;
clear ouPrice;
clear ouErrorCode;
// validate the parameters
if inCustomerID <= *zero;
ouErrorCode = 1;
return;
endif;
if inProductID = *blanks;
ouErrorCode = 2;
return;
endif;
end-proc CalculateUnitPrice;
dcl-proc writeln;
dcl-pi *n;
inString varchar(LINE_LENGTH) const;
inPosition uns(3) const options(*nopass);
end-pi;
dcl-ds ReportLine len(LINE_LENGTH) end-ds;
dcl-s Position uns(3);
if %parms() >= %ParmNum(inPosition);
Position = inPosition;
else;
Position = 1;
endif;
%subst(ReportLine: Position) = inString;
write qsysprt ReportLine;
end-proc writeln;
以下が出力です。
Cus Prod Qty Due Price Err
00000 0 2000-01-01 .00 1
00100 0 2000-01-01 .00 2
00100 AB-101 1 2000-01-01 .00 0
価格(Price)とエラー コード(Err)を見てみてください。ここまでのところは、すべて問題ありません。
では、価格設定ルーチンの作成を続けましょう。必要な顧客データおよび商品データを取得するためのコードを追加します。
dcl-proc CalculateUnitPrice;
dcl-pi *n;
inCustomerID like(CustomerID_t) const;
inProductID like(ProductID_t) const;
inOrderQuantity packed (7) const;
inDueDate date const;
ouPrice packed (7: 2);
ouErrorCode uns (3);
end-pi;
dcl-s Price like(ouPrice);
dcl-s CustomerType char(1);
clear ouPrice;
clear ouErrorCode;
// validate the parameters
if inCustomerID <= *zero;
ouErrorCode = 1;
return;
endif;
if inProductID = *blanks;
ouErrorCode = 2;
return;
endif;
// Start with the list price.
exec sql
select p.price
into :Price
from products as p
where p.id = :inProductID;
if SqlState >= '02000';
ouErrorCode = 3;
return;
endif;
exec sql
select c.type
into :CustomerType
from customers as c
where c.id = :inCustomerID;
if SqlState >= '02000';
ouErrorCode = 4;
return;
endif;
ouPrice = Price;
end-proc CalculateUnitPrice;
追加のテスト ケースが必要です。
TryIt ( 0 : *blank : 0 : d'2000-01-01' );
TryIt ( 100 : *blank : 0 : d'2000-01-01' );
TryIt ( 100 : 'AB-101' : 1 : d'2000-01-01' );
TryIt ( 100 : 'smurf' : 1 : d'2000-01-01' );
TryIt ( 999 : 'smurf' : 1 : d'2000-01-01' );
TryIt ( 999 : 'AB-101' : 1 : d'2000-01-01' );
顧客「100」および商品「AB-101」は有効なデータで、「999」および「smurf」はそうではないのだとすれば、ここまでのところ、これらのテストはあらゆる可能性に備えられていると言えます。
Cus Prod Qty Due Price Err
00000 0 2000-01-01 .00 1
00100 0 2000-01-01 .00 2
00100 AB-101 1 2000-01-01 1.80 0
00100 smurf 1 2000-01-01 .00 3
00999 smurf 1 2000-01-01 .00 3
00999 AB-101 1 2000-01-01 .00 4
すべてのエラー コードが的確で、実際に、取得された価格は、品目「AB-101」のカタログ価格のみでした。ここまでのところ、問題はなさそうです。
考え方としてはすべてカバーできたので、ここで終わりにしようと思います。これが本番プログラムだったとしたら、割引を計算するためのコードを追加するでしょう。また、それぞれのコード片をテストするために、必要なTryItの呼び出しを追加します。最終的には、私の望んだ通りに正確に動作する、バグのない価格設定ルーチンが出来上がるでしょう。グローバル データがないので、望ましくない副作用が引き起こされる心配なく、プログラムまたはサービス プログラムに組み込むことができます。
「ちょっと待った」という声が聞こえてきそうです。「最初の2つのパラメーターは、グローバル データで定義されているのではないでしょうか」 答えは「ノー」です。それらはグローバルな データ定義をベースにしています。そして、グローバルなデータ定義は、特にコピーブック内にある場合は、ソフトウェア アプリケーション全体を通じて一貫してデータを定義するための素晴らしい手法です。
「じゃあ、」と続きます。「QSYSPRTファイルの使用はグローバルではないのでしょうか」 はい、もちろんそうです。しかし、それによってコードがよりシンプルになり、私はターゲット サブプロシージャーを作成する作業に集中させてもらえるようになります。グローバルなプリンター ファイルであることから、そうする必要があるのなら、ターゲット サブプロシージャー内にwriteln呼び出しを追加することができます。もちろん、移動先となる本番ソース メンバーにターゲット サブプロシージャーを移動させる前には、それらを削除します。
TryItテンプレートのコピーにサブプロシージャーを組み込むことで、プログラムの他の部分に影響を及ぼすことなく、行う必要があることを正確に行うサブプロシージャーの開発が容易になり、可能性のあるすべての入力値で簡単にコードをテストできるようになります。これで心の平安が得られます。
その上、私には、仕事中にモグラ叩きゲームで遊んでいる暇はありません。