САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ
На правах рукописи
Терехов Андрей Андреевич
ЯЗЫКОВЫЕ ПРЕОБРАЗОВАНИЯ В...
15 downloads
171 Views
2MB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ
На правах рукописи
Терехов Андрей Андреевич
ЯЗЫКОВЫЕ ПРЕОБРАЗОВАНИЯ В ЗАДАЧАХ РЕИНЖИНИРИНГА ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ
05.13.11 – Математическое и программное обеспечение вычислительных машин, комплексов и компьютерных сетей
Диссертация на соискание ученой степени кандидата физико-математических наук
Научный руководитель: кандидат физико-математических наук, доцент Фоминых Н.Ф.
Санкт-Петербург 2002
СОДЕРЖАНИЕ ВВЕДЕНИЕ ................................................................................................................ 6 Актуальность темы............................................................................................................... 6 История проекта RescueWare ............................................................................................. 7 Научный контекст работ по созданию RescueWare...................................................... 10 Основные результаты диссертационной работы .......................................................... 11 Апробация работы............................................................................................................... 12 Благодарности...................................................................................................................... 12 ГЛАВА 1. ОБЗОР ЗАДАЧ РЕИНЖИНИРИНГА .................................................... 13 1.1. Реинжиниринг и его экономические предпосылки ............................................... 13 1.2. Основные задачи реинжиниринга ............................................................................ 18 1.2.1. Возвратное проектирование ................................................................................... 19 1.2.2. Извлечение знаний................................................................................................... 21 1.2.3. Реструктуризация программ ................................................................................... 22 1.2.4. Языковые преобразования ...................................................................................... 23 1.3. Смежные вопросы реинжиниринга .......................................................................... 27 1.3.1. Сопровождение программ....................................................................................... 27 1.3.2. Повторное использование программ ..................................................................... 30 ГЛАВА 2. ТРУДНОСТИ, ВОЗНИКАЮЩИЕ ПРИ ЯЗЫКОВЫХ ПРЕОБРАЗОВАНИЯХ ............................................................................................ 33 2.1. О сложности языковых преобразований ................................................................. 34 2.2. Требования к средствам преобразования языков ................................................. 35 2.3. Технические проблемы ............................................................................................... 38 2.3.1. Преобразование типов данных ............................................................................... 39 2.3.2. Кобол в Visual Basic................................................................................................. 39 2.3.3. Кобол в Java.............................................................................................................. 41
2
2.3.4. OS/VS Cobol to VS Cobol II..................................................................................... 43 2.3.5. Turbo Pascal to Java .................................................................................................. 46 2.3.6. Перевод языково-специфичных конструкций ...................................................... 48 2.3.7. Проблемы поддержки сгенерированного текста .................................................. 49 2.4. Обсуждение .................................................................................................................... 51 2.5. Процесс преобразования языков............................................................................... 53 2.6. Заключение .................................................................................................................... 58 ГЛАВА 3. ОПИСАНИЕ КОНКРЕТНОГО ПРОЕКТА ПО ПРЕОБРАЗОВАНИЮ ЯЗЫКОВ .................................................................................................................. 59 3.1. Краткое описание проекта.......................................................................................... 60 3.2. Особенности языка Rules ............................................................................................ 61 3.3. Автоматизация решения задачи................................................................................ 64 3.4. Процесс конвертации и его трудности ..................................................................... 65 3.4.1. Преобразование в Кобол ......................................................................................... 65 3.4.2. Преобразование в VB .............................................................................................. 70 3.5. Обсуждение .................................................................................................................... 72 3.5.1. Программные факторы, влияющие на уровень автоматизации при языковых преобразованиях ................................................................................................................ 72 3.5.2. Экономические соображения при разработке автоматизированных средств преобразования языков ..................................................................................................... 74 3.5.3. Индустриальная проблема: нахождение компромисса между поставщиком услуг по реинжинирингу и заказчиком ........................................................................... 76 3.6. Заключение .................................................................................................................... 76 ГЛАВА 4. ИЗВЛЕЧЕНИЕ КЛАССОВ ИЗ УСТАРЕВШЕЙ СИСТЕМЫ................. 78 4.1. Краткое изложение предлагаемого подхода............................................................ 79 4.2. Предварительная структуризация программ......................................................... 80 4.2.1. Выделение процедур ............................................................................................... 80 4.2.2. Локализация или полное уничтожение GOTO ..................................................... 81
3
4.2.3. Локализация данных................................................................................................ 81 4.2.4. Оптимизирующие преобразования ........................................................................ 82 4.3. Переход к объектно-ориентированным программам............................................ 83 4.3.1. Попытка создания автоматического решения....................................................... 83 4.3.2. Некоторые эвристики для разбиения устаревших программ на классы ............ 84 4.3.3. Диалоговый процесс выделения классов............................................................... 86 4.3.4. Недостатки предложенного подхода и возможности дальнейшего усовершенствования.......................................................................................................... 87 4.4. Пример преобразования программы к объектно-ориентированному виду ..... 87 4.5. Другие подходы к созданию объектов ...................................................................... 91 4.5.1. Генерация класса, соответствующего всей программе ........................................ 91 4.5.2. Создание объектных интерфейсов к устаревшим программам........................... 91 4.5.3. Генерация классов по срезам программ ................................................................ 92 4.5.4. Перепроектирование с помощью CASE-средств.................................................. 93 4.6. Заключение .................................................................................................................... 93 ГЛАВА 5. ИСПОЛЬЗОВАНИЕ ПРОЕКТНО-ОРИЕНТИРОВАННЫХ НЕФОРМАЛЬНЫХ ЗНАНИЙ ПРИ РЕИНЖИНИРИНГЕ ....................................... 95 5.1. Связанные работы ....................................................................................................... 97 5.2. Формальная семантика и неформальные знания.................................................. 98 5.3. Интерактивное извлечение языка, характерного для данного проекта ......... 101 5.3.1. Понимание устаревших программ и настройка инструментальных средств... 102 5.3.2. Уточненная схема процесса извлечения языка проекта .................................... 106 5.3.3. Настраиваемая генерация...................................................................................... 107 5.4. Обсуждение .................................................................................................................. 108 5.5. Заключение .................................................................................................................. 110 ЛИТЕРАТУРА ........................................................................................................ 113 ПРИЛОЖЕНИЕ 1. ПРИМЕР РЕАЛИЗАЦИИ ОБЪЕКТНООРИЕНТИРОВАННОГО МАКРОРАСШИРЕНИЯ PL/I ....................................... 121
4
ПРИЛОЖЕНИЕ 2. РЕЗУЛЬТАТ ИЗВЛЕЧЕНИЯ КЛАССОВ В ПРОЦЕССЕ ПЕРЕНОСА ПРОГРАММЫ С КОБОЛА НА С++ ................................................ 123 ПРИЛОЖЕНИЕ 3. СПИСОК ИЛЛЮСТРАЦИЙ.................................................... 127
5
Введение Актуальность темы Программирование существует уже более 50 лет. За это время было написано огромное количество программ – согласно оценкам, приведенным в книге [50], объем созданного программного обеспечения превышает 800 миллиардов строк кода. Существует также более консервативная оценка [94], согласно которой объем реально используемых систем по состоянию на 1990 год составлял около 120 миллиардов строк кода. Кроме того, при создании программных систем использовались самые разнообразные языки программирования. Согласно оценкам из книги [53], 30% существующих в мире программ написаны на Коболе, 20% – на С/С++, 10% на Ассемблере, а остальные 40% программ написаны на одном из остальных распространенных
языков
программирования
распространенным
языкам
имеет
смысл
(утверждается,
отнести
еще
что
около
к
500
таким языков
программирования [61]). Объем написанного программного обеспечения (ПО) постоянно растет. С одной стороны, это обусловлено тем, что постоянно пишутся все новые и новые программы, но еще важнее то, что однажды написанные программы крайне медленно выходят из обращения. Многие программные системы живут десятилетиями и при этом не теряют своей актуальности. Так как технологии программирования развиваются очень быстро, то такие системы рано или поздно становятся устаревшими или унаследованными (legacy systems). Действительно, 220 миллиардов строк на Коболе говорят сами за себя. Таким образом, одной из центральных проблем программной инженерии становится сопровождение и эволюция ПО. Исследования показывают, что от 67 до 80% всех затрат жизненного цикла программы приходится именно на этап сопровождения [64]. Тем не менее, с течением времени структура сопровождаемых программ обычно ухудшается, и стоимость сопровождения заметно возрастает. Этот этап жизненного цикла программ характеризуется возникновением так называемого волнообразного эффекта возникновения ошибок [103] и постепенным старением программ [38, 72]. В тех случаях, когда программная система становится трудной в сопровождении, но все еще не потеряла своей экономической ценности, необходимо предпринять какие-то действия по ее улучшению. Одним из возможных путей выхода из этого кризиса является реинжиниринг программного обеспечения (software reengineering), т.е. изучение и изменение существующей системы с целью представления ее в новой, улучшенной форме, а также последующей реализации этой формы [27].
6
Одной из наиболее распространенных форм реинжиниринга являются языковые преобразования (language conversion), подразумевающие преобразование устаревших программ в эквивалентные им по функциональности программы на том же или другом языке высокого уровня. Первоначально активные исследования в этой области сводились к совершенствованию методов так называемой транслитерации, т.е. прямолинейной замены синтаксиса одного языка на синтаксис другого [44, 57]. Однако такой подход не всегда позволяет получить программы приемлемого качества на целевом
языке.
Поэтому
в
последние
годы
также
изучаются
возможности
преобразования языков с одновременным проведением других содержательных изменений, например, преобразование программ, написанных на процедурных языках, к эквивалентной объектно-ориентированной программе на современном языке [39, 47, 68, 118]. Другой актуальный вопрос реинжиниринга программ – это вовлечение человека в процесс трансформации устаревших систем. Потребность в участии человека связана с тем, что знания об устаревших системах постепенно теряются, и автоматическое восстановление таких знаний обычно не представляется возможным [12]. Таким образом, в процессе преобразования устаревшей системы желательно участие инженера, обладающего знаниями о рассматриваемой системе, и современные методики реинжиниринга должны предоставлять возможность учета и использования этих знаний [59]. Данная диссертация представляет собой попытку создания инструментального средства и методологии реинжиниринга, основанных на технологии преобразования языков и отвечающих современным требованиям к автоматизированным средствам подобного рода. Предлагаемый процесс реинжиниринга содержит возможность преобразования исходной программы в объектно-ориентированную форму на целевом языке ("извлечение объектов"), а также возможность настройки инструментального средства реинжиниринга на конкретную исходную систему с участием человека. Диссертация во многом основана на опыте и результатах работ, полученных автором во время участия в создании и применении на практике инструментального средства реинжиниринга RescueWare, поэтому мы начнем с краткого изложения истории этого проекта, его научного контекста, а также основных результатов диссертационной работы.
История проекта RescueWare Проект RescueWare имеет долгую и интересную историю. Задача написания автоматизированного средства преобразования языков была сформулирована в американской компании SEER в начале 1990-х годов. Однако эта задача оказалась
7
значительно сложнее, чем исходно предполагалось, и потому компания SEER начала искать потенциальных подрядчиков для выполнения этой работы в университетах. После
нескольких
неудачных
попыток
сотрудничества
с
американскими
университетами в 1994 году представители компании SEER начали искать исполнителей за рубежом. По различным причинам одним из первых направлений поиска стала Россия, и в 1994 году коллектив компании "ТЕРКОМ" и лаборатории системного программирования СПбГУ приступил к решению этой задачи. Автор участвовал в этом проекте с 1995 года. Исходно проект RescueWare был привязан к платформе HPS (High Productivity System), разрабатывавшейся и поддерживавшейся в то время компанией SEER. Поэтому первой задачей, поставленной перед нашим коллективом, было написание автоматического конвертора из языков Кобол и PL/I в Rules, внутренний язык системы HPS. Автор участвовал в создании синтаксического анализатора и написании синтеза целевого языка. Проект продолжался около двух с половиной лет и закончился созданием коммерческого продукта, получившего название RescueWare 1.0. К этому моменту участники проекта пришли к выводу, что целевой язык был выбран неудачно, так как потенциальный рынок продукта, привязанного к платформе HPS, был слишком мал. Поэтому некоторые представители заказчика начали продвигать идею создания средства реинжиниринга, ориентированного на генерацию распространенных современных языков программирования, таких как C++, Java и Visual Basic. С помощью этого средства предлагалось выйти на новый для компании SEER рынок реинжиниринга устаревшего программного обеспечения. Однако перспективность этого рынка ставилась многими противниками этой идеи под сомнение. Эти разногласия привели в 1997 к отделению небольшой группы сотрудников SEER и созданию абсолютно новой компании, получившей название Relativity Technologies. Эта компания предполагала создать новый продукт для преобразования устаревших систем и выйти с ним на рынок реинжиниринга, получая прибыль как от продаж продукта, так и от применения этого продукта к реальным устаревшим системам. Все задачи по разработке программного обеспечения для Relativity Technologies были возложены на все тот же российский коллектив. В 1997 году началась разработка принципиально нового продукта, получившего рабочее название RescueWare NT. При создании этого продукта был учтен весь опыт, накопленный коллективом разработчиков в процессе работы над RescueWare 1.0. В частности, одним из важных решений была ориентация на многоязыковость средства – с самого начала планировалось, что RescueWare будет поддерживать множество входных и выходных языков (хотя вначале исходный язык был только один – Кобол).
8
К 1998 году появилась новая версия продукта, получившая название RescueWare 3.0, и начались проекты по применению этого продукта к реальным устаревшим системам. В этом же году была написана первая академическая статья, описывающая историю проекта в целом и архитектуры RescueWare, сложившейся к тому времени [122] (к сожалению, данная статья и весь сборник статей [121] по техническим причинам были опубликованы только в 2000 году). Кроме того, в 1998 году начались работы по созданию "дополнений" (add-ons) к RescueWare.
По
иронии
судьбы,
первым
дополнительным
входным
языком,
поддержанным в RescueWare NT, стал язык HPS*Rules – тот самый язык, который служил целевым языком для RescueWare 1.0. С начала 1999 года автор возглавляет группу разработчиков, участвующих в создании именно этой подсистемы RescueWare. В 1999 году в связи с резким увеличением количества работ по реинжинирингу конкретных систем в нашем коллективе появился отдел консалтинга. Для увеличения эффективности работы к участию в проектах этого отдела привлекались и разработчики инструментальных средств. В рамках таких работ в 2000 году автор принимал участие в переводе крупной промышленной системы из HPS*Rules в языки Кобол и Visual Basic. Проект длился около полугода, причем большая часть работы была выполнена во время длительной командировки автора в США. Опыт участия в подобных проектах позволил разработчикам получить совершенно иной взгляд на свои повседневные задачи создания инструментальных средств. В частности, с появлением первых проектов по реинжинирингу конкретных устаревших систем стало ясно, что любой реинжиниринг устаревшей системы требует активного участия человека. Поэтому устаревших
RescueWare программ
интерактивную предоставляет
систему
стало и
постепенно
извлечения
знаний,
реинжиниринга.
пользователю
полный
обрастать
На
спектр
средствами
превращаясь сегодняшний возможностей
в день по
понимания
полноценную RescueWare анализу
и
преобразованию устаревшей системы: •
Поддержка процессов возвратного проектирования, включая средства анализа и навигации по исходным текстам, средства построения диаграмм, создание словарей системы, редокументация и т.п. [106, 7]
•
Извлечение знаний, включая создание срезов программы и компонентизацию приложения [108].
•
Генерация программ на современных языках программирования по устаревшей системе [110, 113]. На данный момент, в состав продукта входят универсальный
9
конвертор из Кобола, PL/I и Adabas Natural в C++, Java и Visual Basic, а также конверторы из HPS*Rules в Кобол, Java и Visual Basic.
Научный контекст работ по созданию RescueWare Изначально проект по разработке RescueWare воспринимался как обычная производственная задача. Более того, казалось, что написание средства реинжиниринга не слишком отличается от создания компилятора, с отличиями только на этапе генерации кода, так как вместо ассемблера необходимо было порождать другой язык программирования высокого уровня. Как уже упоминалось выше, такая точка зрения на средства реинжиниринга оказалась слишком узкой и недостаточной для решения промышленных задач по преобразованию реальных устаревших систем. Поэтому нам пришлось искать другие подходы к решению задач реинжиниринга. Мы решили обратиться к опыту других проектов в данной области. Оказалось, что помимо нас решением задач реинжиниринга программ занимаются как многочисленные промышленные компании, так и несколько исследовательских коллективов из различных стран. Первыми книгами, с которыми мы ознакомились, стали книги [69, 20, 2]. Особенно полезной оказалась последняя из них. Эта книга, представляющая собой нечто среднее между справочником и учебным пособием, была выпущена в 1993 году в авторитетном издательстве IEEE Computer Society Press под редакцией известного исследователя проф. Роберта Арнольда (Robert Arnold). В каком-то смысле, эта книга представляет собой подведение основных результатов, полученных в данной области до 1993 года. Основу книги составили наиболее значимые статьи, опубликованные мировыми исследователями в различных журналах и представленные на международных конференциях. Кроме того, специально для книги был написан целый ряд обзорных статей по отдельным вопросам реинжиниринга. Изучение чужих работ оказалось для нас очень полезным. В некоторых случаях выяснилось, что мы повторили результаты, полученные другими исследователями. В других случаях изучение чужих результатов позволило нам существенно улучшить собственное средство реинжиниринга. Наконец, многие результаты, достигнутые в проекте RescueWare, оказались соответствующими мировому уровню исследований или даже превосходящими этот уровень. По этой причине в 1998 году в нашем коллективе начались работы по написанию сборника статей, отражающих основные научные результаты, полученные в процессе работы над продуктом RescueWare. По различных техническим причинам, эта книга увидела свет только через два с половиной года. В конце 2000 года в издательстве
10
Санкт-Петербургского государственного университета был выпущен сборник научных статей "Автоматизированный реинжиниринг программ" (под редакцией проф. А.Н. Терехова и А.А. Терехова) [121]. В этом сборнике было представлено 19 научных статей двадцати одного автора. Все эти статьи излагали различные технические и исследовательские результаты работ над проектом RescueWare. Постоянная и активная научная работа в области реинжиниринга продолжается в нашем коллективе и сегодня.
Основные результаты диссертационной работы В данной диссертационной работе получены следующие основные результаты: 1. Показаны принципиальные отличия между средствами реинжиниринга и компиляторами, обуславливающие неприменимость стандартных критериев оценки компиляторов к средствам реинжиниринга, основанным на языковых преобразованиях. 2. Предложен
метод
оценки
осуществимости
языковых
преобразований,
основанный на сравнении выразительной мощности исходного и целевых языков программирования. 3. Предложен новый метод преобразования устаревших систем, основанный на последовательном применении преобразований в рамках исходного языка, реструктуризации программ с последующим преобразованием языков и оптимизации полученных результатов в рамках целевого языка. 4. Предложен метод преобразования языков, основанный на эмуляции типов данных и конструкций, отсутствующих в целевом языке. 5. Продемонстрированы
противоречия
между
достижением
максимальной
автоматизации процесса языковых преобразований и качеством получаемого кода. 6. Предложена методология создания объектно-ориентированных программ по устаревшим системам. 7. Продемонстрирован потенциал методов настройки средств реинжиниринга, основанных на использовании неформальных знаний о системе, на примере улучшения качества кода, порождаемого при языковых преобразованиях. Все основные результаты диссертационной работы являются новыми.
11
Апробация работы Результаты диссертации докладывались на семинаре Института системного программирования (2002 год, Москва), а также на конференциях по сопровождению программного обеспечения (ICSM 2001, Флоренция, Италия), сопровождению и реинжинирингу программного обеспечения (CSMR 2002, Будапешт, Венгрия) и средствам для объектно-ориентированных языков и систем (TOOLS EE 2000, София, Болгария). Часть результатов была опубликована в журнале IEEE Software. Основные результаты работы изложены в 10 публикациях, написанных при непосредственном участии автора [120, 122, 123, 118, 104, 119, 90, 92, 91, 17]. Методы и полученные результаты данной диссертационной работы были реализованы как составные средства продукта RescueWare и успешно прошли практическую проверку на крупных проектах по преобразованию промышленных программных систем.
Благодарности Автор
диссертации
хотел
бы
выразить свою
глубокую
благодарность
и
признательность всем людям, прямо или косвенно помогавшим в работе над данной диссертацией: •
Участникам проекта RescueWare, особенно руководителям и архитекторам проекта (Лен Эрлих, Валентин Оносовский, Михаил Громов, Татьяна Попова, Александр Апрелев, Олег Плисс, Михаил Бульонков и многие другие)
•
Соавторам статей (Крис Верхуф, Андрей Терехов-ст., Лен Эрлих, Дмитрий Булычев, Дмитрий Кознов, Александра Береснева)
•
Коллегам, помогавшим мне улучшить текст статей (Карина Терехова, Дмитрий Байков, Анна Голодная, Гарри Снид, анонимные рецензенты)
•
Неформальному научному руководителю, помогавшему мне повысить научный уровень исследований (Андрей Терехов-ст.)
•
Всем авторам сборника "Автоматизированный реинжиниринг программ", который я редактировал
•
Студентам 4 курса, принявшим участие в организованном мною спецкурсе "Реинжиниринг программного обеспечения"
•
Родным и близким за моральную поддержку в процессе написания диссертации
12
Глава 1. Обзор задач реинжиниринга В данном разделе описывается текущее состояние дел в области реинжиниринга программного обеспечения. Кратко изложены основные научные достижения и их практические приложения, сформулированы некоторые перспективные направления развития исследований. Кроме того, здесь описывается научный и практический контекст диссертационной работы, производится попытка сопоставления результатов, полученных в данной диссертации, с мировыми достижениями в области языковых преобразований и реинжиниринга в целом. Поскольку в области реинжиниринга пока что не существует устоявшейся русской терминологии, большинство переведенных терминов сопровождаются английским эквивалентом, а также некоторым общепринятым определением. Глава организована следующим образом. Первая часть содержит обзор основных вопросов, изучаемых под общим названием "реинжиниринг". Как мы увидим, реинжиниринг
представляет
собой
очень
широкую
область
исследований,
простирающуюся от вопросов понимания программ и извлечения знаний до таких специфических приложений, как преобразование языков программирования. Во второй части введения обсуждаются средства автоматизированного преобразования языков программирования (language conversion), являющиеся основной темой диссертации. Наконец, третья часть посвящена различным смежным вопросам (сопровождение, повторное использование и т.п.).
1.1. Реинжиниринг и его экономические предпосылки Прежде всего, необходимо зафиксировать основные понятия, с которыми мы будем работать. Процитируем определение из классической статьи Чикофски и Кросса (Chikofsky and Cross) [27], в которой была предложена общепринятая таксономия данной предметной области: "Реинжиниринг – это изучение и изменение существующей системы с целью представления ее в новой форме, а также последующей реализации этой формы". Так как в данном определении упоминается "существующая система", то становится очевидно, что реинжиниринг должен рассматриваться в более широком контексте сопровождения и эволюции программных систем. Действительно, реинжиниринг – это всего лишь один из возможных сценариев развития сопровождаемой программной системы. Чаще всего решение о реинжиниринге принимается только в тот момент, когда становится ясно, что другие варианты сопровождения системы себя уже исчерпали или появились принципиально новые и более совершенные технологии.
13
Сопровождение устаревших систем обычно связано с большими затратами, так как изменения внешних условий и обнаружение ошибок в программах вынуждают постоянно вносить в систему все новые и новые изменения. При этом, чем хуже написана исходная программа, тем выше стоимость последующего сопровождения. Если поток управления программы запутан и неясен, а различные компоненты системы чрезмерно зависят друг от друга, то даже небольшие исправления могут иметь заметные побочные эффекты. Например, в книге [40] приведены результаты исследований, проведенных в одной крупной компании, занимающейся сопровождением программ. В результате этих исследований
выяснилось,
что
даже
однострочное
изменение
программы
с
вероятностью 55% сохраняло ошибку или вносило новую. Естественно, эти показатели могут
быть
существенно
улучшены
за
счет
улучшения
самого
процесса
сопровождения, введения формальных технических рецензий кода и т.д. Тем не менее, на практике в большинстве случаев стоимость внесения исправлений рано или поздно становится слишком высокой, система постепенно теряет гибкость и перестает справляться со стоящими перед ней задачами. До возникновения такой ситуации необходимо предпринять какие-то действия. Одним из возможных путей выхода из этого кризиса является реинжиниринг программной системы. Руководство по сопровождению программного обеспечения, опубликованное американским бюро стандартов (National Bureau of Standards) в 1983 году предлагает 11 критериев, помогающих определить необходимость произведения реинжиниринга [95]: •
при частых отказах системы;
•
если система была написана более 7 лет назад;
•
если структура программы и ее логическая схема становятся слишком сложными;
•
если система была написана для предыдущего поколения аппаратных средств;
•
если программа работает в режиме эмуляции;
•
если размер модулей или компонент становится слишком большим;
•
если для запуска программы требуются чрезмерно большие ресурсы;
•
если надо изменить жестко запрограммированные параметры;
•
если стоимость сопровождения становится слишком большой;
•
если документация устарела;
•
если спецификации проекта утеряны, неполны или устарели.
14
Из этого списка критериев можно вывести основные ожидания владельцев систем от реинжиниринга – чаще всего, основной задачей реинжиниринга является улучшение сопровождаемости системы, хотя встречаются и другие причины: •
уменьшение количества ошибок;
•
перенос системы на новую платформу;
•
увеличение времени жизни системы;
•
поддержка изменений бизнеса компании и т.д.
Наиболее наглядным образом возможные решения проблем сопровождения были сформулированы в известной статье [47] (см. рис. 1). Изменяемость
Сопровождать
Улучшать
Выбрасывать
Проводить реинжиниринг
Ценность для бизнеса Рис. 1. Варианты развития устаревшей системы.
Эта диаграмма служит всего лишь общим руководством к действию, однако существуют и численные методики расчета эффективности реинжиниринга устаревшей системы [84, 85]. Полное рассмотрение этих методик выходит за рамки данной работы, мы ограничимся формулировкой основных соображений, лежащих в основе экономического анализа вариантов развития устаревшей системы. Можно выделить четыре основных фактора, влияющие на выбор между реинжинирингом, новой реализацией и дальнейшим сопровождением существующей системы. Это: •
соотношение между стоимостью реинжиниринга, стоимостью новой реализации и дальнейшего сопровождения;
•
соотношение между ценностью системы после реинжиниринга, новой системы и существующей;
•
соотношение между факторами риска реинжиниринга, новой реализации и сохранения системы в прежнем состоянии;
15
•
соотношение между предполагаемым временем жизни системы в случае реинжиниринга, реализованной заново системы и существующей на данный момент.
Чем точнее удастся определить издержки, прибыли, риск и сроки выполнения работ, чем правильнее будет оценено качество и функциональность программного продукта, тем больше шансов принять верное решение. Отметим, что качество систем, полученных при разработке заново и при сопровождении/реинжиниринге, может заметно различаться. Тем не менее, владельцы систем обычно готовы идти на некоторые компромиссы, помогающие продлить жизнь полезной программной системы, особенно учитывая относительно невысокую стоимость работ по реинжинирингу – считается, что проведение реинжиниринга имеет смысл в тех случаях, когда его стоимость составляет не более 50% от стоимости разработки заново. Другой
подход,
позволяющий
снизить
стоимость
работ,
заключается
в
преобразовании лишь наиболее критичных фрагментов кода. Такой подход имеет смысл, так как обычно наибольшие проблемы вызывает относительно небольшой участок кода (так называемый закон Парето: "80% всех трудностей и ошибок сосредоточены в 20% кода"; аналогичную статистику приводил еще в 1970-х годах Ф.П. Брукс: "47% ошибок проекта OS/360 были сосредоточены в 4% кода" [105]). Таким образом, в тех случаях, когда реинжиниринг всей системы чрезмерно дорог, имеет смысл выделить наиболее важные компоненты системы и проводить реинжиниринг только этих компонент. Еще раз подчеркнем, что реинжиниринг бессмысленно рассматривать в отрыве от экономической
составляющей
программного
обеспечения:
если
убрать
из
рассмотрения экономические параметры создания программных систем (т.е. время разработки, материальные и человеческие ресурсы), то наилучшим выходом из кризиса сопровождения всегда будет полное переписывание системы "с чистого листа". Однако на практике полное переписывание существующей системы с использованием современных технологий зачастую оказывается экономически неосуществимым – начиная с некоторого объема системы, повторная реализация существующей функциональности ("революционный" подход) оказывается чрезмерно дорогостоящей, и потому единственным вариантом развития системы оказывается реинжиниринг ("эволюционный" подход). Последнее утверждение неочевидно, так как противоречит повседневному опыту подавляющего большинства программистов. Среди программистов-практиков весьма распространено мнение, что нахождение и использование знаний, заключенных в
16
существующей программной системе, занимает значительно больше времени, чем написание системы заново. Однако это утверждение верно лишь наполовину: понимание устаревшей системы – это действительно трудоемкий процесс, однако разработка устаревшей системы заново может оказаться и вовсе экономически невыполнимой задачей. Сторонники разработки заново обычно подкрепляют свою позицию утверждением, что разработка программного обеспечения более производительна, чем сопровождение. Для небольших программ это действительно так, но с увеличением объема рассматриваемой системы уровень производительности разработки и сопровождения начинает сходиться (см. рис. 2, заимствованный из [89]):
Рис. 2. Зависимость производительности разработки и сопровождения от размера системы
На рис. 2 оси координат соответствуют размеру исходной системы, измеряемой в функциональных точках входа1 и средней производительности разработки и сопровождения системы за один человеко-месяц. На графике жирной линией обозначена средняя производительность разработки ПО, а точечной линией – средняя производительность сопровождения, в зависимости от размера программной системы.
1
Функциональные точки входа (function points или, сокращенно, f.p.) – это метрика, предназначенная для
оценки размера программного обеспечения на ранних этапах жизненного цикла. Одна функциональная точка входа должна соответствовать одной бизнес-функции с точки зрения конечного пользователя. Для приложений, активно работающих с данными, такая метрика дает удовлетворительные результаты
17
Из рисунка видно, что производительность разработки для относительно небольших систем (до 100 f.p.) действительно больше, чем производительность сопровождения аналогичных систем. Однако для систем с объемом больше, чем 100 f.p. производительности разработки и сопровождения уже практически неотличимы. Таким образом, основная психологическая проблема сравнения разработки заново и сопровождения заключается в том, что опыт большинства программистов ограничен относительно небольшими по объему программными системами. Экстраполяция такого опыта на большие системы приводит к полностью неверным выводам, так как для больших систем основной составляющей затрат становится не написание кода, а понимание требований к системе и фиксирование этих требований в проектной документации. Например, согласно оценке, приведенной в книге [49], для успешного сопровождения системы из 750,000 строк необходимо подготовить порядка 60,000 страниц документации. Другая интересная оценка приведена в статье [14]: на больших программных проектах до двух третей всех усилий приходится на документацию, а не на исходный текст. Понятно, что ни один человек не в состоянии самостоятельно охватить такой объем знаний. Итак, для достаточно больших систем разработка аналогичной системы "с нуля" экономически невозможна. Можно привести грубую оценку верхней границы целесообразности полного переписывания устаревшей системы – такой подход имеет смысл для систем с объемом меньше 1000 f.p., что примерно соответствует 100 000 строк кода на Коболе или 130 000 строк на С. В дальнейшем мы не будем останавливаться на экономическом аспекте реинжиниринга, предполагая, что мы имеем дело с системой, имеющей высокую ценность для бизнеса, но малую изменяемость, так как именно для таких систем реинжиниринг системы и переход на новые технологии является наиболее выгодным решением. Такое предположение является абсолютно оправданным, так как все примеры реинжиниринга, рассматриваемые в данной диссертации, основаны на опыте преобразования реальных устаревших систем, владельцы которых обычно проводят формальный анализ стоимости владения принадлежащих им систем перед принятием решения о проведения реинжиниринга системы.
1.2. Основные задачи реинжиниринга Реинжиниринг
программного
обеспечения
представляет
собой
практически
необозримую область исследований, включающую в себя десятки направлений исследований, и в рамках одной работы невозможно полноценно осветить все направления. Диссертационная работа посвящена изучению только одной из интересных проблем реинжиниринга – языковых преобразований (language conversion).
18
Тем не менее, в целях полноты изложения мы приведем в данном разделе краткое описание и других содержательных задач реинжиниринга. Вначале мы рассмотрим задачи, относящиеся к возвратному проектированию (reverse engineering; распространены также более узкие термины program comprehension и legacy understanding) и ориентированные на анализ устаревших систем. Затем мы рассмотрим самостоятельную область исследований, называемую извлечением знаний (knowledge mining). По своему характеру это направление занимает некоторое промежуточное положение между возвратным проектированием и реинжинирингом. В заключение мы рассмотрим преобразование устаревших систем (legacy transformation, широко распространены также термины software renovation и software reclamation). Под этим понимается изменение реализации рассматриваемой системы с целью улучшения ее характеристик. В данном обзоре мы рассмотрим два наиболее крупных направления в этой области – реструктуризацию программ и языковые преобразования. Определения и описания, приведенные в данном разделе, преимущественно заимствованы из статей [27, 3, 65].
1.2.1. Возвратное проектирование Возвратное
проектирование
(reverse
engineering)
–
это
процесс
анализа
рассматриваемой системы с целью идентификации компонент системы и их взаимодействий или с целью создания некоторого представления системы в другой форме на более высоком уровне абстракции. Таким образом, в процессе создания программ мы движемся от абстрактных высокоуровневых понятий (требования, спецификации) к их низкоуровневой реализации, а при возвратном проектировании мы движемся в обратном направлении. Для того чтобы лучше различать эти два процесса, объединенные
одним
словом
engineering,
в
англоязычной
литературе
по
реинжинирингу для создания программ ввели словосочетание forward engineering. Возвратное проектирование обычно включает в себя извлечение проектной документации, а также создание или синтез абстрактных представлений системы, которые меньше зависят от деталей реализации, чем сама система. Отметим, что чаще всего возвратное проектирование производится на основе устаревшей системы, но определение этого не подразумевает. Например, возвратное проектирование может производиться над другими абстракциями системы или даже в более ранних этапах жизненного цикла программы – до того, как система сдана в эксплуатацию. Важно понимать, что возвратное проектирование не включает в себя изменение исходной системы или создание новой системы на базе существующей. Возвратное
19
проектирование – это только изучение, а не изменение или репликация существующей системы (если такие изменения все-таки происходят, то мы уже имеем дело не с возвратным проектированием, а с реинжинирингом). Возвратное проектирование является чрезвычайно широкой областью исследований и включает в себя множество более мелких задач, среди которых хотелось бы выделить следующие: •
Понимание программ (program understanding), исходящее из неявного предположения, что основным источником информации о системе обычно является исходный текст программ – в противоположность обобщенному возвратному проектированию, которое может отталкиваться от исполнимой формы системы или даже от высокоуровневого описания архитектуры системы. Понимание программ – это, прежде всего, когнитивная наука и потому в данной области изучаются такие вопросы, как мысленные процессы человека при разборе исходных текстов программ, средства визуализации
программ,
средства
построения
диаграмм,
средства
форматирования исходных текстов и т.д. •
Редокументация (redocumentation), определяемая как процесс реорганизации системы, результатом которого является семантически эквивалентное представление системы на том же уровне абстракции, предназначенное для улучшения
понимания
системы
человеком.
Получаемые
формы
представления системы обычно являются альтернативными взглядами, удобными для восприятия человеком (например, потоки данных в системе, структуры данных, поток управления программы и т.д.). Одной из наиболее распространенных форм редокументирования является
комментирование
программ – известно, что комментарии к программам являются первичным источником
информации
для
программистов,
(architecture
extraction),
занимающихся
сопровождением [43]. •
Извлечение
архитектуры
определяемое
как
определение архитектурных решений по данной программной системе (например, по исходным текстам). Извлечение архитектуры может быть использовано в целях улучшения сопровождения системы (например, для фиксации тех решений, которые не должны изменяться в процессе сопровождения), при сравнении двух различных реализаций одного и того же алгоритма и для прочих подобных задач. •
Извлечение проектных решений (design recovery), определяемое как "подмножество возвратного проектирования, в котором знания о предметной
20
области, внешняя информация и выводы, сделанные человеком, добавляются к наблюдениям об изучаемой системе". Задачей извлечения проектных решений
является
идентификация
значимых
абстракций
предметной
области, причем более высокого уровня, чем те, что могут быть получены путем изучения самой системы.
1.2.2. Извлечение знаний Извлечение знаний является одной из разновидностей возвратного проектирования, при котором конечной целью работ является идентификация и последующее оформление в виде самостоятельных компонент полезных знаний, заключенных в устаревшей системе. Для извлечения знаний система анализируется, и из нее извлекаются так называемые бизнес-правила (business rules), представляющие собой относительные небольшие фрагменты кода, реализующие ясно определенный и замкнутый набор функциональности. Одной из наиболее распространенных технологий извлечения знаний является построение статических срезов программ (static program slicing). Задача построения статического среза заключается в построении новой программы с меньшим количеством операторов, но с тем же самым поведением относительно заранее фиксированной переменной в некоторой точке программы [100, 65]. При традиционном подходе к построению срезов программ размер программы уменьшается за счет простого удаления операторов, не влияющих на интересующие нас переменные (так называемые срезы с сохранением синтаксиса, syntax-preserving slices). Другим распространенным подходом является построение так называемых аморфных срезов программ (amorphous slicing) [45, 13], к которым предъявляется только одно требование – сохранение исходной функциональности. При этом срез не обязан быть точным синтаксическим подмножеством исходной программы. Понятно, что аморфные срезы программ могут быть значительно меньше, чем срезы с сохранением синтаксиса, но в то же время аморфные срезы менее узнаваемы (и, следовательно, труднее в сопровождении), чем срезы с сохранением синтаксиса. К сожалению, срезы программ зачастую получаются весьма объемными – легко сконструировать примеры, в которых единственно возможным срезом при заданном критерии является вся программа. По этой причине с целью уменьшения объема срезов программ современные исследования рассматривают помимо статических срезов программ и динамические срезы (dynamic program slices) [1, 58], в которых все входные данные для программы известны в момент построения среза (предполагается, что срез строится после выполнения программы), и условные срезы программ (conditioned slices) [23], при
21
построении которых известна только некоторая часть информации об исполнении программы. После извлечения бизнес-правил, необходимо оформить их в виде самостоятельных и готовых к использованию компонент (этот этап называется компонентизацией). Для этого бизнес-правила выделяются в отдельные модули, определяются входные и выходные параметры этих модулей, удаляются неиспользуемые участки кода и т.п. Отметим, что извлечение знаний включает в себя как действия, характерные для возвратного проектирования (построение срезов программ может рассматриваться как метод анализа устаревшей системы), так и активные преобразовательные действия, традиционные для реинжиниринга программ (вынос бизнес-правил в отдельные модули, замена соответствующих фрагментов кода на вызов выделенных бизнесправил и т.д.).
1.2.3. Реструктуризация программ Согласно
определению
из
статьи
[3],
реструктуризация
программ
–
это
модификация программного обеспечения с целью облегчения сопровождения, а также для уменьшения вероятности ошибок в процессе дальнейшего развития системы2. Отметим, что не всякое изменение программного обеспечения попадает под это определение – например, оптимизация программ, очевидно, ведет к модификации программ, но обычно не облегчает сопровождение системы и потому не является реструктуризацией. Реструктуризация программ включает в себя следующие методы: •
замена некоторых операторов программ на эквивалентные им структурные операторы в целях упрощения потока управления (control flow restructuring);
•
выравнивание исходных текстов и введение отступов (pretty printing);
•
приведение программ в соответствие со стандартами компании.
Все перечисленные задачи (кроме первой) носят технический характер и потому редко исследуются специально. В то же время задача структуризации потока управления значительно более содержательна и потому систематически изучалась с 1960-х годов. Причиной для начала исследований стало осознание трудности
2
В данном определении под "программным обеспечением" иногда понимают не только исходные тексты,
но и документацию к ним, но мы будем рассматривать только реструктуризацию собственно программ, так как в используемой нами классификации работа с документацией уже упомянута как одна из задач возвратного проектирования.
22
понимания и сопровождения программ, написанных на плохо структурированных языках типа Фортран и Кобол. Самой первой работой, видимо, является появившаяся в 1966 году статья [14], в которой было показано, что любые программы могут быть записаны с помощью следующих трех конструкций: SEQUENCE, IF-THEN-ELSE и DO-WHILE, и, следовательно,
операторы
безусловного
перехода
не
являются
теоретически
необходимыми для создания программ. Приведенное в статье конструктивное доказательство в принципе можно считать первым алгоритмом реструктуризации программ. Дополнительным толчком к развитию данного направления послужило появление в 1968 году классической статьи Э. Дейкстры "Go to statement considered harmful" [36], в которой утверждалось, что оператор goto не только не является необходимым, но и зачастую ведет к неясностям и логическим ошибкам. В 1971 году в статье [5] был предложен алгоритм уничтожения операторов безусловного перехода с помощью использования конструкций WHILE и CASE (так называемый "giant case statement approach"). Позже были предложены некоторые усовершенствования данного подхода; схожий подход описан в статьях [114, 115]. В 1977 году был предложен алгоритм, дающий более удачные результаты, но допускающий сохранение некоторых операторов goto в конечной программе [10]. Этот алгоритм впоследствии был использован в средстве структуризации программ на Фортране struct. В задачах сопровождения и реинжиниринга реструктуризация используется очень часто. Эффективность реструктуризации программ подтверждается статистическими исследованиями –
так,
например,
в
отчете
[6]
утверждается,
что
после
реструктуризации время сопровождения и тестирования уменьшается в среднем на 44%. В последнее время реструктуризация чаще всего используется не как самостоятельное средство улучшения сопровождения, а как предварительный этап перед применением других методов (например, как составная часть процесса языковых преобразований).
1.2.4. Языковые преобразования Задачей языковых преобразований (language conversion, source-to-source translation) является преобразование программ в эквивалентные им по функциональности программы
на
том
же
или
другом
языке
высокого
уровня.
К
языковым
преобразованиям можно отнести следующие классы задач: •
Обычный сценарий языковых преобразований, т.е. преобразования программ, написанных на одном языке высокого уровня, в эквивалентные им программы
23
на другом языке высокого уровня (например, преобразование Кобола в C++). Именно этому классу языковых преобразований будет уделяться наибольшее внимание в данной диссертации. •
Преобразования программ с сохранением языка программирования (так называемые
преобразования
диалектов,
dialect
conversion).
В
данной
диссертации мы рассмотрим некоторые проблемы преобразования диалектов и покажем, что эта задача сложнее, чем принято думать. •
Преобразование ассемблера в языки высокого уровня (так называемая декомпиляция, decompilation). На сегодняшний день, декомиляция представляет собой самостоятельную и несколько обособленную дисциплину, поэтому рассмотрение вопросов декомпиляции не входит в задачи данной диссертации. Наиболее известные работы в данной области принадлежат Кристине Сифуэнтес (Cristina Cifuentes) [28, 29].
На первый взгляд, задачи языковых преобразований практически не отличаются от компиляторных задач. Многие исследователи даже включают средства языковых преобразований в классификацию компиляторов под названием конверторов. Однако такая классификация представляется слишком ограниченной, так как в ней не учитывается специфика предметной области, на которую ориентированы средства языковых преобразований. При ближайшем рассмотрении оказывается, что задачи языковых преобразований и компиляторов различаются достаточно сильно, а потому различаются и требования к этим средствам, и типичные сценарии их использования. Перечислим наиболее существенные различия между компиляторами и средствами реинжиниринга: •
Процесс компиляции неизбежно понижает уровень программы (от языка высокого уровня мы переходим к ассемблеру некоторой – возможно, виртуальной – машины), в то время как процесс реинжиниринга обычно подразумевает, как минимум, сохранение выразительного уровня исходной программы, а возможно, даже его повышение (например, это может произойти во время перехода от процедур к объектно-ориентированным конструкциям).
•
При компиляции неизбежно происходит потеря некоторой информации о программе [109] (например, теряются имена переменных, комментарии и т.п.), в то время как при реинжиниринге обычно ставится задача сохранения практически всей информации, содержавшейся в исходной программе. Этот тезис можно развить и дальше: на самом деле, основной проблемой реинжиниринга является тот факт, что к началу работ по реинжинирингу
24
большое количество информации уже утеряно – например, исходная постановка задачи, проектные решения и т.п. обычно недоступны для программистов, проводящих реинжиниринг. Все, что доступно при реинжинринге, – это одна конкретная реализация рассматриваемой системы, отражающая как исходную постановку задачи, так и ее последующие уточнения. •
В компиляторах нет практически никаких ограничений на вид генерируемого кода,
в
то
время
как
в
конверторах
обычно
ставится
требование
сопровождаемости и читаемости целевого кода. Обычно к конверторам предъявляется даже требование узнаваемости генерируемого кода, в таких случаях генерированный текст должен иметь ту же общую структуру, что и исходная программа. По этой причине аморфные срезы программ используются достаточно редко, так как в практических задачах реинжиниринга выигрыш в размерах получаемых срезов менее важен, чем сходство с исходной программой. •
Соображения читаемости влияют и на вопрос оптимизации программ: в компиляторах чаще всего нет никаких требований к внешнему виду оптимизированной программы, в то время как в конверторах оптимизированный код должен быть не менее легким в сопровождении, чем исходный. Такое ограничение делает неприменимым в задачах реинжиниринга целый ряд оптимизаций (развертывание циклов, распространение констант и т.п.), хотя оставляет актуальными такие оптимизации, как удаление недостижимого кода, уничтожение неиспользуемых структур данных и т.д.
•
Компиляция одной и той же программы проводится очень часто (в любом случае – многократно, хотя бы из соображений отладки), в то время как активная трансформация системы, характерная для задач реинжиниринга (скажем, перенос на новую платформу, переход к новому языку и т.п.) чаще всего выполняется всего лишь один раз. Иногда трансформация системы выполняется несколько раз (например, в тех случаях, когда по ходу проекта приходится уточнять само средство реинжиниринга), но в любом случае, количество преобразований системы редко бывает большим.
"Штучность" задач реинжиниринга проявляется не только в частоте использования средств трансформации. По большому счету, все устаревшие системы по-своему уникальны, так как характеризуются огромным количеством параметров, включая язык программирования (например, по оценкам, приведенным в [61], существует более 300 диалектов Кобола, включая даже диалекты, специфичные для какого-либо конкретного предприятия!), аппаратной платформой, инструкциями по сборке и т.п. При таком
25
разнообразии исходных систем в некоторых случаях экономически более выгодно предоставить окончательную доработку результатов реинжиниринга человеку, чем проводить
дорогостоящую
доработку
самих
инструментальных
средств
ради
однократного их применения. Очевидно,
что
использование
компиляторов
основывается
на
прямо
противоположной предпосылке: любая программа, которая не может быть успешно пропущена через транслятор, считается ошибочной и требует последующей доработки программистом.
Таким
образом,
компиляторные
техники
основываются
на
предположении о фиксированности исходного языка. В то же время в задачах реинжиниринга та же самая ситуация может быть разрешена как минимум тремя способами: •
Устаревшая программа также может быть доработана для достижения соответствия данному средству реинжиниринга (хотя надо отметить, что такой сценарий не совсем типичен для работ по реинжинирингу).
•
В
устаревшей
программе
могут
быть
закомментированы
фрагменты,
вызывающие проблемы в процессе реинжиниринга. В таком случае чаще всего подразумевается,
что
после
трансформации
устаревшей
системы
закоментированные фрагменты будут вручную дописаны программистами, выполняющими
реинжиниринг.
Семантика
самих
программ
устаревшей
системы обычно считается неизменяемой. •
Наконец, само средство реинжиниринга может быть доработано для достижения адекватной поддержки данной устаревшей системы. Этот сценарий является достаточно массовым, например, такой подход употребляется для поддержки вновь встреченных диалектов устаревшего языка.
Все три приведенные сценарии объединены тем, что для их реализации необходимо участие
программистов,
разбирающихся
не
только
в
исходном
языке
программирования, на котором написана устаревшая система, но и в особенностях используемого средства реинжиниринга. Таким образом, можно утверждать, что реинжиниринг предъявляет более жесткие требования к квалификации и технической оснащенности программистов, чем средства компиляции. Итак,
традиционные
трансляторные
технологии
зачастую
оказываются
недостаточными для создания средств языковых преобразований. Более подробно проблемы языковых преобразований и вытекающие из них требования к средствам языковых преобразований рассмотрены в главе 2.
26
1.3. Смежные вопросы реинжиниринга Реинжиниринг является лишь одним из возможных решений общей проблемы устаревших систем, поэтому реинжиниринг необходимо изучать в контексте других дисциплин, прежде всего, сопровождения программ и повторного использования программ.
1.3.1. Сопровождение программ Как уже говорилось выше, реинжиниринг чаще всего является попыткой решить проблемы сопровождения. Поэтому имеет смысл изложить основные задачи сопровождения программ, терминологию, используемую в этой области, и основные проблемы. Сопровождение программ — это долгосрочный процесс, длящийся иногда десятки лет. К сожалению, в течение долгого времени этот этап жизненного цикла программ был незаслуженно обойден вниманием исследователей – достаточно сказать, что одна из первых монографий на эту тему появилась только в 1981 году [43]. В то же время, сопровождение является одним из наиболее затратных этапов жизненного цикла программ – еще в конце 1970-х известное исследование [64], проведенное Линцом, Свонсоном и Томпкинсом, показало, что на этап сопровождения в среднем приходится от 67 до 80% всех затрат по разработке и поддержке программных систем. В том же исследовании была предложена следующая классификация работ по сопровождению: •
исправительное
сопровождение
(corrective
maintenance),
связанное
с
исправлением выявленных ошибок; •
адаптивное сопровождение (adaptive maintenance), связанное с изменениями внешних по отношению к системе условий (перенос на новую операционную систему, замена аппаратной платформы и т. п.);
•
совершенствующее
сопровождение
(perfective
maintenance),
обычно
производимое по предложениям или требованиям пользователей; •
превентивное сопровождение (preventive maintenance), задача которого состоит в предотвращении будущих проблем и облегчении последующего сопровождения.
Впоследствии был выполнен целый ряд исследований [63, 79, 11], посвященных различным видам сопровождения и распределению затрат на них. Результаты этих работ хорошо согласуются с оценками, приведенными Линцом, Свонсоном и Томпкинсом в исходной статье: •
совершенствующее сопровождение – 50%;
•
адаптивное сопровождение – 25%;
27
•
исправительное сопровождение – 21%;
•
превентивное сопровождение – 4%.
Последняя цифра весьма примечательна: только 4% работ по сопровождению связаны с превентивным сопровождением и, следовательно, все остальные изменения связаны с ошибками, изменениями в окружающем мире, связанном программном обеспечении и т.п. Таким образом, можно утверждать, что в процессе сопровождения структура
системы
не
претерпевает
значительных
изменений,
а
инженеры
сопровождения стараются вносить в систему только минимальные изменения. Несмотря на это, многие системы со временем становятся неуправляемыми, так как исправление ошибок становится слишком дорогостоящим. Это даже дало современным исследователям основания утверждать, что, вопреки распространенному мнению, программы подвержены старению (software aging, code decay) – прежде всего, за счет изменений внешних условий [38, 72]. Кроме того, в большинстве сопровождаемых систем рано или поздно возникает так называемый лавинообразный эффект (ripple effect), когда любое исправление вносит больше ошибок, чем исправляет [102]. Необходимо отметить, что трудности сопровождения программных систем во многом носят объективный характер. В частности, одна из самых сложных проблем заключается в постоянном изменении требований к программному обеспечению, например, исследования, проведенные в компании Microsoft, показали, что еще в процессе разработки может измениться около 30% требований к системе [33]. Аналогичная оценка приведена в классической книге Барри Бема [14] – даже в хорошо управляемых проектах по разработке программного обеспечения требования меняются по ходу проекта в среднем на 25%. Период сопровождения системы обычно значительно дольше времени разработки и потому за время сопровождения система может изменяться еще сильнее. Кроме того, этап сопровождения системы весьма неоднороден – частота запросов на изменение может резко возрастать на короткие сроки, а затем снова падать до среднего значения [62]. Многие исследователи полагают, что упомянутые проблемы сопровождения программных систем демонстрируют внутренние проблемы, присущие традиционной водопадной модели жизненного цикла программ, и предлагают новые варианты, более точно описывающие развитие современных промышленных систем. Наибольшее распространение получила так называемая этапная модель, предложенная Райлихом и Беннеттом [76]. Согласно этой модели, любая программная система последовательно проходит несколько этапов (см. Рис. 3):
28
Начальная разработка Первая работающая версия Эволюция Потеря развиваемости Тех.обслуживание
Улучшения
Отмена обслуживания Окончание
Исправления
Снятие с эксплуатации Закрытие проекта
Замораживание продукта
Рис. 3. Общая схема этапной модели жизненного цикла программ
Основные характеристики этапов жизненного цикла программы согласно этапной модели таковы: •
Начальная разработка (initial development). Программисты разрабатывают первую работающую версию системы.
•
Эволюция
(evolution).
Программисты
расширяют
возможности
и
функциональность системы для удовлетворения запросов пользователей. Возможно, эти расширения влекут за собой кардинальные изменения в системе. •
Техническое обслуживание (servicing). Программисты выполняют мелкие исправления и простые изменения в функциональности.
•
Окончание
эксплуатации
(phaseout).
Компания
принимает
решение
не
производить дальнейшего технического обслуживания системы, продолжая получать прибыль от системы. •
Закрытие проекта (closedown). Компания снимает продукт с рынка и, возможно, предлагает пользователям перейти на новый продукт-замену (если такой продукт существует).
Данная
модель
достаточно
хорошо
отражает
жизненный
цикл
типичных
программных систем, разрабатываемых в современных промышленных компаниях. В основном, это достигается за счет того, что в данной модели дается точное описание основных стадий, которые проходит программа на этапе сопровождения, в то время как обычная водопадная модель подразумевает, что этап сопровождения равномерен и потому концентрируется на описании этапа разработки первой версии программы.
29
Такое понимание жизненного цикла программ сегодня представляется чрезмерным упрощением. Более того, современные исследователи скорее склонны считать именно сопровождение
наиболее
содержательным
этапом
–
вплоть
до
предложения
рассматривать начальную разработку как специальный случай сопровождения программ [96]. Существует также уточненный вариант этапной модели, называемый этапной моделью с версиями (versioned staged model). В этой модели предполагается, что новая версия продукта разрабатывается параллельно с эволюцией предыдущей версии, а после выпуска новой версии продукта предыдущая версия последовательно проходит стадии технического обслуживания, окончания эксплуатации и закрытия. Авторы модели считают наиболее важным этап эволюции, так как именно на этом этапе система может успешно сопровождаться – как только система переходит в стадию технического обслуживания, дальнейшее развитие системы становится практически невозможным. Более того, можно утверждать, что любой переход на следующий этап в данной модели крайне трудно обратить. Соответственно,
можно
указать
место
реинжиниринга
в
данной
модели:
потребность в реинжиниринге возникает в тех случаях, когда система уже находится в стадии технической эксплуатации (т.е. потеряла изменяемость), однако не потеряла ценность для бизнеса (т.е., требует развития).
1.3.2. Повторное использование программ Лучший способ справиться с трудностями разработки программных систем — это минимизировать нашу потребность в разработке новых программ. В самом деле, с экономической точки зрения современная разработка программного обеспечения устроена очень плохо, так как одни и те же функции, компоненты и прочие составляющие программ разрабатываются снова и снова, в том числе, одними и теми же программистами в рамках одной и той же организации. Одной из попыток решения этих проблем являются исследования в области повторного использования программ (software reuse). Очевидно, что реинжиниринг и повторное использование программ достаточно тесно связаны [4]. Можно утверждать, что повсеместное применение повторного использования могло бы значительно уменьшить потребность в реинжиниринге программ. Однако на практике повторное использование программного обеспечения вовсе не так успешно, как можно было бы предположить. Например, Каперс Джонс (Capers Jones) пишет в своей книге [52, с. 609]: "У наиболее опытных программистов есть свои личные библиотеки, позволяющие им при разработке новых программ повторно использовать до 30% исходных текстов. На корпоративном уровне процент повторно
30
используемых компонент может достигать 75% и требует специальных библиотек и администрирования. Повторное использование кода в корпоративных масштабах требует изменений в бухгалтерии проекта и методах измерения работы". В той же книге можно найти и еще одно свидетельство того, что повторное использование еще не стало массовой практикой – согласно Джонсу, в небольших программистских фирмах (т.е. имеющих 500 или менее разработчиков), всего 10% ведут формальные исследования повторной используемости. Особо подчеркнем – не применяют на практике, а только исследуют. В то же время, крупные компании считают, что повторное использование критично для их успеха – 100% компаний с 5000 и более разработчиков имеют внутренние программы повторного использования ПО. Брукс [105] отмечает, что повторное использование хорошо развито в отдельных хорошо формализованных областях (математика, ядерная физика, моделирование климата и т.п.), так как в соответствующих научных сообществах существует общепринятая система обозначений. Итак, на сегодняшний день повторное использование программного обеспечения является потенциально перспективным, но еще недостаточно успешным направлением исследований (если только не считать повторным использованием доступность для использования в других программах стандартных офисных приложений и систем управления баз данных). Очевидно, использования
основным является
препятствием сомнительность
для
массового
возврата
развития
инвестиций
в
повторного повторное
использование. Дело в том, что создание удачной повторно используемой компоненты стоит в 2-3 раза дороже, чем разработка модуля для данной конкретной задачи [105]. Такие значительные дополнительные вложения могут окупиться только в крупных компаниях: если компонента используется всего один или два раза, то не имеет никакого смысла вкладывать в нее дополнительные деньги; в то же время, в относительно небольшой компании вероятность повторного применения достаточно мала Брукс также указывает на трудности, связанные с обобщением компонент. Например, на сегодняшний день многие библиотеки насчитывают свыше 3000 элементов (в качестве примеров можно сослаться на MFC, OWL, JDK и т.п.). Для достижения большей общности объекты обрастают чрезвычайно перегруженными методами и интерфейсами – порядка 10-20 параметров и вспомогательных переменных. В таком случае использование компонент затрудняется тем, что любой программист, желающий воспользоваться подобной библиотекой, должен предварительно потратить много времени на изучение "словаря" системы. Конечно, такая задача совсем не безнадежна — средний человек использует около 10,000 слов, следовательно,
31
возможно использовать библиотеки подобного объема. Но необходимы специальные методики, помогающие применить к обучению таким библиотекам какие-то навыки, помогающие нам при изучении естественных языков, например, иностранных. Наконец, замедление
при
повторном
программ
–
использовании
например,
любая
может
возникнуть
программа,
существенное
использующая
MFC,
компилируется и запускается приблизительно в 1.5 раза медленнее, чем без использования данной библиотеки. Удобство и массовость повторного использования сильно зависит и от языка программирования. Согласно отчетам NASA, в их внутренних проектах существенное возрастание процента повторного использования программ (от 20% до 70%) наблюдалось после перехода с Фортрана на Аду [9]. Действительно, повторное использование
значительно
более
популярно
в
современных
объектно-
ориентированных языках. Возможность настройки классов в сочетании с применением наследования должны обеспечить удобный инструмент для повторного использования программистом собственных и сторонних программ. Наконец, необходимо постоянно отслеживать количество использований той или иной компоненты. Это критично, так как достичь действительного повторного использования компонент значительно труднее, чем сгенерировать множество компонент и записать их в общий репозиторий [2, с. 563]. Об этом мы еще будем говорить в главе 4, "извлечение классов из устаревшей системы". Несмотря на некоторую несогласованность терминологии, практически все исследования в данной области следуют одному и тому же устоявшемуся процессу извлечения повторно используемых компонент: •
поиск потенциальных кандидатов на повторное использование (prospecting)
•
оформление компоненты (transformation)
•
сертификация качества компоненты (certification)
•
повторное использование оформленных и сертифицированных компонент (reuse)
С точки зрения реинжиниринга программного обеспечения наибольший интерес представляют два первых этапа, так как они могут выполняться как в процессе начальной разработки программного обеспечения, так и во время реинжиниринга уже существующей системы [4, 21, 35]. Однако еще раз отметим, что весь процесс повторного
использования
программного
обеспечения
на
сегодняшний
день
недостаточно развит, что во многом определяет больший интерес к реинжинирингу.
32
ГЛАВА 2. Трудности, возникающие при языковых преобразованиях Как отмечалось во введении, миллиарды строк, написанных на Коболе, PL/I и прочих устаревших языках, все еще активно используются. В связи с этим многие компании ставят перед собой задачу переписать устаревшие программы на более современных языках. Однако в большинстве случаев это оказывается труднее, чем предполагалось изначально. В данной главе мы попробуем осветить трудности языковых преобразований и обсудить возможности и ограничения автоматизированных языковых конверторов. Стоимость программного обеспечения, в основном, зависит от менеджмента, кадрового состава и процесса разработки, а не от программных средств. К сожалению, и поставщики ПО, и академические исследования придают особое значение именно программным средствам. В результате, многие менеджеры становятся жертвами шарлатанских продуктов и сервисов по модификации программ. Этот механизм, известный в программном обеспечении как "синдром серебряной пули", получил у антропологов название волшебства слова: достаточно произнести имя вещи — "преобразование Кобола в Java", и вы уже владеете всей силой этого имени. Термин волшебство слова показывает, что для подтверждения ваших притязаний не требуются никакие
доказательства:
люди,
отчаянно
нуждающиеся
в
решениях,
и
так
безоговорочно поверят вашим утверждениям [34]. На самом деле, многие менеджеры рано или поздно обнаруживают, что они погребены под огромными объемами устаревших программ, нуждающихся в модификации. В то же время, обучение программированию ориентировано именно на новые разработки, а не на улучшение существующих, не говоря уже о сопровождении устаревших приложений. Эта проблема настолько серьезна, что Каперс Джонс (Capers Jones) упоминает ее как один из 60 главных рисков программного обеспечения [52]. Не нужно быть семи пядей во лбу, чтобы понять, что языковый конвертор мог бы решить все ваши проблемы с персоналом: преобразование языков ликвидирует разрыв между знаниями обычного программиста и знаниями, необходимыми для решения проблем устаревших систем. Но насколько это просто? В этой главе мы описываем реальное положение дел в области преобразования языков. Мы также приводим простые примеры, показывающие суть проблем.
33
2.1. О сложности языковых преобразований Одна компания утверждала следующее про свой конвертор из Кобола в Visual Basic: "Конвертор работает в режиме простого мастера (wizard): с ним может работать любая секретарша". Более того, на международной конференции по сопровождению программ в докладе, посвященном задачам нового века, утверждалось, что автоматизированные преобразования языков перестали быть проблемой, а трудности связаны только с преобразованиями, изменяющими парадигму программирования. Это означает не только перевод Кобола в С++, но и преобразование текстового процедурного кода в объектно-ориентированные программы на С++, работающие в Интернете. Из этих высказываний можно сделать вывод, что автоматизированные преобразования языков – это несложная задача. И действительно, перевод присваивания ADD 1.005 TO A из Кобола в эквивалентную форму в VB, A = A + 1.005, не вызывает проблем. Даже очень простой конвертор может справиться с этим. С другой стороны, преобразование языков почему-то является очень рискованным бизнесом. Нам известно несколько случаев, когда провалившиеся проекты по преобразованию языков приводили к банкротству компаний. Том Холмс (Tom Holmes) из компании Reasoning утверждает, что если критерием успеха считать получение прибыли, то большинство проектов по реинжинирингу неуспешны. Роберт Гласс (Robert Glass) в своей книге о катастрофах программирования упоминает о провале системы, которая должна была транслировать программы из старой системы в новое окружение. Менеджеры считали, что задача имеет очень ограниченный масштаб, и программистам необходимо перевести только небольшой набор конструкций исходной системы. Это предположение оказалось неверным. Анализ по окончании проекта показал, что конвертор должен быть в 10 раз сложнее, чем показывали исходные оценки. Из-за этого задача, казавшаяся технически возможной, внезапно стала экономически и технически невыполнимой [42]. C. Спронг (S.C. Sprong), работавший над переносом программ из Фортрана в С, высказал
в
дискуссионной
группе
alt.folklore.computers
следующее
мнение:
“Низкоуровневый перенос программ, даже при наличии документации, является одной из самых темных разновидностей программистского колдовства; знание о том, как это правильно делать, гарантирует вам прямое попадание в ад”. Итак, вопрос о трудности языковых преобразований вызывает большие разногласия. Возможно, что автоматическое изменение структуры системы с изменением парадигмы программирования — например, введение объектно-ориентированных концепций, — теоретически возможно. Однако этот процесс очень трудно автоматизировать, и в нем подразумевается активное участие человека [57]. В то же время, отдача от таких
34
вложений может значительно превышать затраты, особенно для систем с длительным временем жизни. В связи с проблемами автоматизации, большинство средств реинжиниринга используют технологию синтаксических преобразований. Но даже при этом, казалось бы, простом и низкоуровневом подходе, возникает множество трудностей, и масштаб этих проблем еще не окончательно осознан. Несмотря на небольшое количество содержательных публикаций на тему языковых преобразований (мы приводим ссылки на наиболее полезные публикации [44, 73, 102, 98, 25, 70, 80, 87], но, к сожалению, большинство из них трудно достать), рынок программных продуктов и услуг по миграции приложений переполнен. Многие компании утверждают, что они могут перевести ваши системы на любой язык программирования по вашему усмотрению. В большинстве случаев такие заявления оказываются необоснованными. Например, одна компания, рекламировавшая свои продукты в Интернете, приводила примеры переведенных программ, которые даже не компилировались! Другая компания заявляла, что может конвертировать приложения из PowerBuilder в Java, но в ответ на наши запросы сотрудники этой компании признались, что у них нет ни опыта подобных работ, ни соответствующих средств. Зато, по их утверждениям, им был понятен процесс выполнения данной работы! Следующая цитата из книги Гарри Снида (Harry Sneed), посвященной преобразованиям программ в объектно-ориентированную форму [87], резюмирует текущее состояние дел на рынке трансформации приложений: "В действительности все обстоит по-другому. Те, кто умеет читать между строк, знают, что проблемы сильно упрощены и рекламируемые продукты далеки от того, чтобы их можно было использовать на практике".
2.2. Требования к средствам преобразования языков Постановка задачи для языковых преобразований очень проста: необходимо перевести исходную систему на другой язык программирования, оставив неизменным внешнее поведение системы. С абстрактной точки зрения, проект по миграции приложения на новый язык кажется обманчиво простым. В связи с этим требования к языковым конверторам очень часто не формулируются явным образом. Обычно легкость формулирования решения проблемы перевода зависит от наличия в целевом языке соответствующих языковых конструкций. Мы будем называть такие элементы языка встроенными языковыми конструкциями. Например, если необходимо выбрать один из вариантов в зависимости от какого-то условия, то язык, поддерживающий условные предложения, будет для нас наиболее удобен. Если же нам придется использовать язык, в котором отсутствует конструкция if-then-else, то нам придется
ее
эмулировать.
Подобные
фрагменты
35
кода
мы
будем
называть
эмулированными языковыми конструкциями. Например, таким образом можно эмулировать объекты в языке, не имеющем поддержки объектной ориентации. Встроенные конструкции
Встроенные конструкции
Эмулированные конструкции
Эмулированные конструкции
Отсутствующие конструкции
Рис. 4. Отображение языковых конструкций при языковых преобразованиях
Проблема языковых преобразований сводится к отображению встроенных и эмулированных конструкций исходного языка в по возможности встроенные конструкции целевого языка (см. рис. 4). Существует, как минимум, шесть различных категорий подобного отображения, изображенных на рисунке стрелками. Мы встречали примеры всех шести типов подобного отображения. К сожалению, спецификации конверторов обычно упоминают только ту часть отображения, которая касается
перевода
встроенных
конструкций исходного
языка во встроенные
конструкции целевого. Этот феномен связан со стремлением людей концентрироваться вначале на самых легких частях проблемы. Очень часто в требованиях к языковым конверторам основная часть документа посвящена несущественным деталям. Так, например, в одном неуспешном проекте по созданию средства преобразования языков 80% текста спецификаций было посвящено графическому интерфейсу, а сам языковый конвертор был представлен одной-единственной стрелкой. Кто-то недооценил проблему и потому просто не включил все сложности в спецификации. Недооценка проблем очень часто ведет к неуправляемым проектам. Первые же трудности проекта ведут к задержке преобразования программ, что, в свою очередь, ведет к увеличению давления на команду разработчиков конвертора (возможно, уже новую). Из-за повышенного давления команда разработчиков вообще перестает уделять внимание требованиям. После нескольких повторов этого замкнутого круга, происходит полный развал проекта. В общих чертах данная схема описывает судьбу упомянутых выше обанкротившихся компаний. Для разработки конвертора, преобразующего один язык программирования в другой, необходим как минимум следующий набор спецификаций: •
Необходимо
перечислить
все
встроенные
и
эмулированные
конструкции системы, подлежащие преобразованиям.
36
языковые
•
Необходимо разработать стратегию перевода для каждой языковой конструкции. В частности, необходимо создать набор фрагментов на исходном и целевом языках, показывающий желаемое поведение конвертора.
•
Необходимо явным образом сформулировать, должна ли конвертированная система быть функционально эквивалентна исходной. На первый взгляд, кажется, что это всегда должно быть так, но на практике изощренные автоматизированные системы преобразования программ зачастую выявляют ошибки и опасные места в исходной системе. Чаще всего, заказчик впоследствии требует исправления и этих проблем тоже, так что возникает проблема незаметного разрастания требований. Отметим, что это также вредит тестированию новой системы, так как регрессионное тестирование основано на эквивалентности.
•
Необходимо понять, собираетесь ли вы конвертировать тестовые наборы исходной системы. Кроме того, необходимо сформулировать политику модификации в случае обнаружения ошибок в исходных тестах.
•
Необходимо поставить своей целью достижение максимальной автоматизации процесса перевода, тем самым, уменьшая потребность во вмешательстве пользователя.
•
Если планируется дальнейшее сопровождение конвертированной системы, то необходимо учитывать критерии сопровождаемости при переводе. Например, если сопровождать новую систему будет та же самая команда программистов, что сейчас сопровождает старую, то необходимо добиться максимальной схожести текстов целевой системы с текстами исходной. Таким образом, программистам будет легче находить "знакомые" фрагменты кода. С другой стороны, если планируется передать систему на сопровождение новой группе программистов, то значительно важнее, чтобы конвертированная система использовала стиль целевого языка, тогда программисты будут ориентироваться на знакомые языковые конструкции. Есть и другие варианты, когда, например, исходные тексты программ (скажем, написанные на Коболе) используются для модификации даже после успешной конвертации (скажем, в Java), так как процесс трансляции может быть настолько автоматизирован, что проще заново сгенерировать целевой код на Java, чем пытаться сделать в нем изменения. Это может также помочь при сопровождении в тех случаях, когда инженеры сопровождения хорошо знакомы с исходным языком, но незнакомы с целевым.
•
Необходимо добиваться приемлемой скорости компиляции и выполнения сгенерированных текстов.
37
•
В тех случаях, когда планируется многократное использование конвертора, время его работы также становится важным. Оптимизация времени работы конвертора не всегда тривиальна и иногда требует распределения вычислений по нескольким машинам.
•
Если конвертированная система будет сопровождаться, то ее объем не должен существенно превосходить объем исходного приложения.
Помимо этих явных требований, существуют еще и подразумеваемые ожидания заказчика, отражающие его представления о том, какие преимущества должны появиться в результате переноса системы в современное окружение. Очень часто именно эти воображаемые преимущества служат толчком для начала работ по трансформации приложения, хотя в большинстве случаев эти ожидания и не сбываются.
Например,
существует
распространенное
заблуждение,
что
после
преобразования в систему будет проще вносить изменения и потому появится возможность реализовать абсолютно новую функциональность. Проблема усугубляется тем, что большинство компаний, продающих промышленные средства реинжиниринга, не спешат разубеждать потенциальных клиентов в этих неоправданных ожиданиях. В то же время, качество предлагаемых средств реинжиниринга чаще всего оставляет желать лучшего, а порою и просто никуда не годится. Автоматически конвертированные программы обычно получаются существенно хуже, чем разработанные в рамках целевого языка от начала до конца. Понятно, что в идеале результат преобразований должен выглядеть так, как будто он был написан на целевом языке с использованием всех его средств и особенностей, реальные результаты конверсии зачастую сохраняют идеологию исходного языка. Многие ожидают, что структура
программы
может
только
улучшиться
после
автоматизированных
преобразований, но концептуальные изменения приложений практически всегда связаны с большим количеством ручной работы [44, 47]. Например, представьте себе замену программ, работающих на мэйнфрейме и использующих CICS, на C++ с использованием Microsoft Transaction Server.
2.3. Технические проблемы При преобразовании программ с одного языка на другой полезно составить набор входных и выходных фрагментов кода (шаблонов) — собственно говоря, это и есть сложная часть языковых преобразований. Ниже приводятся примеры типичных проблем, с которыми могут столкнуться авторы средств языковых преобразований.
38
2.3.1. Преобразование типов данных Одна из первых проблем — это преобразование типов данных. Хоть мы и не всегда осознаем это, практически все языки программирования имеют свой уникальный набор типов данных. Даже у настолько похожих языков как С++ и Java легко найти различия. Например, в С++ есть указатели, а в Java они отсутствуют; в С++ размеры типов данных варьируются от платформы к платформе, а в Java они зафиксированы и т.д. Так что при преобразовании из С++ в Java мы уже сталкиваемся с проблемой несовместимости данных. Когда же речь заходит о различиях между такими языками, как Кобол или PL/I, и современными языками, такими как Java, VB или C++, то найти эквиваленты становится попросту невозможно. Действительно, рассмотрим следующее описание переменной на языке PL/I: DECLARE F FIXED DECIMAL (4, -1);
Переменная F занимает три байта, причем подразумевается, что десятичная точка находится на одну позицию правее, чем само число. Таким образом, F может содержать значения 123450 и 123460, но не 123456. F может принимать значения от -99999*10 до 99999*10, и у всех присваиваемых F значений будет урезана последняя цифра, так что присваивание F = 123456 будет эквивалентно F = 123450. Так как последняя цифра всегда равна нулю, она вообще нигде не хранится. Очевидно, что ни приведенный тип данных, ни соответствующий ему оператор присваивания не соответствуют никакому стандартному типу данных С++ и стандартному оператору присваивания (отметим, что в статье [57] Костаса Контогианниса (Kostas Kontogiannis) и его коллег данный вопрос не был затронут, так как в отличие от общепринятого стандарта на язык PL/I, типы данных PL/IX в основном совпадают с типами данных языка C).
2.3.2. Кобол в Visual Basic Во время преобразования Кобола в VB одним из возможных вариантов является перевод только тех типов данных, для которых удается найти эквивалентные в целевом языке. Согласно схеме на рис. 4 это соответствует переводу встроенных конструкций во встроенные. В этом случае конвертор будет игнорировать переменные всех остальных типов данных и предлагать пользователю переписать те части кода, в которых эти переменные используются. Следующий простой фрагмент на Коболе иллюстрирует проблемы, связанные с таким подходом к преобразованию данных:
39
DATA DIVISION. 01 A PIC S9V9999 VALUE -1. PROCEDURE DIVISION. ADD 1.005 TO A. DISPLAY A.
Переменная А может содержать такие значения, как –3,1415; в данном примере она инициализируется значением –1. В процедурной части кода, мы прибавляем к А число 1.005 и выводим результат на экран. Конвертор может перевести эту простую программу на Коболе в следующую программу на VB: Dim A As Double A = -1 A = A + 1.005 MsgBox A
В этой программе на VB переменная A объявлена с типом Double. Затем переменная A инициализируется и получает значение –1. Команда ADD из Кобола представлена в VB оператором +. Однако запуск программы на VB дает другой результат (+0.0049, свидетельствующий об ошибке округления), чем программа на Коболе. Эта небольшая разница возникает из-за того, что программа на Коболе использует тип данных с фиксированной точкой, а фрагмент на VB использует тип данных с плавающей точкой. Неточность вычислений переменных с плавающей точкой около нуля не является ошибкой и документирована в справочниках по Visual Basic. В приведенном выше примере мы можем решить проблему округления путем использования встроенного типа данных Currency. Но рассмотрим несколько измененный пример: DATA DIVISION. 01 A PIC S9V99999 VALUE -1. PROCEDURE DIVISION. ADD 1.00005 TO A. DISPLAY A.
Преобразуем этот пример с использованием типа данных Currency: Dim A As Currency A = -1 A = A + 1.00005 MsgBox A
40
Программа на Коболе проводит вычисления с большей точностью, чем в предыдущем примере, и выдает ожидаемый результат. В то же время, программа на VB печатает 0.0001, что в два раза больше, чем во втором примере на Коболе. Данная проблема возникает из-за того, что тип Currency использует четыре знака после десятичной точки и округляет результаты для меньших значений. То есть, Currency тоже плохо ведет себя в других контекстах. Таким образом, ни один тип данных VB не соответствует структурам Кобола с фиксированной точкой. Поэтому любая простая стратегия перевода этих типов обречена на провал. Итак, нетривиальный анализ типов данных необходим даже в таких простых случаях, как приведенные выше примеры.
2.3.3. Кобол в Java Любая языковая конструкция, которая может быть неправильно употреблена, будет неправильно
употреблена.
Так
называемые
"умные"
использования
языковых
конструкций могут привести к неожиданным различиям во внешнем поведении исходной и конвертированной программ. Существуют проблемы с переполнением, приведением типов и прочими сложными манипуляциями с типами данных. Рассмотрим следующую программу на Коболе: DATA DIVISION. 01 A PIC 99. PROCEDURE DIVISION. MOVE 100 TO A. DISPLAY A.
В этом фрагменте объявлена переменная А, которая может содержать двузначные числа. В процедурной части этой переменной присваивается трехзначная константа, и затем мы печатаем результат. Конвертор мог бы преобразовать эту программу в следующий код на Java: public class Test { public static void main(String arg[]) { short A = 100; System.out.println (A); } }
41
Мы объявляем целочисленную переменную типа short, присваиваем ей значение 100 и печатаем результат. Однако результаты работы этих программ абсолютно разные: программа на Коболе печатает 00, а программа на Java печатает 100. Решение проблем, связанных с неожиданными побочными эффектами типов данных, – это одна из трудностей языковых преобразований. Для борьбы с этой проблемой необходим другой подход, который мы называем эмуляция типов данных. Для каждого типа данных исходного языка, для которого не существует точного эквивалента в целевом языке, мы создаем специальную поддержку, эмулирующую поведение переменных данного типа. Можно сказать, что мы добавляем в целевой язык некоторые конструкции, так чтобы стрелка из "встроенных конструкций" на рис. 4 стала
указывать
в
"эмулированные
конструкции"
вместо
"отсутствующих
конструкций". Например, следующие описания переменных на Коболе 01 A PIC 9V9999. 01 B PIC X(15).
не
имеют
удовлетворительного
эквивалента,
скажем,
в
Java.
Поэтому
преобразованные элементы должны иметь следующий синтаксис: Picture A = new Picture ("9V9999"); Picture B = new Picture ("X(15)");
В этом фрагменте кода класс Picture эмулирует все подробности поведения исходного типа данных, включая обработку присваиваний, преобразование в другие типы данных и обработку переполнения. Конечно же, теперь конвертированная программа стала очень похожей на Кобол, хотя это все еще программа на Java. Можно называть подобные программы Java-совместимыми программами на Коболе. Как только мы начинаем эмулировать типы данных, возникает вопрос о композиционности. А именно, если мы используем эмулированные типы данных во встроенной арифметической конструкции конвертированной программы, то является ли результат корректным? Если нет, то необходимо создать специальную функцию типа Add(Pic, Pic)
или
создать
специальные
методы,
корректно
реализующие
арифметические операции для эмулированных типов данных, например, a.Add(b). В С++ мы можем перегрузить операторы +, –, * и =. Однако в этом случае может возникнуть ошибка, если программист, дописывающий полученную программу на С++, воспользуется неправильным эмулированным и перегруженным оператором + вместо обычного оператора.
42
2.3.4. OS/VS Cobol to VS Cobol II Как было показано выше, преобразование данных из одного языка в другой является достаточно сложной задачей. Преобразование процедурной части программ также не очень просто. Поэтому мы попробуем немного умерить наши ожидания и изучим преобразования диалектов. В качестве примера рассмотрим преобразование одного из диалекта стандарта Кобол 74 под названием OS/VS Cobol в современный диалект Кобола 85, VS Cobol II. Оба диалекта поддерживаются IBM и работают на мэйнфреймах IBM. Многие считают, что преобразования такого рода – это не проблема. Но рассмотрим следующий фрагмент, печатающий слово IEEE: PIC A X(5) RIGHT JUSTIFIED VALUE 'IEEE'. DISPLAY A.
Данный синтаксис легален в обоих диалектах Кобола, поэтому кажется, что нет никакой нужды в преобразованиях. Однако проблема заключается в том, что в данном случае одинаковый синтаксис имеет разное поведение. Например, компилятор OS/VS Cobol напечатает ожидаемый результат, т.е. ' IEEE' с выравниванием по правому краю. Но компилятор Cobol/370 напечатает 'IEEE ' с выравниванием по левому краю (для тех, кого это удивляет, у нас есть два слова: стандарты ANSI). Это не единичный случай [25, 101, 19]. Рекс Видмер (Rex Widmer) в своей книге Cobol Migration Planning [101] называет эту проблему "одинаковый синтаксис, разное поведение". Мы называем это проблемой омонимов (homonym problem).
IDENTIFICATION DIVISION.
IDENTIFICATION DIVISION.
PROGRAM-ID. TEST-1.
PROGRAM-ID. TEST-2.
DATA DIVISION.
DATA DIVISION.
WORKING-STORAGE SECTION.
WORKING-STORAGE SECTION. 01 TMP PIC X(6).
01 TMP PIC X(8).
01 H-DATE.
01 H-DATE. 02 H-MM
02 FILLER PIC 02 H-DD
PIC XX.
02 FILLER PIC
X.
02 H-DD
PIC XX.
02 FILLER PIC 02 H-YY
02 H-MM
PIC XX.
PIC XX.
02 FILLER PIC
X.
02 H-YY
PIC XX.
X. X.
PIC XX.
PROCEDURE DIVSION.
PROCEDURE DIVSION.
PAR-1.
PAR-1.
MOVE CURRENT-DATE TO TMP
ACCEPT TMP FROM DATE
MOVE TMP TO H-DATE
MOVE TMP TO H-DATE
43
DISPLAY 'DAY
= ' H-DD.
DISPLAY 'DAY
= ' H-DD.
DISPLAY 'MONTH = ' H-MM.
DISPLAY 'MONTH = ' H-MM.
DISPLAY 'YEAR
DISPLAY 'YEAR
= ' H-YY.
= ' H-YY.
Рис. 5. Две похожие программы на Коболе: (а) OS/VS Cobol; (б) VS Cobol II
Другой пример данной проблемы, приведенный на рис. 5а, не так просто обнаружить. По существу, программа на OS/VS Cobol'е определяет переменную TMP, рассчитанную на хранение восьми позиций, и затем переменную H-DATE для работы с датами, такими как 13/01/99. В процедурной части, значение специального регистра CURRENT-DATE присваивается переменной TMP. Программа записывает это значение в специальную переменную и затем печатает день, месяц и год. Для преобразования этой программы в новый диалект Кобола, необходимо заменить использование специального регистра CURRENT-DATE на системный вызов DATE. Функция DATE возвращает значение YYMMDD, не совпадающее с типом DD/MM/YY, используемым
CURRENT-DATE.
Cobol
Migration
Guide
[30],
написанный
специалистами IBM, предлагает изменить тип переменной TMP из PIC X(8) в X(6) и преобразовать MOVE CURRENT-DATE в оператор ACCEPT. Это приводит нас к программе на VS Cobol II, приведенной на рис. 2б. Однако решение, предложенное IBM, не работает в данном контексте, так как при выполнении оператора MOVE неявно предполагается, что тип переменной TMP совпадает с типом переменной H-DATE. Это предположение уже не выполняется, и потому
результат
работы
программы
абсолютно
ошибочен.
Выполнение
конвертированной программы 15 сентября 1999 года дало следующие результаты: DAY
= 91
MONTH = 99 YEAR
=
Программа присвоила строку 990915 структурной переменной H-DATE: в поле HMM попали первые две цифры — 99, а в FILLER попал следующий за этим 0. Поле HDD получило следующие две цифры, 91, а еще один FILLER пожирает последнюю пятерку. Поэтому H-YY не получает ничего, а программа печатает результат, приведенный выше. Как можно исправить данную ошибку? Одним из вариантов решения является преобразование типа переменной H-DATE и всего кода, использующего эту переменную. Эти изменения могут затронуть всю программу, а возможно, и какие-то другие программы, так как эта переменная может быть использована в базе данных или
44
передана в качестве параметра в другую программу. Таким образом, преобразование процедурного кода тесно связано с преобразованием типов данных, используемых в процедурном коде. Если мы воспользуемся решением, предложенным IBM, то нам придется модифицировать всю систему в целом. Но мы можем избежать тотальных изменений путем использования эмуляции типов данных, как в следующем примере: IDENTIFICATION DIVISION. PROGRAM-ID. TEST-3. DATA DIVISION. WORKING-STORAGE SECTION. 01 F-DATE. 02 F-YY PIC XX. 02 F-MM PIC XX. 02 F-DD PIC XX. 01 TMP PIC X(8). 01 H-DATE. 02 H-MM
PIC XX.
02 FILLER PIC 02 H-DD
PIC XX.
02 FILLER PIC 02 H-YY
X. X.
PIC XX.
PROCEDURE DIVISION. PAR-1. ACCEPT F-DATE FROM DATE STRING F-MM '/' F-DD '/' F-YY DELIMITED SIZE INTO TMP END-STRING. MOVE TMP TO H-DATE DISPLAY 'DAY
= ' H-DD.
DISPLAY 'MONTH = ' H-MM. DISPLAY 'YEAR
= ' H-YY.
Вначале мы определяем новую переменную F-DATE с тем же типом, что и новый системный вызов DATE в процедурной части кода. Мы сохраняем результат системного вызова в этой переменной, а затем эмулируем старый специальный регистр путем записи в него значения в соответствующем формате. После этого весь код, использующий старый формат даты, работает как если бы он по-прежнему использовал специальный регистр. Глобальное изменение программы предотвращено, а решение может быть полностью автоматизировано (хотя результат получается и не таким красивым, как хотелось бы). IBM исправило эту ошибку в более новых версиях руководства по конверсии диалектов.
45
2.3.5. Turbo Pascal to Java Следующий пример основан на программе, написанной на языке HPS*Rules, о котором еще будет идти речь в следующей главе. Программу
необходимо было
перевести на Java. В целях данной главы исходный текст этого примера был переписан на Turbo Pascal'е, чтобы этот код стал понятней для читателей. Program StringTest; var s: string; a: integer; begin s := 'abc'; a := pos ('d', s); writeln (a); s [pos ('a', s)] := 'd'; writeln (s); end;
В данном примере используются две переменные. Вначале, мы присваиваем строковой переменной значение 'abc'. Затем мы присваиваем переменной a значение, равное позиции первого появления буквы 'd' в строке s. Поскольку эта буква ни разу не встречается, переменная a получает значение 0. Мы печатаем этот результат на экран. После этого, мы ищем первое появление буквы 'a' в строке 'abc', заменяем эту букву на 'd' и снова печатаем результат. Итак, программа печатает 0 и 'dbc'. Следующая программа на Java могла бы стать результатом работы "наивного" автоматического конвертора: public class StringTest { public static void main (String args[]) { StringBuffer s = new StringBuffer ("abc"); int a; a = s.toString().indexOf('d'); System.out.println (a); s.setCharAt (s.toString().indexOf('a'), 'd'); System.out.println (s); } }
Мы объявляем переменную s со значением 'abc' и присваиваем переменной a значение, равное позиции первого появления буквы 'd' в строке s. Однако согласно соглашениям, принятым в Java, в тех случаях, когда подстрока не найдена,
46
возвращается значение -1, поэтому программа печатает -1. Итак, семантика первой части программы при переводе на Java стала некорректной. К счастью, вторая часть переведена правильно. Поэтому мы учитываем изменение возвращаемого значения и переписываем конечную программу следующим образом: public class StringTest { public static void main (String args[]) { StringBuffer s = new StringBuffer ("abc"); int a; a = s.toString().indexOf('d') + 1; System.out.println (a); s.setCharAt (s.toString().indexOf('a') + 1, 'd'); System.out.println (s); } }
Для решения проблемы с кодом возврата мы прибавляем единицу, эмулируя таким образом поведение программы на Turbo Pascal. Теперь переменная a получает правильное значение 0. Однако это ведет к тому, что вторая часть программы становится некорректной: вместо 'dbc' печатается 'adc'. Ясно, что программа заменила вторую букву в строке 'abc' на букву 'd'. Этот эффект возник из-за другого побочного эффекта преобразования: массивы в Java начинаются с 0, а не с 1, как в Turbo Pascal. Если мы учтем и этот факт, то мы получим следующий код: public class StringTest { public static void main (String args[]) { StringBuffer s = new StringBuffer ("abc"); int a; a = s.toString().indexOf('d') + 1; System.out.println (a); s.setCharAt ((s.toString().indexOf('a') + 1) - 1, 'd'); System.out.println (s); } }
Мы
добавили
единицу
к
результату
работы
функции
indexOf,
чтобы
скомпенсировать разницу в значениях кода возврата. При использовании массивов в Java, мы вычитаем 1 из индекса массива, чтобы скорректировать разницу с Turbo
47
Pascal'ем. Во время этапа реструктуризации конвертированного кода, мы можем провести константные вычисления и преобразовать код s.setCharAt ((s.toString().indexOf('a') + 1) - 1, 'd');
путем переписывания его в s.setCharAt (s.toString().indexOf('a'), 'd');
Теперь становится понятным, почему первая попытка преобразования давала правильный результат во второй части программы: это произошло из-за наложения двух ошибок, уничтоживших друг друга и выдавших правильный ответ по чистой случайности. Или, как Скотт Адамс (Scott Adams) сформулировал это в своей книге "Принцип Дильберта": "Две ошибки дают правильный ответ – почти что" (Two wrongs make a right, almost).
2.3.6. Перевод языково-специфичных конструкций Сущность реинжиниринга заключается в точном переводе всех самых мелких деталей, причем список потенциальных проблем практически бесконечен. Особенные трудности вызывает перевод операторов, работающих с внутренним представлением данных, и вообще операторов, работающих с памятью. В таких случаях приходится писать специальные процедуры поддержки времени исполнения. Рассмотрим следующий оператор на Коболе (пример заимствован из [97]): STRING ID-1 ID-2 DELIMITED BY "*" ID-4 ID-5 DELIMITED BY SIZE INTO ID-7 WITH POINTER ID-8 ON OVERFLOW GO TO OFLOW-EXIT.
Этот оператор собирает воедино некоторые части или все содержимое четырех различных переменных в одну новую переменную (ID-7), а также предоставляет указатель на последнюю букву в поле-получателе. Кроме того, если при записи в ID-7 происходит переполнение поля, то происходит переход на параграф OFLOW-EXIT. Данный оператор работает со внутренним представлением переменных и потому должен быть эмулирован при переводе на новый язык программирования, не поддерживающий подобные операции (т.е. при переводе практически на любой другой язык). При этом для сохранения семантической корректности программы новые типы данных должны иметь то же внутреннее представление, что и в исходном языке.
48
Таким образом, описанная проблема очень тесно соприкасается с проблемой преобразования типов данных и решение этой проблемы зависит от решения предыдущей. Если принимается решение об эмуляции типов данных, то необходимо эмулировать и операторы подобного рода. Если же принимается решение переводить типы данных в родные типы целевого языка, то, скорее всего, операции подобного рода поддержать просто не удастся. Естественно, STRING – не единственная операция, работающая с памятью. В Коболе предусмотрены также специальные средства для поиска в переменных, подсчета количества шаблонов и замены одних шаблонов на другие. Например, оператор INSPECT дает возможность подсчитать количество использований буквы или некоторого шаблона в данной строке, или заменить такие шаблоны на что-то иное, или даже проделать и то, и другое одновременно. Автоматическое преобразование в языки, не имеющие встроенной поддержки таких конструкций, должно учитывать подобные ситуации и расширять целевые языки функциональностью, реализующей синтаксис и семантику этих конструкций. Это приводит нас к более общему вопросу области применения (application domain), на которую ориентирован язык программирования. Из приведенных выше примеров становится ясно, что Кобол содержит богатый набор весьма нетривиальных конструкций, предназначенных для обработки данных. Любая попытка перевести подобные типичные конструкции Кобола в язык с другой областью применения заранее обречена на провал, так как в целевом языке попросту не найдется соответствующих операторов. Точно также и Кобол практически безнадежен как язык системного программирования, и потому было бы трудно, а то и вовсе невозможно перевести хорошую системную программу (на любом языке) в Кобол. Таким образом, попытка преобразования между языками, ориентированными на разные области применения, заранее обречена на провал. Но даже обратное утверждение (преобразование между схожими языками легко) тоже
не
всегда
верно.
Проблема
омонимов
является
контрпримером,
демонстрирующим, что преобразования языков всегда сложны.
2.3.7. Проблемы поддержки сгенерированного текста Перевод приложений с одного языка на другой чаще всего служит лишь средством для упрощения дальнейшего сопровождения, поэтому тексты на целевом языке должны удовлетворять всем обычным требованиям к исходным текстам – структурированность, минимальный объем глобальных данных и т.д. А так как программы на устаревших языках, особенно на Коболе, не удовлетворяют этим требованиям, то при трансформации возникает насущная потребность в реструктуризации программ
49
(например, разбиение программ на процедуры, полное или частичное уничтожение операторов GOTO, локализация данных и т.д.). Однако тексты, полученные в результате перевода с одного языка на другой, должны
удовлетворять
и
еще
одному
требованию,
а
именно,
схожести
сгенерированной программы с исходной. Дело в том, что очень часто исходная программа используется как "справочник" по функциональности для новой, и при необходимости уточнения смысла некоторых операций или для внесения изменений в первую очередь смотрят именно в старую программу. Поэтому в целях повышения сопровождаемости новой системы нельзя целиком разрушать привязку к исходной программе. Понятно, что эти требования напрямую противоречат друг другу, так как структуризация повышает сопровождаемость, но ведет к уменьшению узнаваемости текстов на целевом языке (аналогичные по сути проблемы сформулированы в работе [22]). При переводе программ с Кобола в современные языки особенно критичными являются процессы выделения процедур из параграфов, преобразования GOTO в структурированные операторы (например, это является жизненно необходимым при переводе программ в Java, в котором вообще нет операторов goto) и локализация данных, при которой глобальные данные Кобола преобразуются с помощью анализа потоков данных в локальные переменные процедур и передачу параметров. Существуют и менее масштабные проблемы, связанные с различиями между исходным и целевым языками, ведущие к трудностям в сопровождении. Приведем следующий пример – во многих устаревших языках разрешена неполная квалификация выборки из структуры при условии ее непротиворечивости. Поэтому следующий оператор: MOVE SQL-RETURN-CODE TO CUSTOMERID(8)
может иметь следующий весьма длинный эквивалент: Accounts.PhysicalCustomer(8).CustomerId = SQLCA.SqlReturnCode;
При конвертации промышленных приложений встречаются и более длинные строки, сами по себе представляющие порой предмет для занимательного чтения. Естественно, здесь мы их цитировать не будем. Чтобы решить данную проблему, при переводе в С++ иногда прибегают к следующему приему [110]. Создаются специальные макроопределения:
50
#define CustomerId Accounts.PhysicalCustomer(8).CustomerId #define SqlReturnCode SQLCA.SqlReturnCode
что позволяет в дальнейшем записывать выражения как в исходном тексте: CustomerId = SqlReturnCode;
Однако несмотря на более короткую форму записи, последний оператор, по сути, противоречит идеологии C++, так как в современных языках полная квалификация переменных обязательна, и в первую очередь это предназначено для повышения сопровождаемости. Так что найти удачный баланс между этими противоречивыми требованиями и точно определить, какие из требований важнее с точки зрения сопровождаемости целевого текста, совсем непросто.
2.4. Обсуждение Приведенные выше примеры показывают, что сложность преобразования языков существенно недооценивается, в том числе и хорошо известными специалистами по реинжинирингу [24]. Даже в тех случаях, когда мы ограничиваемся преобразованиями между двумя различными диалектами, поддерживаемыми одной и той же компанией (IBM), мы все равно сталкиваемся с проблемами. Похоже, сама компания IBM недооценила сложность преобразования собственных диалектов. Примеры
со
всей
очевидностью
демонстрируют,
что
автоматизированные
преобразования языков значительно труднее, чем принято полагать. Возможной причиной недооценки сложности проблемы является тот факт, что синтаксис преобразованной арифметики внешне выглядит обманчиво похожим на оригинал. В примерах вроде ADD 1.005 TO A и A = A + 1.005, нас подводит наше собственное восприятие арифметических действий. Более того, функции вывода на экран в Коболе (DISPLAY), VB (MsgBox) и С (printf) тоже относительно похожи, но их семантика различается с нашими интуитивными ожиданиями. Наконец, когда мы ограничиваемся преобразованием диалектов, проблема становится даже сложнее: несмотря на то, что программы могут успешно компилироваться различными компиляторами, семантика одного и того же синтаксиса может меняться от компилятора к компилятору. Как следствие, семантика конвертированного кода обычно отличается от исходной, если только мы не предпримем специальных усилий.
51
Примеры также иллюстрируют опасности, поджидающие нас при отображении типов данных одного языка на приблизительно похожие типы данных другого языка (см. рис. 4, стрелка из "Встроенных конструкций" в "Отсутствующие конструкции"). Использование встроенных или эмулированных типов данных зависит от требований к результатам преобразований. Встроенные типы данных не всегда гарантируют корректность кода, но и эмулированные типы данных связаны с большим количеством проблем. Применение встроенных типов данных целевого языка может упростить сопровождение (в связи с уменьшением объема чужеродного кода), но с другой стороны, оно уменьшает уровень автоматизации или влияет на семантическую корректность результата. Использование эмулированных типов данных ведет к большей автоматизации и более корректным программам, но с другой стороны, требует дополнительных
работ
по
написанию
библиотек
динамической
поддержки,
увеличивает затраты на сопровождение и уменьшает производительность целевой системы. В принципе, обе методики могут быть использованы одновременно. Например, можно вначале проанализировать, является ли исходная программа безопасной с точки зрения типов (т.е. убедиться, что в ней отсутствуют проблемы, подобные перечисленным выше). Если это требование выполняется, то можно воспользоваться встроенными типами целевого языка, а в проблематичных случаях можно применить эмуляцию типов данных. Далее, проблема омонимов вскрывает распространенное заблуждение о том, что синтаксическая схожесть различных языков или диалектов является полезным признаком сложности проектов по переводу, а именно: чем более похожи языки, тем проще преобразование между ними. На самом деле, данная мера сложности языковых преобразований только запутывает, так как чем ближе языки программирования, тем сложнее обнаружить различия между ними. Помимо всех обычных проблем с языковыми преобразованиями, нам дополнительно приходится иметь дело с семантическими различиями, которые мы даже не в состоянии обнаружить синтаксически! Любой менеджер проекта, подумывающий о преобразованиях языков или диалектов для решения своих проблем, должен понимать, что вместо проблем, которые хотелось бы решить с помощью преобразований языков, появятся другие, возможно, менее очевидные проблемы. При этом, чем больше объем кода, подлежащего переводу, тем больше потребность в автоматизации, что, в свою очередь, ведет к увеличению объема чужеродного кода. Очевидно, что между этими целями наблюдается некоторое противоречие. компромисса
В
промышленных
между
проектах
автоматизацией
необходимо
процесса
52
добиться
реинжиниринга,
некоторого
без
которой
невозможно решение хоть сколько-нибудь крупных задач, и качеством порождаемого кода, без достижения которого любой проект по реинжинирингу становится бессмысленным. Более подробно достижение подобного компромисса описывается в следующей главе.
2.5. Процесс преобразования языков Преобразование приложений из одного языка программирования в другой обычно предпринимается
для
упрощения
сопровождения.
Следовательно,
тексты
сгенерированных программ должны быть хорошо структурированы, содержать минимальное количество глобальных данных и т.п. К сожалению, очень немногие исходные программы удовлетворяют подобным требованиям, и потому любое осмысленное
преобразование
языков
должно
начинаться
со
всесторонней
реструктуризации, — несмотря на все проблемы, возникающие при использовании классических средств реструктуризации [22]. Некоторые шаги при преобразовании языков следует считать обязательными. На рис. 6 изображена простейшая схема процесса преобразования языков: Реструктуризация
Оригинальная программа
Реструктуризация
Замена синтаксиса
Целевая программа
Рис. 6. Простейший процесс языковых преобразований
Во-первых, мы проводим реструктуризацию исходного приложения с целью уменьшения количества проблематичных преобразований, изображенных на рис. 4. Например, программы на Коболе не обязаны содержать main, в отличие от программ на С. Поэтому при конвертации из Кобола в С, в процессе реструктуризации исходных программ необходимо создать искусственный параграф main. Процедуры не используются в Коболе, но широко распространены в других языках. Извлечь процедуры из исходных текстов достаточно трудно, особенно в случае слабо структурированных программ. Например, во время преобразования программ из Кобола в Аду, описанного в [44], обилие операторов GO TO в исходном тексте (приблизительно один оператор на каждые 225 строк) затруднило идентификацию процедур. В
подавляющем
большинстве
преобразований,
реструктуризация
является
обязательным предварительным этапом для того, что мы называем заменой синтаксиса.
53
На
этом
шаге
мы
заменяем
синтаксис
специально
подготовленного
и
реструктуризированного исходного текста на целевой синтаксис. Это относительно несложный шаг. Преобразованные программы обычно не очень красиво выглядят, поэтому необходима еще одна реструктуризация, теперь уже в терминах целевого текста, чтобы максимально приблизить полученный код к целевому языку. В качестве иллюстрации процесса, мы возьмем небольшую программу из реального устаревшего приложения, использовавшегося в швейцарском банке, и преобразуем ее из Кобола в С. Мы адаптировали исходную программу и перенесли ее из исходной области применения на задачу о путешествиях, чтобы упростить отслеживание ее логики. В этот раз мы не будем останавливаться на проблемах, связанных с типами данных, и сосредоточимся на процедурном коде. Качество программы именно такое, какого следует ожидать от устаревшего приложения: одно GO TO на каждые четыре строчки. Исходный текст приведен на рис. 7а, а сильно реструктуризированный вариант на Коболе представлен на рис. 7б [80].
IDENTIFICATION DIVISION.
IDENTIFICATION DIVISION.
PROGRAM-ID. TRAVEL.
PROGRAM-ID. TRAVEL.
DATA DIVISION.
DATA DIVISION.
WORKING-STORAGE SECTION.
WORKING-STORAGE SECTION.
01 D PIC 9(6) VALUE 980912.
01 D PIC 9(6) VALUE 980912.
01 X PIC 9 VALUE 1.
01 X PIC 9 VALUE 1.
PROCEDURE DIVISION.
PROCEDURE DIVISION.
TRAVEL SECTION.
TRAVEL SECTION.
AMSTERDAM.
AMSTERDAM.
IF D = 980912
PERFORM TEST BEFORE UNTIL (D 980912)
GO ATLANTA. GO HOME. LOS-ANGELES. GO NEW-YORK. HONOLULU.
PERFORM PITTSBURGH PERFORM TORONTO END-PERFORM STOP RUN.
DISPLAY 'WCRE & ASE'
BAR SECTION.
ADD 14 TO D
BAR-PARAGRAPH.
GO LOS-ANGELES. DETROIT. DISPLAY 'NOBODY'. WATERLOO. DISPLAY 'UNIV. OF WATERLOO' ADD 6 TO D
STOP RUN. TRAVEL-SUBROUTINES SECTION. PITTSBURGH. DISPLAY 'S.E.I.' ADD 14 TO D. VICTORIA.
MOVE 0 TO X
DISPLAY 'UNIV. OF VICTORIA'
GO TORONTO.
ADD 4 TO D
ATLANTA.
MOVE 1 TO X.
54
GO PITTSBURGH. NEW-YORK. GO AMSTERDAM. VANCOUVER. IF X = 0 GO VICTORIA.
WATERLOO. DISPLAY 'UNIV. OF WATERLOO' ADD 6 TO D MOVE 0 TO X. TORONTO. PERFORM TEST BEFORE UNTIL X 1
GO HONOLULU. PITTSBURGH.
PERFORM WATERLOO
DISPLAY 'S.E.I.'
END-PERFORM
ADD 14 TO D
PERFORM TEST BEFORE UNTIL X 0
GO TORONTO. VICTORIA.
PERFORM VICTORIA
DISPLAY 'UNIV. OF VICTORIA'
END-PERFORM
ADD 4 TO D
DISPLAY 'WCRE & ASE'
MOVE 1 TO X
ADD 14 TO D.
GO VANCOUVER. TORONTO. IF X = 1 GO WATERLOO. GO VANCOUVER. HOME. STOP RUN. Рис. 7. Пример программы на Коболе: (a) исходный текст; (b) сильно реструктурированный текст
Обе программы печатают на выходе следующие строчки: S.E.I. Univ. of Waterloo Univ. of Victoria WCRE & ASE
Программа описывает поездку, начинающуюся в Амстердаме в определенный день. Согласно программе, мы отправимся через Атланту в Питтсбург, чтобы поработать в SEI. Затем через некоторое время мы путешествуем из Питтсбурга в университет Ватерлоо через Торонто. Далее, мы летим в университет Виктории через Торонто и Ванкувер, затем в Гонолулу через Ванкувер, чтобы посетить две конференции. Через некоторое время мы летим в Амстердам через Нью-Йорк. Обратите внимание на неиспользуемый код: мы не летим через Детройт. Такие призрачные пункты назначения иногда возникают в сложных поездках просто потому, что билет в обе стороны порою бывает дешевле билета в одну сторону. Неявный код, представленный в данной программе стыковочными рейсами, и неиспользуемый код ("призрачные
55
пункты назначения") весьма характерны для устаревших систем. Кроме того, очень характерно массовое использование операторов безусловного перехода, ухудшающих качество исходной программы. Легко убедиться, что реструктурированная программа имеет ту же семантику, что и исходная. Однако неиспользуемый и неявный код, а также операторы GO TO исчезли, появилась секция "кандидатов в процедуры" и т.д. Таким образом, большой объем работ по приближению исходного Кобола к целевому языку С уже проделан. Теперь мы готовы к замене синтаксиса. Нетрудно видеть, что это не самая сложная часть преобразований. Результат таков: #include <stdio.h> long D = 980912 ; int X = 1 ; void PITTSBURGH ( ) { printf("S.E.I.\n"); D += 14; } void VICTORIA ( ) { printf("Univ. of Victoria\n"); D += 4; X = 1; } void WATERLOO ( ) { printf("Univ. of Waterloo\n"); D += 6; X = 0; } void TORONTO ( ) { while ( X == 1 ) { WATERLOO ( ) ; }; while ( X == 0 ) { VICTORIA ( ) ; }; printf("WCRE & ASE\n"); D += 14; } void main ( ) { while ( D == 980912 ) { PITTSBURGH ( ) ; TORONTO ( ) ; }; exit ( ) ; }
56
Полученный код нуждается в дальнейшей реструктуризации. Во-первых, исходный диалект Кобола не имел встроенной поддержки функций, которая присутствует в С. Поэтому мы можем свернуть похожие участки целевого кода в функции с параметрами. Во-вторых, необходимо сделать глобальные переменные локальными в тех или иных процедурах. Наконец, имеет смысл переписать неявный код, т.е. вызовы функций, состоящих только из вызова другой функции. Выполнение всех этих типичных шагов по реструктуризации приложений на С дает нам следующую программу: #include <stdio.h> void f(long dD, int newX,long *D,int *X, char *s) { printf(s); *D += dD; *X = newX; } void TORONTO (long *D, int *X ) { while ( *X == 1 ) { f(6,0,D,X,"Univ. of Waterloo\n"); }; while ( *X == 0 ) { f(4,1,D,X,"Univ. of Victoria\n"); }; f(14,*X,D,X,"WCRE & ASE\n"); } void main ( ) { long D = 980912; int X =1; while ( D == 980912 ) { f(14,X,&D,&X,"S.E.I.\n"); TORONTO (&D,&X ) ; }; exit ( ); }
Отметим, что практически все упомянутые выше действия в предлагаемом нами процессе реализованы в инструментальном средстве реинжиниринга RescueWare. В результате языковых преобразований мы получили некоторый код на С. Однако хотелось бы еще раз подчеркнуть, что это не означает, что выполнение языковых преобразований – легкая задача. Дело в том, что приведенный нами пример существенно не дотягивает до реальной программы: в нем нет ввода данных, вывод тривиален, типы данных практически не используются и потому потенциально опасные преобразования отсутствуют, внешнее поведение программы тривиально, программа
57
имеет небольшие размеры, процедуры печати очень просты и т.д. Мы всего лишь проиллюстрировали
процесс
преобразования
языков.
В
полномасштабных
промышленных проектах решение этих задач значительно сложнее (см. главу 3).
2.6. Заключение В данной главе мы сформулировали трудности, возникающие при использовании прямолинейного подхода к языковым преобразованиям (транслитерации): 1. преобразование типов данных; 2. перевод языково-специфичных конструкций; 3. несоответствие парадигм исходного и целевого языков; Показано, что эти проблемы возникают не только при преобразовании из одного языка в другой, но и в процессе преобразования между различными диалектами одного и того же языка. Кроме того, сформулирована специфичная для преобразования диалектов проблема, названная проблемой омонимов, которая заключается в том, что при преобразовании диалектов может возникнуть ситуация, в которой фрагмент программы является синтаксически корректным в обоих диалектах, но имеет различную семантику. В качестве решения первых двух из проблем, перечисленных выше, предлагается эмуляция типов данных и конструкций исходного языка в целевом языке. Подчеркнуты потенциальные недостатки такого подхода: потеря эффективности и проблемы поддержки сгенерированного текста. Предлагаемый подход к решению третьей проблемы будет изложен ниже, в главах 4 и 5. Результатом рассмотрения основных проблем реинжиниринга стало перечисление основных требований к инструментальным средствам языковых преобразований. Данный набор требований может быть использован для проведения сравнительного анализа различных средств языковых преобразований. Кроме того, нами было показано внутреннее
противоречие
между
требованием
достижения
максимальной
автоматизации процесса преобразования и качеством получаемого кода на целевых языках.
58
ГЛАВА 3. Описание конкретного проекта по преобразованию языков В процессе переноса устаревших программ на новые языки и платформы перед программистами стоит задача "повторения" исходной системы на другом целевом языке программирования. Одним из наиболее принципиальных вопросов является достижение максимальной эффективности процесса конвертации при сохранении качества переведенной системы. В данной главе поиск такого компромисса проиллюстрирован на примере реального промышленного проекта по реинжинирингу приложения, в процессе которого клиент/серверная система, написанная на одном языке, переводится в два разных целевых языка программирования. Кроме того, делаются некоторые выводы о том, какие особенности исходной системы влияют на структуру трансформированной системы. Важным экономическим соображением при реинжиниринге является уровень автоматизации, доступный при трансформации приложения. Наличие или отсутствие подобных средств может стать критичным при принятии решения о начале проекта по реинжинирингу. В то же время вопросы автоматизации реинжиниринга по-прежнему недостаточно освещены в литературе, несмотря на то, что в последние годы появился целый ряд статей, описывающих различные автоматизированные подходы к преобразованию устаревших систем (см. ссылки на литературу в разделе 2.1). Действительно, в большинстве этих работ авторы обычно предполагают, что чем больше процент автоматизации, тем лучше – и ограничиваются техническим описанием предлагаемого процесса реинжиниринга. Однако на практике процесс реинжиниринга значительно сложнее, так как обычно он ограничен бюджетом, временными ограничениями и требованиями заказчика. Кроме
того,
приходится
учитывать
и
уровень
подготовленности
инженеров
сопровождения, выполняющих эту работу. Поэтому в некоторых случаях имеет смысл бороться за увеличение уровня автоматизации процесса, а в других случаях выгоднее положиться на инженеров сопровождения, участвующих в процессе реинжиниринга. Наш опыт показывает, что уровень автоматизации зависит от множества деталей, как технических, так и экономических, и потому исследования в данной области не должны ограничиваться сугубо техническими вопросами. В частности, необходимо учитывать человеческий фактор (например, участие в проекте программистов заказчика).
59
В данной главе мы рассмотрим различные факторы, влияющие на уровень автоматизации реинжиниринга. Мы также покажем, как средства автоматизации повлияли на выполнение реального проекта по модернизации устаревшей системы, в котором более миллиона строк кода были преобразованы из малоизвестного языка HPS*Rules в Visual Basic и Кобол. Рассматриваемый проект представляет собой весьма типичный сценарий: поддержка системы, созданной в рамках специальной технологии HPS, стала со временем слишком дорогостоящей для заказчика – помимо стоимости сопровождения как таковой, необходимо было также ежегодно обновлять лицензию на технологию HPS. Кроме того, были и другие ассоциированные затраты, например, стоимость обучения инженеров сопровождения (естественно, рынок программистов, умеющих работать на HPS, исчезающе мал). Таким образом, заказчик хотел перевести систему на более современную платформу, которая позволила бы развивать систему и дальше, но с меньшими финансовыми вложениями. Команда программистов из ЛАНИТ-ТЕРКОМа, в числе которой был и автор, приняла участие в этом проекте по реинжинирингу. Автор лично участвовал как в разработке средств автоматизации трансформации, так и в самом преобразовании системы. Данная глава организована следующим образом. В разделе 1 приводится краткая сводка проекта в целом и приведены цифры, отражающие объем выполненных работ. В разделе 2 описываются основные особенности языка HPS*Rules. В разделе 3 приводятся цифры, показывающие процент автоматизации работы, достигнутый в различных частях проекта. Предметом раздела 4 является сам процесс преобразования, возникшие во время этого процесса проблемы и пути их преодоления. Пятый раздел посвящен обсуждению проекта (post-mortem) и некоторым выводам. Наконец, заключение обобщает результаты и предлагает направления для дальнейшего развития.
3.1. Краткое описание проекта В рамках описываемого проекта перед исполнителем была поставлена задача перевода программной системы с объемом исходных текстов в полтора миллиона строк на новые языки программирования и платформу. Проект состоял из трех различных подпроектов. Общий объем системы после преобразования составил около 1,066,000 строк кода на Коболе и Visual Basic'е. Исходная система была создана в технологии HPS (High Productivity System) на собственном (proprietary) языке технологии Rules. Система имела архитектуру клиент/сервер и уже в исходном варианте была распределена между персональными компьютерами и мэйнфреймами.
60
Заказчик поставил перед нами задачу перевода системы в два целевых языка программирования: серверную требовалось целиком перевести в Кобол, а клиентскую составляющую, содержащую диалог с пользователем, разделили на две подсистемы, одна из которых была переведена в COBOL/CICS/BMS, а другая — в VB (см. рис. 8). Программы на Rules для PC
Программы на Rules для мэйнфрейма
COBOL/CICS/BMS
VB
Сервер на языке Кобол
Сервер на языке Rules
Рис. 8. Общая схема преобразования приложений в рассматриваемом проекте
Общий объем исходной системы составил 2125 файлов на Rules. В этих файлах собственно программная логика занимает 762,000 строк, из которых около 300,000 являются пустыми или комментариями. Описания данных занимают 1,054,000 строк (описания данных избыточны и во многом дублируются, поэтому достаточно трудно посчитать "чистый" объем описаний). Система использует 86 таблиц DB/2 и 305 окон. Всего система имеет объем в 1,816,000 строк и создавалась в течение 5 лет. Следующая таблица содержит основные данные о законченных подпроектах (все числа в таблице означают количество строк кода; описания данных исходной системы в таблице не учитываются). Тип приложения
Исходная система
Целевая система
Серверная часть
362,000 на Rules
465,000 на Коболе
222,000 на Rules
360,000 на Коболе
10,000 в Panel-файлах
4,500 на BMS
Клиентская часть
178,000 на Rules
241,000 на VB
(преобразование в VB)
9,500 в Panel-файлах
(бизнес-логика и формы)
Клиентская часть (преобразование в Кобол)
Таблица 1. Объем работ, выполненных в рамках данного проекта
3.2. Особенности языка Rules Прежде чем перейти к описанию процесса конвертации системы и трудностей, возникших на этом пути, скажем несколько слов об исходном языке.
61
Язык HPS*Rules обладает ограниченным набором конструкций, поддерживающих построение диалога с пользователем и простые вычисления. Основная область применения языка HPS*Rules — это банковские приложения. В HPS*Rules существует три типа исходных файлов: rule-файлы, содержащие бизнес-логику, bind-файлы, содержащие описания данных, и panel-файлы, содержащие формализованные описания экранных форм, используемых в программах. Бизнес-логика в HPS*Rules организуется с помощью разбиения на независимые модули (rules, правила – от этого термина возникло и название всего языка). Каждое правило содержит четко определенный набор входных, выходных и локальных переменных (глобальных переменных в языке нет). Обычная последовательность действий программы на HPS*Rules заключается в чтении/записи данных во входные/выходные переменные, общении с пользователем и дальнейшим многоступенчатом анализе результатов (см. следующий пример): converse window TGN_CUST_SRCH_CRTR_DTL if WINDOW_RETCODE of TGN_CUST_SRCH_CRTR_DTL = 'ENTER' and L_INVLD_USER_F YES in TGS_CHAR_YES_NO_SYM map CUST_SRCH_CRTR_SD of TGN_CUST_SRCH_CRTR_DTL to
CUST_SRCH_CRTR_SD of TGN_CUST_SRCH_CRTR_DTL_VAL_I
use rule TGN_CUST_SRCH_CRTR_DTL_VAL ... endif while WINDOW_RETCODE of TGN_CUST_SRCH_CRTR_DTL 'MENU' caseof WINDOW_RETCODE of TGN_CUST_SRCH_CRTR_DTL case 'SKIP_ACTN' *> Re-display the window and allow the user to make a new action caseof WINDOW_RETCODE