はじめに
レガシーモダナイゼーションが活況である。レガシーモダナイゼーションは、古いシステムの近代化なので、その中身はいろいろだが、その中の一つの大きなテーマとして、COBOLからJavaへのストレートコンバージョンがある。コンバージョンされたJavaプログラムは、当然ながらもとのCOBOLのソースの面影を色濃く残しており、JABOLと揶揄されたりしている。
ただ、JABOLとは何かということを本当に理解している方は少ないと思われる。中には、「Javaはオブジェクト指向だけど、COBOLはそうじゃないから、変換した結果はオブジェクト指向ではないけど、それくらいは仕方ないよね」と考えている人もいる。正直言ってそんなレベルではなく、単純変換では、普通のJavaのプログラマの感覚からすると全く受け入れられないものになる。
ここでは、特定のソリューションではなく、純粋にCOBOLとJavaの言語仕様に焦点をあてて、単純な変換では、JABOLがどのようなものになるのかを説明したい。また、最後にのところで述べるが、ソリューションによっては、ここで述べている言語仕様の差異による制約の一部、または多くを解決しているものもあることは触れておきたい。
以下では、言語仕様として、プログラムの構造、固定長文字列、COPY句、GOTO文に焦点をあてて、説明したい。
なお、本稿は筆者の個人的見解であり、所属組織の公式見解や立場を示すものでは一切ありません。了承の上、お読みいただけますと幸いです。
プログラムの構造
COBOLのプログラム構造
COBOLの典型的なプログラム構造をソース1に示す。
ソース1: COBOLの典型的なプログラム構造
IDENTIFICATION DIVISION.
PROGRAM-ID. SAMPLE-PROGRAM.
DATA DIVISION.
WORKING-STORAGE SECTION.
* プログラムで利用する変数をすべて定義
01 CUSTOMER-ID PIC X(10).
01 CUSTOMER-NAME PIC X(50).
01 ORDER-AMOUNT PIC 9(10).
01 WORK-COUNTER PIC 9(5).
LINKAGE SECTION.
* 他プログラムから呼び出されるときの引数
01 INPUT-PARAM.
05 PARAM-ID PIC X(10).
01 OUTPUT-PARAM.
05 RESULT-CODE PIC X(2).
05 RESULT-MESSAGE PIC X(100).
PROCEDURE DIVISION USING INPUT-PARAM OUTPUT-PARAM.
PERFORM INITIALIZE-SECTION.
PERFORM MAIN-PROCESSING-SECTION.
PERFORM FINALIZE-SECTION.
STOP RUN.
INITIALIZE-SECTION.
MOVE SPACES TO WORK-AREA.
MOVE '00' TO RESULT-CODE.
EXIT.
MAIN-PROCESSING-SECTION.
MOVE PARAM-ID TO CUSTOMER-ID.
* メイン処理
PERFORM CALCULATE-SECTION.
EXIT.
CALCULATE-SECTION.
ADD 1 TO WORK-COUNTER.
* 計算処理
EXIT.
FINALIZE-SECTION.
MOVE 'Processing completed' TO RESULT-MESSAGE.
EXIT.
Data Divisionにプログラムで利用する変数をすべて記載する。Java開発者の視点では、クラスのフィールド変数をすべて最初に宣言するようなイメージである。
Linkage Sectionはサブプログラム用の領域で、ここに他から呼び出されるときの引数を記載する。Javaのメソッドの引数に相当するが、COBOLでは呼び出し元と呼び出し先でメモリ領域を共有する「参照渡し」が基本となる。
Procedure Divisionには、処理を記述する。ここに書かれたPERFORM 節名は、Javaでいうメソッド呼び出しに相当する。Procedure Divisionは処理単位ごとにsection(節)に分けられることが多い。
COBOLとJavaのプログラム構造の対応
これをJavaの言語構造に素直に対応させるとソース2のようになる。
ソース2: COBOLプログラムをJavaに対応させた例
public class SampleProgram {
// Data Division → インスタンス変数
private String customerId;
private String customerName;
private int orderAmount;
private int workCounter;
// Linkage Section → public関数の引数として定義
// (InputParam, OutputParamクラスは別途定義)
// Procedure Division → public関数とsectionごとのprivate関数
public void execute(InputParam inputParam, OutputParam outputParam) {
initializeSection();
mainProcessingSection();
finalizeSection();
}
// 各section → 引数なしのprivate関数
private void initializeSection() {
workArea = new byte[100];
customerId = "";
customerName = "";
orderAmount = 0;
workCounter = 0;
}
private void mainProcessingSection() {
// メイン処理
calculateSection();
}
private void calculateSection() {
workCounter++;
// 計算処理
}
private void finalizeSection() {
// 終了処理
}
}
COBOLの言語要素は、以下のようにJavaに対応する。
- Data Division → Javaクラスのインスタンス変数として定義
- Linkage Section → public関数の引数として定義
- Procedure Division全体 → 一つのpublicメソッドとして実装
- 各Section → 引数なしのprivateメソッドとして実装
プログラム構造の違いにおける闇
このJavaのプログラムを見て、どのように感じられるであろうか。COBOL視点では、素直に対応できていると見えるかもしれない。逆にJava視点だとどうか。
- アプリケーションで使う変数がすべてインスタンス変数となっており、ローカル変数が利用されていない
- 全変数のスコープがクラス全体となり、影響範囲が見通しづらくなる。private関数には引数がないので単体テストもままならない
問題はプログラムの可読性にとどまらない。仮に、コンバージョンプロジェクトでなく、一般的なJavaのプロジェクトでこのようなプログラムを作成したら、深刻なセキュリティインシデントやGCの多発による性能問題を発生させる可能性があり、とても許容できるものではないだろう。
このように、COBOLとJavaのプログラム構造を対応させることはできるが、単純に対応させるだけでは、Javaとしては、かなり問題のあるプログラムとなる。
固定長文字列
COBOLのすべてのデータ項目は、プログラム作成時にそのサイズが固定される固定長であるのに対し、Javaの文字列は、実行時にサイズが変えられる可変長である。一見、「固定長から可変長」と制約が少ないほうに変換されるので問題が無いように感じられるかもしれない。しかしながら、固定長だからできることがあり、通常のCOBOLのプログラムではそれを多用している。
COBOLの固定長の仕組み
Javaとは異なり、COBOLの変数はメモリ上の連続した領域として確保される。COBOLでは、以下のように領域が確保される。
01 CUSTOMER-RECORD. * 合計75バイト
05 CUSTOMER-ID PIC X(10). * 0-9バイト目
05 CUSTOMER-NAME PIC X(50). * 10-59バイト目
05 CUSTOMER-TEL PIC X(15). * 60-74バイト目
このCUSTOMER-RECORDは、集団項目と呼ばれ、CUSTOMER-ID、CUSTOMER-NAME、CUSTOMER-TELを要素として持つ構造であり、メモリ上で75バイトの連続した領域として確保される。重要なのは、COBOLでは集団項目(構造体)への代入は、メモリのバイト列をそのままコピーするという点である。これがJavaの構造体(クラス)のコピーとは根本的に異なる。
項目名が同じ場合の代入
ソース3はCOBOLの構造をもった変数を代入する例である。このように1行で、構造をもった値を代入(コピー)することができる。対して、Javaの場合は、項目名称が同じであれば、MapStructなどのライブラリを活用して、ソース4のように書くことができる。
ソース3: COBOLの集団項目(項目名称同じ)のMOVE例
DATA DIVISION.
WORKING-STORAGE SECTION.
01 SOURCE-CUSTOMER.
05 CUSTOMER-ID PIC X(10).
05 CUSTOMER-NAME PIC X(50).
05 CUSTOMER-TEL PIC X(15).
01 TARGET-CUSTOMER.
05 CUSTOMER-ID PIC X(10).
05 CUSTOMER-NAME PIC X(50).
05 CUSTOMER-TEL PIC X(15).
PROCEDURE DIVISION.
* 一行で構造全体をコピー
MOVE SOURCE-CUSTOMER TO TARGET-CUSTOMER.
ソース4: Javaでの対応(MapStruct使用)
// DTOクラス定義
public class Customer {
private String customerId;
private String customerName;
private String customerTel;
// getter/setterは省略
}
// MapStructマッパー定義
@Mapper
public interface CustomerMapper {
CustomerMapper INSTANCE = Mappers.getMapper(CustomerMapper.class);
Customer copy(Customer source);
}
// 使用例
Customer sourceCustomer = new Customer();
sourceCustomer.setCustomerId("C001");
//・・・・
//一行で構造全体をコピー。項目名が同じため実装できる
Customer targetCustomer = CustomerMapper.INSTANCE.copy(sourceCustomer);
項目名が異なる場合でも代入可能(COBOLの特徴)
ただし、COBOLの場合は、サイズさえ合っていればソース5のように、項目名称が異なっていても問題なく代入できる。これは、先述のとおりメモリのバイト列をそのままコピーするからである。Javaでいえば、System.arraycopy()でバイト配列をコピーするのに近い。
ソース5: COBOLの集団項目(項目名称異なる)のMOVE例
DATA DIVISION.
WORKING-STORAGE SECTION.
01 SOURCE-RECORD.
05 SRC-CODE PIC X(10).
05 SRC-DESC PIC X(50).
05 SRC-PHONE PIC X(15).
01 TARGET-RECORD.
05 TGT-ID PIC X(10).
05 TGT-NAME PIC X(50).
05 TGT-NUMBER PIC X(15).
PROCEDURE DIVISION.
* 項目名が異なっても、サイズが同じ(各75バイト)なら代入可能
MOVE SOURCE-RECORD TO TARGET-RECORD.
* 結果: メモリの75バイトがそのままコピーされる
* 位置0-9: SRC-CODE → TGT-ID
* 位置10-59: SRC-DESC → TGT-NAME
* 位置60-74: SRC-PHONE → TGT-NUMBER
Java開発者の視点では信じられないかもしれないが、COBOLではこれが当たり前の動作である。項目名は単なる「メモリ位置へのエイリアス」であり、実際の代入はバイト単位で行われる。
構造が異なる場合でも代入可能
さらに言えば、項目数に差異があっても、代入ができる(ソース6)。
ソース6: COBOLの集団項目(構造異なる)のMOVE例
DATA DIVISION.
WORKING-STORAGE SECTION.
01 DATE-YYYYMMDD PIC X(8).
* 例: "20251106"
01 DATE-PARTS.
05 YEAR PIC X(4).
05 MONTH PIC X(2).
05 DAY PIC X(2).
PROCEDURE DIVISION.
MOVE "20251106" TO DATE-YYYYMMDD.
* 同じ8バイトなので、構造が異なっても代入可能
MOVE DATE-YYYYMMDD TO DATE-PARTS.
* 結果: メモリの8バイトがそのまま分割して解釈される
* YEAR = "2025" (0-3バイト目)
* MONTH = "11" (4-5バイト目)
* DAY = "06" (6-7バイト目)
この例を見れば、COBOLの固定長の威力がわかるだろう。日付を"20251106"という8バイトの文字列で持っておけば、必要に応じて年月日に分解したり、一括で扱ったりできる。これはメモリレイアウトが固定されているからこそ可能な技法である。Javaの一般的なクラスの場合、項目名や構造が異なる場合、シンプルにこのような実装はできない
なぜ異なる変数名をつけるのか
データ構造が同じなのに、なぜわざわざ違う名前を付けるのか。COBOLの場合、変数名が異なっていれば、構造を持っている変数でも、構造の指定を省略できるなど、言語仕様上、異なる変数名をつけることに一定の合理性があるのである。
そのため、構造が同じで異なる変数名というデータ構造は、ソースコード上、それなりの頻度で登場する。そのため、単純なコンバージョンでは、異なる項目名でも対処できる必要がある。
Javaでの固定長実装
このような固定長だからできる処理をJavaで実装する方法としてとれるのが、Java側の変数をStringでなくByte配列とする方法である。COBOLの変数全体で確保されるのと同じサイズのByte配列をJava側で確保し、Byte配列上の変数の開始位置とサイズをGetterやSetterで指定することで、COBOLと同じようなコードを書くことが可能になる(ソース7)。
ソース7: Byte配列を使った固定長実装
// SOURCE-RECORD相当のクラス
public class SourceRecord {
// 75バイトのByte配列(ソース5のCOBOLレコードと同じサイズ)
private byte[] buffer = new byte[75];
// SRC-CODE (位置0-9, 10バイト)
public String getSrcCode() {
return new String(buffer, 0, 10).trim();
}
public void setSrcCode(String value) {
byte[] bytes = String.format("%-10s", value).getBytes();
System.arraycopy(bytes, 0, buffer, 0, 10);
}
// SRC-DESC (位置10-59, 50バイト)
public String getSrcDesc() {
return new String(buffer, 10, 50).trim();
}
public void setSrcDesc(String value) {
byte[] bytes = String.format("%-50s", value).getBytes();
System.arraycopy(bytes, 0, buffer, 10, 50);
}
// SRC-PHONE (位置60-74, 15バイト)
public String getSrcPhone() {
return new String(buffer, 60, 15).trim();
}
public void setSrcPhone(String value) {
byte[] bytes = String.format("%-15s", value).getBytes();
System.arraycopy(bytes, 0, buffer, 60, 15);
}
// バッファ全体を取得(MOVE操作用)
public byte[] getBuffer() {
return buffer;
}
}
// TARGET-RECORD相当のクラス
public class TargetRecord {
// 75バイトのByte配列
private byte[] buffer = new byte[75];
// TGT-ID (位置0-9, 10バイト)
public String getTgtId() {
return new String(buffer, 0, 10).trim();
}
public void setTgtId(String value) {
byte[] bytes = String.format("%-10s", value).getBytes();
System.arraycopy(bytes, 0, buffer, 0, 10);
}
// TGT-NAME (位置10-59, 50バイト)
public String getTgtName() {
return new String(buffer, 10, 50).trim();
}
public void setTgtName(String value) {
byte[] bytes = String.format("%-50s", value).getBytes();
System.arraycopy(bytes, 0, buffer, 10, 50);
}
// TGT-NUMBER (位置60-74, 15バイト)
public String getTgtNumber() {
return new String(buffer, 60, 15).trim();
}
public void setTgtNumber(String value) {
byte[] bytes = String.format("%-15s", value).getBytes();
System.arraycopy(bytes, 0, buffer, 60, 15);
}
// COBOL風のMOVE操作(バイト列をそのままコピー)
public void moveFrom(SourceRecord source) {
System.arraycopy(source.getBuffer(), 0, this.buffer, 0, buffer.length);
}
}
// 使用例
SourceRecord source = new SourceRecord();
source.setSrcCode("CODE001");
source.setSrcDesc("商品説明");
source.setSrcPhone("03-1234-5678");
TargetRecord target = new TargetRecord();
target.moveFrom(source); // バイト列をそのままコピー
// 結果: 項目名が異なっても、メモリの内容がそのままコピーされる
// source.getSrcCode() → target.getTgtId()
// source.getSrcDesc() → target.getTgtName()
// source.getSrcPhone() → target.getTgtNumber()
この例では、getterやsetterの定義が少々煩雑であるが、ライブラリなどを工夫することで、もっとシンプルに記述することは可能と思われる。COBOLの他のデータ型を含め、基本的には、こうすれば、固定長および固定長であるためにできている異なる変数名があっても一括代入ができることの実装は可能である。ただし、この方法では、変換の結果のJavaプログラムが固定長の文字列という、もともとJavaにはない大きな制約を抱え込んでしまうという問題がある。
固定長文字列の根本的な問題
固定長の場合、たとえば、文字列項目のサイズを変更する際には、Javaであれば、入出力時のサイズ(画面上のサイズや入力チェック時のサイズ)とDBへの格納サイズを変更するくらいで済むような処理でも、その項目の代入先の変数のサイズ、その先の代入先の変数のサイズ、その変数が所属する領域の全体サイズなど多くの影響範囲が出る。変数のサイズを間違えると、深刻な障害の原因になりうる。
プログラミング中に文字列のサイズを気にしなくてよいというのは、プログラミングの生産性だけでなく、開発したシステムの安全性を高める上での大きな進歩である。それにも関わらず、その進歩を完全に無かったことにしてシステムをモダナイズして本当に意味があるといえるのか。自分の経験では、可変長文字列になってプログラミングのストレスは大きく減った。固定長の世界には戻れない。
COPY句
COBOLには、COPY句という共通化のための仕組みがある。このCOPY句の仕組みは単純で、COPY句で指定したファイルを、その部分にテキスト展開するというものである(ソース8)。
これは、Cの#includeと同様の機能であり、ビルド時にファイルの内容がそのまま埋め込まれる。JavaにはImportがあるが、これは「参照」であって「展開」ではない点が異なる。
このような機能はPL/IやCなど古い言語には存在するが、Javaをはじめ、最近の言語では実装されていない。そのため、COPY句をJavaで実装するには工夫が必要である。
ソース8: COPY句の例
* --- CUSTOMER-REC.cpy (COPYブック) ---
01 CUSTOMER-RECORD.
05 CUSTOMER-ID PIC X(10).
05 CUSTOMER-NAME PIC X(50).
05 CUSTOMER-TEL PIC X(15).
05 CUSTOMER-EMAIL PIC X(100).
* --- メインプログラム (展開前) ---
DATA DIVISION.
WORKING-STORAGE SECTION.
COPY CUSTOMER-REC.
* --- メインプログラム (展開後) ---
DATA DIVISION.
WORKING-STORAGE SECTION.
01 CUSTOMER-RECORD.
05 CUSTOMER-ID PIC X(10).
05 CUSTOMER-NAME PIC X(50).
05 CUSTOMER-TEL PIC X(15).
05 CUSTOMER-EMAIL PIC X(100).
COPY句の実装方法とその問題
COPY句を実装する最も簡単な方法は、COPY句を展開した後のCOBOLをJavaに変換するというものである。これであればCOPY句を気にする必要はなくなる。ただし、この方法では、共通化というCOPY句の利点はなくなるし、そもそも変換後のコードのステップ数が、数倍以上に膨れ上がってしまうため、通常は許容できない。
COPY句に対処する方法として次に考えられるのが、COPY句の部分をJavaのData Transfer Object(DTO)クラスとして実装する方法である。COPY句の通常の使い方は、データ構造の共通化であり、多くのCOPY句はその用途で使われている。そのような場合は、Java側では、DTOクラスとして実装することで共通化できることが多いが、例外も存在する。
COPY REPLACINGの問題
JavaのDTOとして共通化できない例に、COPY REPLACINGがある(ソース9)。これは、COPY句によりテキスト展開すると同時に、展開された文字列をREPLACINGで指定された内容に置き換えるというものである。
これはC言語のマクロに近い概念である。テンプレートのようなものを展開時に文字列置換する。Javaのジェネリクスとは全く異なり、単純な文字列置換である。
主に項目名称を書き換えるために利用される。参照先ごとに項目名称が異なってしまうので、単純にDTOとして共通化することはできない。
ソース9: COPY REPLACING の例
* --- COMMON-REC.cpy (COPYブック) ---
01 :PREFIX:-RECORD.
05 :PREFIX:-ID PIC X(10).
05 :PREFIX:-NAME PIC X(50).
05 :PREFIX:-STATUS PIC X(1).
* --- メインプログラム (展開前) ---
DATA DIVISION.
WORKING-STORAGE SECTION.
COPY COMMON-REC REPLACING ==:PREFIX:== BY ==CUSTOMER==.
COPY COMMON-REC REPLACING ==:PREFIX:== BY ==PRODUCT==.
* --- メインプログラム (展開後) ---
DATA DIVISION.
WORKING-STORAGE SECTION.
01 CUSTOMER-RECORD.
05 CUSTOMER-ID PIC X(10).
05 CUSTOMER-NAME PIC X(50).
05 CUSTOMER-STATUS PIC X(1).
01 PRODUCT-RECORD.
05 PRODUCT-ID PIC X(10).
05 PRODUCT-NAME PIC X(50).
05 PRODUCT-STATUS PIC X(1).
ご覧の通り、1つのテンプレート(COMMON-REC.cpy)から、項目名が異なる2つの構造体(CUSTOMER-RECORDとPRODUCT-RECORD)が生成されている。これをJavaのDTOとして実装するには、結局2つの別々のクラスを定義する必要があり、共通化の意味がなくなる。
無理やりJavaで実装する方法
共通のDTOとは別の実装方法として、アノテーションプロセッサを使って、ソースコードを書き換えるということも考えられる。アノテーションプロセッサはLombokなどで使われている手法で、ビルド時にソースコードへ要素を埋め込むことができる(図1)。これを使って、テキスト展開と同じような処理を行うことはできるかもしれない。
図1 アノテーションプロセッサを使ったCopy句の実装イメージ

