COBOLからJavaへのコンバーターは「Jobol」を生成します

COBOLからJavaへのコンバーターが「Jobol」を生成する理由と、それを回避する方法

変換作業は3か月で完了した。Javaアプリケーションはコンパイルされ、基本的なスモークテストに合格し、展開準備完了と判断された。運用開始から6か月後、プラットフォームチームはある問題に気づいた。Javaチームの誰もコードを保守できなくなっていたのだ。レビューには通常のJavaレビューの2倍の時間がかかっていた。バグ報告には、ワーキングストレージ、プロシージャ除算ロジック、パックド10進数フィールドの規則といった概念が参照されていたが、メインフレームの経験がないJava開発者には理解できなかった。アプリケーションはCOBOLからJavaに変換されたものの、結果としてできたJavaはCOBOLのように考え、構造化し、動作していた。それはまさに「Jobol」だった。

Jobol(JOBOLと表記されることもある)は、Javaのオブジェクト指向機能を採用せずにCOBOLの手続き型セマンティクスを保持するJavaコードを指す造語です。アナリスト企業Intellyxの創設者であるジェイソン・ブルームバーグ氏は、これを次のように直接定義しています。Jobolは、技術的には有効なJavaコードであり、元のソフトウェアアーキテクチャをそのまま残し、COBOLのセマンティクスを維持するため、開発者はJavaをCOBOLであるかのように扱う必要があります。重大な結果がすぐに生じます。COBOLをJobolに変換した企業は、COBOLの専門知識の必要性から解放されたわけではありません。単に、その専門知識の要件を、コードに含まれるCOBOLパターンをネイティブにサポートしていない言語に移しただけです。元のCOBOLよりも保守が難しく、期待されるJavaの利点を何も提供しないシステムを作り出してしまったのです。

ドメイン設計前の依存関係グラフ

SMART TS XL PERFORMとCALLのすべての依存関係をマッピングすることで、Javaサービス層はCOBOLクラス構造ではなく、実際の結合構造を反映するようになります。

さらに詳しく…

ジョボルの実際の姿

原因を診断する前に、まずパターンを把握する必要があります。ここでは、顧客の信用調査という同じビジネスロジックを、3つの形式で示します。オリジナルのCOBOL、自動変換ツールが通常生成するJobol、そして熟練したモダナイゼーションによって生成される慣用的なJavaです。

オリジナルのCOBOL:

COBOL

       PERFORM-CREDIT-CHECK.
           MOVE WS-CUST-ID TO DB-CUST-KEY
           READ CUSTOMER-FILE KEY IS DB-CUST-KEY
               INVALID KEY
                   MOVE 'NOT FOUND' TO WS-STATUS
                   PERFORM HANDLE-MISSING-CUSTOMER
           END-READ

           IF CUST-CREDIT-SCORE < 650
               MOVE 'DECLINED' TO WS-CREDIT-RESULT
               MOVE 'Score below threshold' TO WS-DECLINE-REASON
           ELSE IF CUST-OUTSTANDING-BALANCE > CUST-CREDIT-LIMIT
               MOVE 'DECLINED' TO WS-CREDIT-RESULT
               MOVE 'Balance exceeds limit' TO WS-DECLINE-REASON
           ELSE
               MOVE 'APPROVED' TO WS-CREDIT-RESULT
           END-IF.

行単位コンバータによって生成されたJobol:

ジャワ

// Jobol: procedural COBOL logic mapped directly to Java
// No OOP, no encapsulation, static global state, COBOL naming conventions
public class CreditCheckProgram {
    static String WS_CUST_ID;
    static String DB_CUST_KEY;
    static String WS_STATUS;
    static String WS_CREDIT_RESULT;
    static String WS_DECLINE_REASON;
    static int CUST_CREDIT_SCORE;
    static double CUST_OUTSTANDING_BALANCE;
    static double CUST_CREDIT_LIMIT;

