RPGの内部構造を改善(リファクタリング)する - GOTO
初めてCOBOLを学んだ頃、私はショップ内のプログラマー全員が行っているのと同じやり方で、ループをコーディングしていました。すなわち、GOTOの使用です。段落名はラベルであり、ルーチンではありませんでした。その後、私はCOBOLのクラスを受講し、構造化プログラミングを学びました。もう、振り返ることはありませんでした。他の人々も同じように思っていてくれればよいのにと思います。GOTOがいっぱいのプログラムは、触りたくないからです。
分岐が無分別に使用されているというのが(RPGではGOTOおよびCABxx命令コードでしょうか)、私がリファクタリングを行うときの大きな理由です。GOTOは、プログラムの「ロジック」(この文脈で使用するのが躊躇される言葉ですが)をめちゃめちゃにしてしまいます。ポップアップ ウィンドウを挿入してほしい、あるいはプログラムの流れを変更してほしい、というような依頼を受けた途端に、すべてが崩壊してしまうのです。
以下では、分岐命令をより堅牢なプログラミング作法に置き換える方法をいくつか紹介します。ここではRPGを例に使用していますが、これらは他の言語、特にCOBOLやCLのような手続き型言語にも当てはめることができます。
1. 伝統的パターン
まずは、伝統的パターンを探して、分岐を構造化命令コードに置き換えることから始めましょう。伝統的パターンおよびそのRPG命令コードには、以下のものがあります。
- トップテスト ループ(DOW)
- ボトムテスト ループ(DOU)
- 選択
- 単純な選択(IF、IF/ELSE、IF/ELSEIF/ELSE)
- Case構造(SELECT)
すべての最新の言語がこれらのパターンをサポートしていると言っても大丈夫だと思います。あと2つのパターンをリストに追加しておこうと思います。
- カウント付きループ(FOR、DO)
- ミドルテスト ループ(RPGでは実装されていない)
一般に、コードの後方へ分岐するのがループで、前方へ分岐するのが選択と言えるでしょう。
さて、ここで問題です。以下はどちらのパターンに当てはまるでしょうか。
C CHGCOD CABNE 1 TAG200
C MOVE . . .
C MOVE . . .
C ADD . . .
C MOVE . . .
C GOTO TAG220
C TAG200 TAG
C MOVE . . .
C MOVE . . .
C SUB . . .
C MOVE . . .
C TAG220 TAG
選択、if/then/else、などと答えた方には、1ポイントを差し上げます。確かに、これは選択です。簡単に次のように変換できます。
C CHGCOD IFEQ 1
C MOVE . . .
C MOVE . . .
C ADD . . .
C MOVE . . .
C ELSE
C MOVE . . .
C MOVE . . .
C SUB . . .
C MOVE . . .
C ENDIF
いくつかのMOVEと、あのADDと、あのSUBをEVALに変更すれば、フリーフォームのコードまでほんのあと一歩です。
では、これはどうでしょうか。
C Z-ADD1 NX
C NXTOPT TAG
C OPT,NX CASEQ'A' ADDSR
C OPT,NX CASEQ'C' CHGSR
C OPT,NX CASEQ'D' DELSR
C END
C EXCPTE1
C ADD 1 NX
C NX CABLE12 NXTOPT
ボトムテスト ループと答えた方に、1ポイントです。正しい答えです。ただ、よく見てみると、NXが1から12までの範囲の値になるため、カウント付きループであることが分かると思います。したがって、カウント付きループと答えた方には2ポイントを差し上げます。
C FOR NDX = 1 to 12
C OPT(NDX) CASEQ 'A' ADDSR
C OPT(NDX) CASEQ 'C' CHGSR
C OPT(NDX) CASEQ 'D' DLTSR
C END
C EXCEPT E1
C ENDFOR
では、以下はどのパターンに当てはまるでしょうか。
C RD100 TAG
C READ QCUSTCDT 44
C *IN44 CABEQ *ON RD110
C MOVE . . .
C MOVE . . .
C MOVE . . .
C MOVE . . .
C EXCEPT E1
C GOTO RD100
C RD110 TAG
ミドルテスト ループだと分かったでしょうか。READは、少なくとも1回は実行する必要があります。MOVEおよびEXCPTは、1回も実行されない可能性があります。RPG、CL、およびCOBOLプログラムは、この種のループでいっぱいです。プログラマーがこのように考えるからです。伝統的パターンのリストにミドルテスト ループを追加するのは、こういうわけなのです。
そのようなループの中に、入出力命令(特にEXFMTおよび各種のREADx命令コード)や、走査命令(SCAN命令コード、%SCAN関数、および各種の表および配列の検索関数および命令コードなど)を見つけることがよくあります。 ミドルテスト ループの実用性についてはすでに記事に記していますので、ここで改めて説明することはしません。
このコードは、次のように簡単に変換できます。
C DOW '1'
C READ QCUSTCDT
C IF %EOF()
C LEAVE
C ENDIF
C MOVE . . .
C MOVE . . .
C MOVE . . .
C MOVE . . .
C EXCEPT E1
C ENDDO
よく使用されているgotoのパターンと、構造化プログラミングにおけるそれらの代替物を表にまとめてみました。
パターン | 構造化プログラミングでの代替物 |
---|---|
A: if 条件 goto B . . . goto A |
トップテスト ループ(do while) |
A: . . . if 条件 goto A |
ボトムテスト ループ(do until) |
A: . . . if 条件 goto B . . . goto A B: |
ミドルテスト ループ |
A: . . . if 条件 goto A . . . if 条件 goto A . . . if 条件 goto A . . . goto A |
ITERを使用したループ |
. . . if 条件 goto A . . . if 条件 goto A . . . if 条件 goto A . . . A: |
case構造(SELECT) |
if 条件 . . . goto A if . . . . . . goto A . . . A: |
case構造(SELECT) |
if 条件 goto A . . . A: |
if/then |
if 条件 goto A . . . goto B A: . . . B: |
if/then/else |
2. 無分別な分岐
すべてのコードが、これらのパターンに当てはまるわけではありません。やはり、「スパゲッティ コード」と呼ばれるだけのことはあります。別の例を示します。これについてはどうしましょうか。
C OPTION CABNE '1' TAG300
. . .
C TAG300 TAG
これをif/then構造に変換しましょうか。いや、ちょっと待ちましょう。例の中では3つのドットで示されていますが、そこにはどれくらいの行数があるでしょうか。行数が少しだけなら、イエスであり、そのような変換はおそらく妥当と言えるでしょう。行数が多いようなら、ノーであり、あるいは、少なくともすぐに行うべきではありません。
「少し」や「多い」は、主観的な言い方でした。私の場合、IFと対応するENDIFの間の行数は、画面上で容易に表示できる行数に収めるようにしています。25行以下くらい、せいぜい50行でしょうか(しっくりこないと思われるようでしたら、これは明確なルールということではなく、好みの問題くらいに考えておいてください)。ループについても同じです。
それらの3つのドットが表しているのが数十行あるいは数百行だとしたら、まず行数を減らしてからGOTOに取り組むことを考えてみます。コードを1つまたは複数のルーチンに書き換えることを試みるわけです。これについても以前の記事に記しています。
このケースでは、GOTOとTAGの間の行を、次のようにサブルーチンに移動するとよいかもしれません。
C OPTION IFEQ '1'
C EXSR OPTION1
C ENDIF
. . .
C OPTION1 BEGSR
. . . (the code that followed the CABNE goes here)
C ENDSR
そのようなサブルーチンを作成できるかどうかは、それらのコード行にどのようなものがあるか次第です。ほとんどの場合、これで済みます。ただし、すべてをめちゃくちゃにしてしまうものがあります。お察しの通り、GOTOです。その例についてもう少し詳しく見てみましょう。
C OPTION CABNE '1' TAG300
C ADD . . .
C MOVE . . .
C CTYPE IFNE 1
C MOVE . . .
C GOTO SKIP1
C ENDIF
C MOVE . . .
C MOVE . . .
C SKIP1 TAG
C MOVE . . .
C MOVE . . .
C MOVE . . .
C TAG300 TAG
GOTO/TAGのペアがもうひとつありますが、それらは新しいルーチンの中にあります。最初の行(CABNEの行)の前または最後の行(TAG300)の後への分岐はありません。プログラム内の他のどこからもSKIP1への分岐がない場合は、新しいサブルーチンを作成できます。
C IF OPTION = 1
C EXSR OPTION1
C ENDIF
. . .
C OPTION1 BEGSR
C ADD . . .
C MOVE . . .
C CTYPE IFNE 1
C MOVE . . .
C GOTO SKIP1
C ENDIF
C MOVE . . .
C MOVE . . .
C SKIP1 TAG
C MOVE . . .
C MOVE . . .
C MOVE . . .
C ENDSR
しかし、CABNEとTAGの間のコードが範囲外へ分岐するとしたらどうでしょうか。希望が失われるでしょうか。そんなことはありません。範囲外の分岐によって、ルーチンの作成が機械的に妨げられるわけではありません。以下を検討してみてください。
C TAG120 TAG
. . .
C OPTION CABNE 1 TAG300
C ADD . . .
C MOVE . . .
C CTYPE IFNE 1
C MOVE . . .
C GOTO TAG120
C ENDIF
C MOVE . . .
C MOVE . . .
C TAG300 TAG
1つ問題があります。新しいサブルーチン内からTAG120へ制御が分岐する可能性があります。サブルーチン内および外への分岐は御法度です。次のようにすれば簡単に回避できます。
D TAGNAME S 10
. . .
C TAG120 TAG
. . .
C IF OPTION = 1
C EXSR OPTION1
C TAGNAME CABEQ 'TAG120' TAG120
C ENDIF
. . .
C OPTION1 BEGSR
C EVAL TAGNAME = *BLANKS
C ADD . . .
C MOVE . . .
C CTYPE IFNE 1
C MOVE . . .
C EVAL TAGNAME = 'TAG120'
C LEAVESR
C ENDIF
C MOVE . . .
C MOVE . . .
C ENDSR
私の場合、TAGNAMEという10バイトの文字変数を定義し、サブルーチンを呼び出した後で分岐を制御するのにその変数を使用しています。TAGNAME変数が1つあれば、プログラム内のすべてのそのようなケースを処理するのに十分です。
私はサブルーチンに新たな先頭行を追加して、TAGNAMEをブランクにセットしました。OPTION1サブルーチンがTAG120へ分岐する必要がある場合は、TAGNAME変数に適切な値(TAGという名前をそのまま使用します)をロードし、サブルーチンを終了します。呼び出し元への復帰と同時に、いつもそうしていたように分岐を制御します。
私はサブルーチンに新たな先頭行を追加して、TAGNAMEをブランクにセットしました。OPTION1サブルーチンがTAG120へ分岐する必要がある場合は、TAGNAME変数に適切な値(TAGという名前をそのまま使用します)をロードし、サブルーチンを終了します。呼び出し元への復帰と同時に、いつもそうしていたように分岐を制御します。
3. 潔癖になることはない
これまでのところ、私はこれらのシンプルな手法でうまくやってこられています。読者の皆さんのケースでもうまく行くことに間違いないと思います。GOTOを除去するリファクタリングは、思想信条上の理由から行っているわけではありません。新たな要件に対処するためにプログラム ソース コードの変更が必要になった場合に、GOTOが使用されているせいでそれが邪魔されるときに、GOTOを除去するだけです。そうでなければ放っておきます。今うまく動いているコードからGOTOをすべて除去する必要があるわけではありません。ネズミやゴキブリと違って、決して増えるものではないのですから。