Перевод проекта занял три месяца. Java-приложение скомпилировалось, прошло базовые дымовые тесты и было признано готовым к развертыванию. Через шесть месяцев после начала эксплуатации команда разработчиков платформы заметила тревожную вещь: никто из Java-команды не мог поддерживать код. Проверка кода занимала вдвое больше времени, чем обычная проверка Java-приложений. В отчетах об ошибках упоминались концепции, WORKING-STORAGE, логика PROCEDURE DIVISION, соглашения об упакованных десятичных полях, которые 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.
УЗНАТЬ БОЛЬШЕ…Как на самом деле выглядит Йобуль
Прежде чем диагностировать причины, необходимо выявить закономерность. Вот та же самая бизнес-логика, проверка кредитоспособности клиента, в трех формах: оригинальный COBOL, Jobol, который обычно создают автоматизированные конвертеры, и идиоматический Java, который создается в результате квалифицированной модернизации.
Оригинальный 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 строк, допускает внедрение зависимостей, тестируется и сразу читается любым Java-разработчиком без опыта работы с COBOL. Обе реализуют одни и те же бизнес-правила. Только одна из них может поддерживаться командой, для которой и была предназначена модернизация.
Шесть типовых печатных форм Jobol и почему производители конвертеров их создают.
Шаблон 1: Глобальное статическое состояние, заменяющее WORKING-STORAGE
В COBOL раздел WORKING-STORAGE содержит переменные программного уровня, которые сохраняются между вызовами PERFORM. Это общее изменяемое состояние программы; каждый абзац читает и записывает одни и те же переменные WS. Автоматизированные преобразователи напрямую сопоставляют это со статическими полями Java в одном классе, сохраняя семантику общего состояния, но нарушая все принципы инкапсуляции Java.
Почему конвертеры это делают: сопоставление WORKING-STORAGE со статическими полями — это механическое преобразование «один к одному». Конвертер не понимает, что модель инкапсуляции Java заменяет общее состояние на уровне программы переменными экземпляра, параметрами методов и возвращаемыми значениями. Он преобразует то, что видит, не понимая, зачем существовала исходная структура.
В результате: статическое состояние невозможно протестировать изолированно, оно небезопасно для многопоточного выполнения без явной синхронизации и структурно идентично COBOL, который оно заменило. Любой разработчик, которому необходимо добавить параллелизм в систему, должен сначала разобраться со статическим состоянием — работа, которая была отложена с момента преобразования на каждого последующего сопровождающего проекта.
Решение: Сопоставьте переменные WORKING-STORAGE с полями экземпляра, параметрами методов или возвращаемыми значениями в зависимости от области их применения. Переменные, используемые только в пределах одного абзаца, становятся локальными переменными. Переменные, передаваемые между абзацами, становятся параметрами методов или возвращаемыми значениями. Переменные, которые действительно представляют собой состояние общего объекта, становятся полями экземпляра в правильно инкапсулированном доменном классе.
Шаблон 2: Выполнение цепочек, превращающихся в длинные статические методы.
Программы на COBOL организованы в виде цепочек 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 и updateInventory Это операции над объектом Inventory. Рефакторинг распределяет статические методы между классами предметной области, к которым они относятся, заменяя длинную цепочку процедурных вызовов коротким методом оркестровки, который делегирует вызовы соответствующим объектам предметной области.
Шаблон 3: Числовые типы без обработки точности
В COBOL поля упакованного десятичного типа (COMP-3) используют точную десятичную арифметику. Существуют конвертеры, которые сопоставляют 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 соответствует BigDecimalВ каждом числовом вычислении, включающем значения COMP-3, используется BigDecimal арифметика. scale() BigDecimal соответствует подразумеваемым десятичным знакам в пункте PIC: PIC S9(9)V99 имеет 2 подразумеваемых десятичных знака, что соответствует BigDecimal с масштабом 2.
Шаблон 4: Строки EBCDIC и остатки байтовых массивов
COBOL обрабатывает символьные данные как строки фиксированной длины с добавлением пробелов в кодировке EBCDIC. Конвертеры часто создают код Java, который сохраняет семантику строк фиксированной длины с добавлением пробелов даже после преобразования в UTF-8, в результате чего Java обрабатывает строки так, как это делал программист на мэйнфреймах 1970-х годов.
Ява
// 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. FILE STATUS 10 (запись не найдена) становится Optional.empty() или исключение, специфичное для предметной области. Ошибки SQLCODE преобразуются в типизированные исключения базы данных. Структура кода Java отражает намерение: try/catch для ожидаемых режимов сбоев, непроверяемые исключения для неожиданных.
Шаблон 6: Монолитная структура классов
Программы на COBOL по своей природе монолитны: одна программа обрабатывает всю бизнес-функцию. Конвертеры создают один Java-класс для каждой программы COBOL, в результате чего получаются Java-классы, которые могут содержать тысячи строк кода и выполнять множество задач, которые в объектно-ориентированном проектировании были бы распределены между несколькими специализированными классами.
Почему конвертеры это делают: одна программа на COBOL сопоставляется с одним классом на Java, единицей трансляции конвертера является программа. Внутренняя структура программы (ее абзацы, разделы, цепочки PERFORM) становится внутренней структурой класса без декомпозиции на более мелкие классы.
Решение: Применить принцип единственной ответственности к каждому преобразованному классу. Класс Jobol объемом 3,000 строк, который проверяет входные данные, считывает данные из базы данных, применяет бизнес-правила, форматирует выходные данные и записывает их в файл, должен стать: валидатором входных данных, репозиторием, службой предметной области, форматировщиком и записывающим устройством, каждое из которых можно тестировать изолированно, и каждое должно быть сосредоточено на одной задаче.
Необходимое условие для проведения структурного анализа.
Для каждого шаблона Jobol существует соответствующая рефакторизация. Но рефакторизация не может быть применена конвертером; для этого необходимо понимать, что делает программа, прежде чем принимать решение о её реструктуризации. Именно такое понимание обеспечивает структурный анализ исходного кода COBOL перед запуском любого инструмента преобразования.
В частности, структурный анализ предоставляет исходные данные, необходимые для принятия решений о преобразовании:
Инвентаризация бизнес-логики. Полный перечень условий EVALUATE, ветвей IF и формул COMPUTE в каждой программе, то, что программа решает, а не только то, как она структурирована. Эта инвентаризация является спецификацией, которой должна соответствовать рефакторизованная Java-программа. Программа с 847 условиями EVALUATE требует целевой структуры класса, которая вмещает 847 точек принятия решений; результатом работы конвертера является черновик, а не готовый продукт.
Граф зависимостей. Какие программы вызывают какие другие программы. Цепочка вызовов PERFORM, охватывающая несколько программ на COBOL, в Java превращается в граф вызовов сервисов. Граф зависимостей определяет, какие программы можно рефакторизовать независимо (небольшое количество входящих зависимостей), а какие необходимо рефакторизовать как скоординированные группы (много общих зависимостей через копибуки или общие данные).
Идентификация мертвого кода. Абзацы и разделы, которые никогда не выполняются ни в одном из путей выполнения в производственной среде, являются мертвым кодом и не должны присутствовать в преобразованном Java-коде. Конвертеры, включающие мертвый код, увеличивают объем преобразованной кодовой базы и, следовательно, объем последующих работ по рефакторингу.
Восстановление модели данных. Записи FD VSAM, определения таблиц DB2 и структуры копибуков, определяющие данные программы. Каждая структура данных COBOL сопоставляется с одним или несколькими объектами домена Java; сопоставление определяется моделью данных, а не именами полей в WORKING-STORAGE.
| Входные данные анализа | Решение о конверсии, которое оно определяет |
|---|---|
| ОЦЕНИТЬ/ЕСЛИ количество филиалов | Декомпозиция предметных классов, сколько вариантов принятия решений обрабатывает каждая служба. |
| Количество входящих зависимостей | Последовательность рефакторинга: программы с наименьшим количеством зависимостей — в первую очередь. |
| Процент мертвого кода | Сокращение объема работ, исключение недоступного кода из процесса преобразования. |
| Поля COMP-3 | BigDecimal сопоставление для каждого числового поля |
| Длины полей PIC X | Сопоставление типов строк и ограничения уровня персистентности |
| Выполнить структуру графа | Декомпозиция уровня сервисов, цепочки PERFORM преобразуются в вызовы доменных сервисов. |
| Использование статуса файла | Разработка модели исключений: какие коды состояния соответствуют каким типам исключений. |
Что должно получиться в результате преобразования вместо этого?
Альтернативой Jobol является процесс преобразования, в котором COBOL используется в качестве спецификации, а не шаблона. Программа на COBOL знает, какова бизнес-логика, а программа на Java должна выразить эту логику на языке Java.
Принцип преобразования: Преобразуйте намерение, а не синтаксис. COBOL
IF CUST-CREDIT-SCORE < 650 MOVE 'DECLINED' TO WS-CREDIT-RESULTВ тексте на Java должно быть написано: «Клиенту с кредитным рейтингом ниже 650 отказано».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: Автоматическое преобразование в качестве генератора черновиков. Используйте инструмент преобразования для создания исходного Java-черновика для каждой программы, основываясь на доменной модели, разработанной на шагах 2 и 3. Проверьте черновик на соответствие перечню бизнес-логики, чтобы убедиться, что представлены все ветви принятия решений.
Шаг 5: Систематическая рефакторизация с использованием паттерна Jobol, применение шести антипаттернов: замена статического состояния на состояние экземпляра, распределение статических методов по классам предметной области, замена кодов состояния исключениями, замена double с BigDecimalзаменить массивы фиксированной длины на StringРазложить монолитные классы на сервисы с одной единственной обязанностью.
Шаг 6: Тестирование эквивалентности поведения. Запустите рефакторизованный код Java на основе корпуса тестовых примеров, сгенерированного в результате анализа COBOL. Каждое условие EVALUATE и ветвь IF должны иметь как минимум один тестовый случай, проверяющий их эквивалентность с результатами анализа COBOL.
Как SMART TS XL Предотвращает использование Jobol в источнике.
Необходимым условием для предотвращения ошибок Jobol является структурный анализ, включающий граф зависимостей, инвентаризацию бизнес-логики, выявление мертвого кода и восстановление модели данных. SMART TS XLАнализ исходного кода COBOL, проводимый этим инструментом, позволяет получить необходимые данные до запуска любого инструмента преобразования.
SMART TS XLАвтора статический анализ кода Перечисляет каждое условие EVALUATE, каждую ветвь IF, каждую формулу COMPUTE и каждое отношение PERFORM в исходном коде COBOL, полный перечень бизнес-логики, который должен точно представлять рефакторизованный код Java. Когда инструмент преобразования создает код Java, перечень бизнес-логики является спецификацией, на основе которой проверяется рефакторинг: каждое условие в перечне должно присутствовать в коде Java, в соответствующем классе предметной области, выраженном в идиоме Java.
Отображение зависимостей приложения формирует граф PERFORM и граф вызовов программ, которые определяют структуру сервисного уровня Java. Программы, часто вызывающие друг друга, находятся в одном ограниченном контексте. Программы, вызываемые многими другими программами, представляют собой общие сервисы, которые должны быть тщательно спроектированы для поддержки всех своих потребителей в архитектуре Java. Граф зависимостей является входными данными для этапа проектирования сервисного уровня, преобразуя неявную структуру связи COBOL в явную структуру внедрения зависимостей Java.
Анализ модернизации устаревших систем выявляет мертвый код, абзацы и разделы, которые никогда не используются в производственных сценариях выполнения и которые следует полностью исключить из области преобразования. Преобразование мертвого COBOL в мертвый Jobol с последующей рефакторизацией мертвого Jobol приводит к пустой трате усилий, которые этап выявления мертвого кода исключает до начала преобразования.
Функция анализа влияния подтверждает правильность последовательности рефакторинга: когда интерфейс программы COBOL изменяется в процессе рефакторинга с Java на идиоматический Java, анализ влияния выявляет все остальные программы, эквивалентные им на Java, которые также необходимо обновить, чтобы они оставались согласованными с измененным интерфейсом. Рефакторинг в порядке зависимостей, начиная с наименьшего числа зависимых программ, минимизирует необходимость координации на каждом этапе.
Конвертеры переводят синтаксис. Модернизация преобразует смысл.
Автоматизированные конвертеры COBOL в Java работают быстрее, чем переписывание кода человеком. Они также оптимизированы для другой цели: синтаксического соответствия, а не семантической эквивалентности в идиоме целевого языка. Java — это результат синтаксического соответствия, когда исходный язык (COBOL) и целевой язык (Java) имеют принципиально разные структурные модели.
Организации, избегающие Jobol, — это не те, кто использует более совершенные конвертеры. Это те, кто рассматривает конвертер как генератор черновиков, а не как готовый продукт, инвестирует в структурный анализ до начала конвертации, проектирует доменную модель Java до запуска конвертера и применяет систематическую рефакторизацию выходных данных конвертера, используя инвентаризацию бизнес-логики в качестве спецификации проверки.
Использование Java-языка можно избежать. Цена отказа от него — это структурный анализ и моделирование предметной области, которые должны предшествовать каждой модернизации. Цена неизбежности — это Java-система, для поддержки которой требуются знания COBOL, а это значит, что ожидаемая выгода от модернизации, возможность поддерживать систему с помощью Java-разработчиков, никогда не будет реализована.
Преобразуйте намерение, а не синтаксис. Разница заключается в самой ценности модернизации.