    public static void PERFORM_CREDIT_CHECK() {
        DB_CUST_KEY = WS_CUST_ID;
        Customer customer = CustomerFile.read(DB_CUST_KEY);
        if (customer == null) {
            WS_STATUS = "NOT FOUND";
            HANDLE_MISSING_CUSTOMER();
            return;
        }
        CUST_CREDIT_SCORE = customer.getCreditScore();
        CUST_OUTSTANDING_BALANCE = customer.getOutstandingBalance();
        CUST_CREDIT_LIMIT = customer.getCreditLimit();

        if (CUST_CREDIT_SCORE < 650) {
            WS_CREDIT_RESULT = "DECLINED";
            WS_DECLINE_REASON = "Score below threshold";
        } else if (CUST_OUTSTANDING_BALANCE > CUST_CREDIT_LIMIT) {
            WS_CREDIT_RESULT = "DECLINED";
            WS_DECLINE_REASON = "Balance exceeds limit";
        } else {
            WS_CREDIT_RESULT = "APPROVED";
        }
    }
}

熟練した近代化によって生み出される慣用的なJava:

ジャワ

// Idiomatic Java: domain model, encapsulation, value objects, Spring service
@Service
public class CreditCheckService {

    private final CustomerRepository customerRepository;

    public CreditCheckService(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    public CreditDecision check(CustomerId customerId) {
        Customer customer = customerRepository.findById(customerId)
            .orElseThrow(() -> new CustomerNotFoundException(customerId));

        if (customer.creditScore() < CreditThresholds.MINIMUM_SCORE) {
            return CreditDecision.declined("Score below threshold");
        }
        if (customer.outstandingBalance().compareTo(customer.creditLimit()) > 0) {
            return CreditDecision.declined("Balance exceeds limit");
        }
        return CreditDecision.approved();
    }
}

Jobol版は、静的なグローバル状態を記述した32行のコードで、手続き型構造を持ち、COBOL風の命名規則が用いられており、グローバルクラスの状態を初期化しないと単体テストが不可能です。一方、慣用的なバージョンは20行で、注入可能、テスト可能であり、COBOLの知識がないJava開発者でもすぐに理解できます。どちらも同じビジネスルールを実装していますが、近代化によって実現されるはずだったチームが保守できるのは、片方だけです。

ジョボルの6つのパターンと、コンバーターがそれらを生成する理由

パターン1:ワーキングストレージをグローバル静的状態に置き換える

COBOLのワーキングストレージセクション(WS)は、PERFORM呼び出しをまたいで保持されるプログラムレベルの変数を保持します。これはプログラムの共有可変状態であり、すべてのパラグラフが同じWS変数を読み書きします。自動コンバータはこれを単一クラスのJava静的フィールドに直接マッピングするため、共有状態のセマンティクスは維持されますが、Javaのカプセル化原則はすべて破られます。

コンバータがこのような動作をする理由:ワーキングストレージから静的フィールドへのマッピングは、機械的な1対1変換です。コンバータは、Javaのカプセル化モデルがプログラムレベルの共有状態をインスタンス変数、メソッドパラメータ、戻り値に置き換えることを理解していません。コンバータは、元の構造が存在する理由を理解せずに、目にしたものをそのまま変換します。

その結果、静的状態は単独ではテスト不可能であり、明示的な同期なしではスレッドセーフではなく、構造的には置き換えたCOBOLと同一である。システムに並行処理を追加する必要がある開発者は、まず静的状態を解きほぐす必要があるが、この作業は変換後から後続の保守担当者に引き継がれてきた。

解決策: WORKING-STORAGE変数を、その使用範囲に基づいてインスタンスフィールド、メソッドパラメータ、または戻り値にマッピングします。1つの段落内でのみ使用される変数はローカル変数になります。段落間で渡される変数はメソッドパラメータまたは戻り値になります。共有オブジェクトの状態を真に表す変数は、適切にカプセル化されたドメインクラスのインスタンスフィールドになります。

パターン2:PERFORMチェーンが長い静的メソッドになる

COBOLプログラムはPERFORMチェーンとして構成され、メインプロシージャは一連の名前付きパラグラフをPERFORMし、各パラグラフは他のパラグラフをPERFORMすることができます。自動コンバータは、各パラグラフを静的メソッドに、各PERFORMを静的メソッド呼び出しに変換します。その結果、COBOLのPERFORM構造を反映した呼び出しグラフを介して互いに呼び出し合う、数十個の静的メソッドを持つ単一のクラスが生成されます。

ジャワ

// Jobol: PERFORM chain mapped to static method calls
public class ProcessOrderProgram {
    public static void main() {
        validateOrder();
        checkInventory();
        calculateTotal();
        applyDiscounts();
        processPayment();
        generateConfirmation();
        updateInventory();
    }