ただ、そこまで言語仕様をCOBOLに寄せてまでJavaに変換するのが正しいのかどうかは疑問である。
なぜ最近の言語にはテキスト展開がないのか
そもそも、最近の言語で、テキスト展開による共通化が実装されていないのは、副作用が大きすぎるからである。例えば以下のような問題を引き起こす。
- 名前の衝突: COPY句に項目を追加することになったら、それが使われているすべてのプログラムで、追加する項目と同じ名前の変数が使われていないことを確認しなければならない
- 再コンパイル: COPY句を変更すると、変更には関係なくても、それを使っているプログラムはすべて再コンパイルしなければならない
- スコープの混乱: 展開されたコードがどこから来たのか、実行時には分からない
過去には多くの言語で採用されてたが、最近の言語では採用されていない機能を、あえて復活させることの是非を考えるべきであろう。
GOTO文
GOTO文も、古いプログラム言語にはあるが、最近の言語にはない機能である。初期のプログラミング言語では必須の機能だったが、プログラムが分かりにくくなるという理由で、最近の言語では実装されていない。
COBOLの場合、40年ほど前の規格で、GOTO文を使わなくてもそれなりにプログラムが書けるようになったので、現存しているCOBOLプログラムには、GOTO文を使っていなかったり、エラー時に処理を抜けるといった限られた範囲でのみ利用しているシステムは多い。しかしながら、それ以前のシステムや、その時点のプログラミングスタイルを継続しているシステムではGOTO文は多用されていると思われる。
限定的なGOTO使用は対応可能
エラー時、もしくは、一定の条件下で処理を完了させるという用途でのみGOTO文を使っている場合は、Javaの例外やreturn文、break文で代替できる:
// COBOLのエラー時GOTO → Javaの例外
try {
validateInput();
processData();
saveResult();
} catch (ValidationException e) {
handleError(); // GOTO ERROR-PROCESS相当
return;
}
// COBOLのループ脱出GOTO → Javaのbreak
for (Record rec : records) {
if (rec.shouldStop()) {
break; // GOTO END-LOOP相当
}
process(rec);
}
アプリケーションロジックでのGOTO使用は深刻
問題は、アプリケーションの処理ロジックにGOTO文を利用しているケース(ソース10)である。
ソース10: GOTO文を使った処理フロー制御
PROCEDURE DIVISION.
MAIN-PROCESS.
PERFORM INPUT-CHECK.
IF INPUT-ERROR
GO TO END-PROCESS.
PERFORM CALCULATION.
IF CALC-RESULT < 0
GO TO STEP-B
ELSE
GO TO STEP-A.
STEP-A.
DISPLAY 'Processing Step A'.
PERFORM PROCESS-A.
GO TO END-PROCESS.
STEP-B.
DISPLAY 'Processing Step B'.
PERFORM PROCESS-B.
GO TO END-PROCESS.
END-PROCESS.
DISPLAY 'Process completed'.
PERFORM CLEANUP.
STOP RUN.
このような場合でも、Javaでは、while文とswitch文の組み合わせでなんとか実装は可能である(ソース11)。
ソース11: GOTO文をwhile + switchで実装
public void execute() {
String label = "MAIN-PROCESS";
boolean inputError = false;
int calcResult = 0;
while (true) {
switch (label) {
case "MAIN-PROCESS":
inputCheck();
if (inputError) {
label = "END-PROCESS"; // GO TO END-PROCESS相当
break;
}
calcResult = calculation();
if (calcResult < 0) {
label = "STEP-B"; // GO TO STEP-B相当
break;
} else {
label = "STEP-A"; // GO TO STEP-A相当
break;
}
case "STEP-A":
System.out.println("Processing Step A");
processA();
label = "END-PROCESS"; // GO TO END-PROCESS相当
break;
case "STEP-B":
System.out.println("Processing Step B");
processB();
label = "END-PROCESS"; // GO TO END-PROCESS相当
break;
case "END-PROCESS":
System.out.println("Process completed");
cleanup();
return; // STOP RUN相当
default:
throw new IllegalStateException("Unknown label: " + label);
}
}
}
少しわかりにくいかもしれないが、以下のような実装となる。
- GOTO文のラベルを、ラベル変数として実装する。GOTO文は、ラベル変数にラベル値を代入後、breakすることで、継続する処理を行わないようにする
- ラベル部分の処理を行うため、switch文のcase句に、ラベル変数の値に応じた処理を書く
- 全体をwhileループで囲うことで、break後、再度switch文の評価を行い、当該ラベル値の処理を行う
実装はできるとはいえ、このようなGOTO文ベースのコードをJavaで再実装したプログラムに価値があるのかは、はなはだ疑問である。label変数とswitch文の条件分岐、whileループの組み合わせでなんとか処理を実装できているが、処理の流れが非常に追いにくく、可読性が著しく低下している。保守が非常に困難なプログラムとなることは間違いない。
最後に
ここまで述べてきたように、COBOLとJavaは言語仕様やそもそもの設計思想が異なるので、単純に変換するだけでは、ローカル変数などJavaの基本的な機能が使えないだけでなく、プログラムの保守性向上のため、あえて捨ててきた機能(GOTO文、固定長文字列、テキスト展開など)をむりやり復活させて利用するという形になる。このような形でシステムを実装すると、文法はJavaだが、影響範囲の特定や機能追加の設計はCOBOLのスキルが必要になる。
また、Javaとしてはアンチパターンの塊となるプログラムになり、このようなプログラムを保守していくことが、一般的なJavaの技術者のキャリアとしてプラスになるとはとても思えない。これが本当にやりたいことなのかどうかは考えるべきであろう。
自分としては、COBOLに特段批判的というわけではない。メモリがキロバイト単位のころに、事務計算を効率的に行うため、この言語仕様を作り出したことは、本当にすごいと思う。ただ、表面上だけJavaという言語仕様で取り繕って、半世紀以上も前につくられた言語制約を今後もひきずっていくのがいいとはとても思えない。
代替策の検討
最後に、代替策としていくつか簡単にあげさせていただく
1. 設計から見直して再構築する
これが王道であると思う。だれもがそう思っているが、現行システムの複雑さや規模、それに対処できる人材不足などのハードルが非常に高く、実行できないことが多いと思う。しなしながら、生成AIの活用により、そのハードルは確実に下がっている。
2. COBOLのままで再構築を行う
メインフレームを脱却するならオープン系のCOBOLに移行という手もある。変換した結果、文法がJavaなだけで、本質的にCOBOLということなら、COBOLのままの方がましであるともいえる。
ただ、20年ほど前ならこの方法はよく使われたが、現時点でこれをお勧めできるかはよくわかない。COBOL技術者は高齢化しているし、新しい技術スタックとの連携も課題である。
3. 高度な変換ソリューションを使う
単純変換ではなく、プログラムのロジックも見たうえで変換を行うソリューションが出てきつつあるのでそれを使う手もある。ロジックを分析しリファクタリングも行うことで、固定長文字列やCOPY句など、ここで述べた多くの問題が解決できる。
ただし限界もある。元のプログラムが、GOTO文を多用していたり、セクションが適切に分けられていないと、変換結果もそれほどよくならないと思われる。
まとめ
JABOLという言葉で揶揄されるCOBOL→Java変換の問題は、単に「オブジェクト指向でない」というレベルの話ではなく、昔の言語の制約や技術スキルを今後にも残すかどうかという問題である。固定長文字列やGOTO文などは昔話にしたいものです。本記事が、レガシーモダナイゼーションを検討されている方の参考になれば幸いです。