    public static void validateOrder() { /* ... */ }
    public static void checkInventory() { /* ... */ }
    // ... 40 more static methods
}

コンバーターがこのような動作をする理由: COBOLの段落からJavaメソッドへの構造的なマッピングは直接的です。コンバーターはPERFORMが「このコードユニットを呼び出す」という意味であることを正しく認識します。しかし、Javaのオブジェクトモデルは処理ステップではなく、ドメインエンティティを中心にコードを構成しているという点を見落としています。

修正: PERFORMチェーンに組み込まれているドメイン概念を特定する。 validateOrder, calculateTotal, applyDiscounts は、注文に対する操作です。 processPayment これは支払いコンテキストにおける操作です。 checkInventory (NAIST) と updateInventory これらは在庫管理に関する操作です。リファクタリングでは、静的メソッドをそれぞれのドメインクラスに分散させ、長い手続き型呼び出しチェーンを、適切なドメインオブジェクトに処理を委譲する短いオーケストレーションメソッドに置き換えます。

パターン3:精度を扱わない数値型

COBOLのパック10進数(COMP-3)フィールドは、正確な10進数演算を行います。COMP-3をJavaにマッピングするコンバーター double or float 数百万件の金融取引にわたって蓄積される、体系的な浮動小数点丸め誤差を導入する。

ジャワ

// Jobol: COMP-3 mapped to double (wrong)
static double WS_TRANSACTION_AMOUNT;  // Was PIC S9(9)V99 COMP-3
static double WS_RUNNING_TOTAL;

// After 10M additions of 0.1:
// Expected: 1,000,000.00
// Actual:   1,000,000.0000000002 (floating point accumulation)

ジャワ

// Idiomatic Java: COMP-3 mapped to BigDecimal (correct)
private BigDecimal transactionAmount;  // Preserves decimal precision
private BigDecimal runningTotal;

// After 10M additions of 0.1:
// Result: exactly 1,000,000.00

コンバーターがこのような動作をする理由: double は Java のデフォルトの数値型です。COMP-3 のセマンティクスを具体的に理解していないコンバータは、最も一般的な Java の数値型をデフォルトとして使用しますが、これは正確な小数演算には不適切な選択です。

修正: すべてのCOMP-3フィールドは、 BigDecimalCOMP-3 値を含むすべての数値計算では、 BigDecimal 算術。 scale() BigDecimal PIC句で暗黙的に示される小数点以下の桁数と一致する。 PIC S9(9)V99 小数点以下2桁が暗黙的に含まれており、 BigDecimal スケール2。

パターン4:EBCDIC文字列とバイト配列の遺物

COBOLは、文字データをEBCDICエンコーディングの固定長パディング文字列として処理します。コンバーターは、UTF-8への変換後も固定長でスペースパディングされた文字列のセマンティクスを保持するJavaコードを生成することが多く、その結果、1970年代のメインフレームプログラマーのように文字列を扱うJavaコードが生成されます。

ジャワ

// Jobol: preserves COBOL fixed-length string handling
static char[] WS_CUSTOMER_NAME = new char[40]; // Was PIC X(40)
static char[] WS_CUSTOMER_ADDR = new char[80]; // Was PIC X(80)

// COBOL-style space-padding comparison
if (new String(WS_CUSTOMER_NAME).trim().equals("SMITH")) { ... }

// COBOL-style string concatenation with padding
System.arraycopy(name.toCharArray(), 0, WS_CUSTOMER_NAME, 0,
                 Math.min(name.length(), 40));

ジャワ

// Idiomatic Java: proper String types and domain objects
private String customerName;  // Trim on read, no padding
private String customerAddress;

// Clean comparison
if ("Smith".equalsIgnoreCase(customer.name())) { ... }

コンバータがこのような動作をする理由: COBOL PIC X(40) 定義には長さがあり、コンバータはそれを Java 配列のサイズとして保持します。EBCDIC から UTF-8 への変換は処理できますが、固定長セマンティクスが不必要に引き継がれてしまいます。

修正: 地図 PIC X(n) にフィールド String オブジェクト、読み取り時のトリミング、固定長制約の削除などを行います。長さ制約が重要となるのは、ドメインモデルではなく、永続化境界(データベースの列長、ファイル形式の位置)のみです。

パターン5:ステータスコードによるエラー処理

COBOLでは、エラー状態はステータスコード(FILE STATUS、SQLCODE、RETURN-CODE)によって伝達されます。コンバーターはJavaでもこのパターンを維持し、Javaの例外モデルを使用するのではなく、各操作後に整数型のステータスコードをチェックするコードを生成します。

ジャワ

// Jobol: COBOL status code pattern in Java
static int WS_FILE_STATUS;
static int WS_SQL_CODE;

CustomerFile.read(DB_CUST_KEY);
WS_FILE_STATUS = CustomerFile.getLastStatus();
if (WS_FILE_STATUS == 10) {
    // Not found
} else if (WS_FILE_STATUS == 35) {
    // File not available
} else if (WS_FILE_STATUS != 0) {
    // Unknown error
}

ジャワ

// Idiomatic Java: exceptions for error conditions
try {
    Customer customer = customerRepository.findById(customerId)
        .orElseThrow(() -> new CustomerNotFoundException(customerId));
    // process customer
} catch (DatabaseUnavailableException e) {
    log.error("Database unavailable during credit check", e);
    throw new CreditCheckServiceException("Service temporarily unavailable", e);
}

コンバーターがこのような処理を行う理由:ステータスコードチェックはCOBOLのパターンであり、それをステータスフィールドを返す、または設定するJavaメソッドに変換するのは機械的な変換です。Java例外が慣用的な同等物であると認識するには、機械的な翻訳ではなく、アーキテクチャ上の判断が必要です。

修正: COBOLのエラー条件をJavaの例外にマッピングします。ファイルステータス10(レコードが見つかりません)は次のようになります。 Optional.empty() またはドメイン固有の例外。SQLCODE エラーは型付きデータベース例外になります。Java コード構造は意図を反映しており、想定される障害モードには try/catch を使用し、予期しない障害モードには非チェック例外を使用します。

パターン6:モノリシックなクラス構造

COBOLプログラムは設計上、モノリシックな構造になっており、単一のプログラムが業務機能全体を処理します。コンバータはCOBOLプログラムごとに単一のJavaクラスを作成するため、生成されるJavaクラスは数千行にも及ぶことがあり、オブジェクト指向設計であれば複数の専門クラスに分散されるような複数の責務を処理することになります。

コンバータがこのような動作をする理由: 1つのCOBOLプログラムは1つのJavaクラスにマッピングされ、コンバータの変換単位はプログラムです。プログラムの内部構造(段落、セクション、PERFORMチェーンなど)は、より小さなクラスに分解されることなく、クラスの内部構造になります。

解決策:変換後の各クラスに単一責任の原則を適用する。入力の検証、データベースからの読み込み、ビジネスルールの適用、出力のフォーマット、ファイルへの書き込みを行う3,000行のJobolクラスは、入力バリデーター、リポジトリー、ドメインサービス、フォーマッター、ライターに分割し、それぞれを個別にテスト可能にし、それぞれが単一の責任に特化させるべきである。

構造解析の前提条件

すべてのJobolパターンには対応するリファクタリングが存在します。しかし、リファクタリングはコンバータによって適用できるものではなく、プログラムの動作を理解した上で再構築方法を決定する必要があります。まさにその理解こそが、変換ツールを実行する前にソースCOBOLの構造解析によって得られるものです。

具体的には、構造分析は、変換決定に必要な入力データを生成する。

ビジネスロジックのインベントリ。各プログラムにおけるEVALUATE条件、IF分岐、COMPUTE式の完全な列挙。プログラムがどのような判断を下すか、つまり構造だけでなく、その判断内容もすべて網羅します。このインベントリは、リファクタリング後のJavaが準拠しなければならない仕様です。847個のEVALUATE条件を持つプログラムには、847個の判断ポイントに対応できるターゲットクラス構造が必要です。コンバータの出力はあくまでも初期段階のドラフトであり、完成品ではありません。

依存関係グラフ。どのプログラムが他のどのプログラムを呼び出すかを示します。複数のCOBOLプログラムにまたがるPERFORMチェーンは、Javaではサービス呼び出しグラフになります。依存関係グラフによって、どのプログラムを個別にリファクタリングできるか(受信依存関係が少ない)、どのプログラムを協調グループとしてリファクタリングする必要があるか(コピーブックや共有データによる共有依存関係が多い)が分かります。

デッドコードの識別。本番環境で実行パスから一度も実行されない段落やセクションはデッドコードであり、変換後のJavaコードには一切含まれるべきではありません。デッドコードを含むコンバーターは、変換後のコードベースを肥大化させ、その後のリファクタリング作業を増大させます。

データモデルのリカバリ。プログラムのデータを定義するVSAM FDエントリ、DB2テーブル定義、およびコピーブック構造。各COBOLデータ構造は、1つ以上のJavaドメインオブジェクトにマッピングされます。このマッピングは、WORKING-STORAGEのフィールド名ではなく、データモデルによって決定されます。

分析入力コンバージョン決定を促す
評価/IF 分岐数ドメインクラスの分解、各サービスが処理する決定ケースの数
受信依存関係数リファクタリングの順序:依存関係の少ないプログラムから順に
デッドコードの割合スコープの縮小、変換対象から到達不能なコードを除外する
COMP-3フィールドBigDecimal すべての数値フィールドのマッピング
PIC Xフィールド長文字列型のマッピングと永続化層の制約
グラフ構造を実行するサービス層の分解により、PERFORM チェーンはドメインサービス呼び出しとなる。
ファイルステータスの使用例外モデルの設計、どのステータスコードがどの例外タイプに対応するか

変換によって代わりに生成されるべきもの

Jobolの代替案としては、COBOLをテンプレートとしてではなく仕様として使用する変換プロセスがあります。COBOLプログラムはビジネスロジックを把握しており、JavaプログラムはそのロジックをJavaのイディオムで表現する必要があります。

変換原理: 構文ではなく、意図を変換してください。COBOL IF CUST-CREDIT-SCORE < 650 MOVE 'DECLINED' TO WS-CREDIT-RESULT 「信用スコアが650未満の顧客は拒否されます」と書かれています。JavaでもJavaの言語で同じことを言うべきです。 if (customer.creditScore() < CreditThresholds.MINIMUM_SCORE) return CreditDecision.declined(...)。 ない if (CUST_CREDIT_SCORE < 650) { WS_CREDIT_RESULT = "DECLINED"; }.

慣用的なJavaコードを生成する実践的な変換ワークフロー:

ステップ1: COBOLソースコードの構造解析、依存関係グラフ、ビジネスロジックインベントリ、データモデルの復元、デッドコードの特定。このステップで仕様書が作成されます。このステップが完了するまで、何も変換されません。

ステップ2:ドメインモデルの設計では、COBOLプログラムが操作するビジネスエンティティ(顧客、アカウント、トランザクション、ポリシー)を特定します。これらはJavaドメインクラスになります。エンティティの状態を表すWORKING-STORAGE変数は、適切なドメインクラスのインスタンスフィールドになります。

ステップ3:サービス層の設計では、COBOLの段落とセクションが実装する業務操作(信用調査、支払処理、残高計算など)を特定します。これらは、適切なドメインサービスクラスのJavaサービスメソッドになります。段落を順序付けるPERFORMチェーンは、サービスのオーケストレーションロジックとなります。

ステップ4:ドラフト生成ツールとしての自動変換。ステップ2と3で設計したドメインモデルに基づいて、変換ツールを使用して各プログラムのJavaドラフトの出発点を作成します。ビジネスロジックインベントリと照らし合わせてドラフトを確認し、すべての決定分岐が表現されていることを確認します。

ステップ5: Jobolパターンに対する体系的なリファクタリング、6つのアンチパターン修正の適用:静的状態をインスタンス状態に置き換える、静的メソッドをドメインクラスに分散する、ステータスコードを例外に置き換える、 double   BigDecimal固定長配列を置き換える Stringモノリシックなクラスを単一責任のサービスに分解する。

ステップ6:動作等価性テスト。リファクタリングされたJavaコードを、COBOL分析から生成された特性テストコーパスに対して実行します。すべてのEVALUATE条件とIF分岐には、それを実行してCOBOL出力との等価性を検証するテストケースが少なくとも1つ必要です。

認定条件 SMART TS XL ジョボルの発生源を阻止する

Jobol防止のための構造分析の前提条件である依存関係グラフ、ビジネスロジックインベントリ、デッドコード識別、データモデルリカバリはまさに SMART TS XLCOBOLソースの分析は、変換ツールが実行される前に提供されます。

SMART TS XLさん 静的コード分析 COBOLソース内のすべてのEVALUATE条件、IF分岐、COMPUTE式、およびPERFORM関係を列挙します。これは、リファクタリングされたJavaが忠実に表現しなければならない完全なビジネスロジックインベントリです。変換ツールがJobolを生成する際、このビジネスロジックインベントリは、リファクタリングの検証基準となります。インベントリ内のすべての条件は、Javaのイディオムで表現された適切なドメインクラス内にJavaコード内に存在しなければなりません。

アプリケーションの依存関係マッピングによって、Javaサービスレイヤーの構造を決定するPERFORMグラフとプログラム呼び出しグラフが生成されます。頻繁に相互に呼び出し合うプログラムは、同じ境界コンテキストに属します。多くのプログラムから呼び出されるプログラムは共有サービスであり、Javaアーキテクチャにおいてすべてのコンシューマーをサポートするように慎重に設計する必要があります。依存関係グラフはサービスレイヤー設計ステップへの入力となり、COBOLの暗黙的な結合構造をJavaの明示的な依存性注入構造に変換します。

レガシーシステムの近代化分析では、本番環境の実行パスから決して到達しないデッドコード、段落、セクションを特定します。これらは変換範囲から完全に除外する必要があります。デッドコードであるCOBOLをデッドコードであるJobolに変換し、さらにJobolをリファクタリングすることは、変換開始前にデッドコードを特定することで排除できる労力を無駄にすることになります。

影響分析機能はリファクタリングの順序を検証します。COBOLプログラムのインターフェースがJobolから慣用的なJavaへのリファクタリング中に変更された場合、影響分析は、変更されたインターフェースとの整合性を維持するためにJava版も更新する必要がある他のすべてのプログラムを特定します。依存関係の少ない順、つまり依存関係の少ない順にリファクタリングを行うことで、各ステップで必要な調整を最小限に抑えることができます。

コンバーターは構文を変換する。現代化は意味を変容させる。

COBOLからJavaへの自動変換ツールは、人間による書き換えよりも高速です。また、これらのツールは、ターゲット言語の慣用表現における意味的な等価性よりも構文的な対応性を重視して最適化されています。Jobolは、ソース言語(COBOL)とターゲット言語(Java)の構造モデルが根本的に異なる場合に、構文的な対応性によって生成されるものです。

Jobol を避ける組織は、より優れたコンバータを使用している組織ではありません。彼らは、コンバータを完成品ではなくドラフト生成ツールとして扱い、変換開始前に構造分析に投資し、コンバータ実行前に Java ドメインモデルを設計し、ビジネスロジックインベントリを検証仕様として、コンバータの出力に対して体系的なリファクタリングを適用しているのです。

COBOLは回避可能です。回避した場合のコストは、あらゆる変換作業に先立って必要となる構造分析とドメインモデリング作業です。回避しない場合のコストは、保守にCOBOLの専門知識を必要とするJavaシステムとなり、Java開発者によるシステム保守という、近代化によって期待されるメリットが実現されないことを意味します。

構文ではなく、意図を変換する。その違いこそが、近代化の真の価値なのだ。