САНКТ-ПЕТЕРБУРГСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ
На правах рукописи
Терехов Андрей Андреевич
ЯЗЫКОВЫЕ ПРЕОБРАЗОВАНИЯ В...
16 downloads
198 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 <* case 'RETURN' map SUCCESS in TGS_RTRN_C_SYM to RTRN_C of TGN_CUST_SRCH_CRTR_DTL_DIS_O return case 'REFRESH' clear CUST_SRCH_CRTR_SD of TGN_CUST_SRCH_CRTR_DTL map 'SSN_C' to FIELD_LONG_NAME of SET_CURSOR_FIELD_I map 'CUST_SRCH_CRTR_SD' to VIEW_LONG_NAME of SET_CURSOR_FIELD_I use component SET_CURSOR_FIELD case 'ENTER' ... endcase *> caseof WINDOW_RETCODE <*
62
Данный пример начинается с CONVERSE WINDOW – оператора, отображающего указанную форму на экране. Затем система ожидает возникновения какого-либо события на экране (или его предке/потомке); только после возникновения такого события программа возобновляет свое выполнение. Другой вариант CONVERSE WINDOW – это использование дополнительной опции NOWAIT. В этом случае программа продолжает работу сразу же по выводу формы на экран. События, возникающие в окне, обрабатываются следующим образом. В той версии HPS*Rules, на которой была написана исходная система, для определения имени последнего
события
необходимо
было
проанализировать
переменную
WINDOW_RETCODE. Таким событием может быть нажатие клавиши или событие, сгенерированное системой (например, при прокрутке экранного элемента значение индекса стало больше, чем значение любого из элементов, показанных на экране). Если последнее
событие
не
было
сгенерировано
системой,
то
переменная
WINDOW_RETCODE будет содержать строку с уникальным именем элемента, с которым было связано это событие (HPSID). В приведенном примере используется также оператор USE RULE. Этот оператор передает управление другому правилу. Если вызываемое правило отображает какуюлибо экранную форму, то по умолчанию эта форма будет показана в модальном режиме (т.е. все остальные окна будут скрыты системой и недоступны). Другой вариант вызова правила заключается в использовании опции NEST, как в приведенном выше примере. В этом случае все окна, показываемые вызываемым правилом, будут выведены поверх уже существующих окон (а не вместо них), но все равно в модальном режиме. Наконец, можно использовать вариант вызова USE RULE DETACH, при котором все окна, показываемые в вызываемом правиле, будут немодальными. Некоторые из вызываемых программ могут быть написаны на другом языке программирования. Такие программы называются в HPS*Rules компонентами. Существуют системные компоненты и пользовательские компоненты. Системные компоненты предоставляют некоторую дополнительную функциональность, чаще всего связанную с пользовательским интерфейсом. С точки зрения программиста, системные компоненты представляют собой "черные ящики", так как язык, на котором они написаны, и их внутренняя организация целиком скрыты от программиста. Пользовательские компоненты обычно написаны на C, PL/I или Коболе. Пользовательские компоненты используются для добавления в приложение различной
63
функциональности,
которую
невозможно
или
слишком
сложно
написать
на
HPS*Rules3.
3.3. Автоматизация решения задачи Описываемый проект не был для нашего коллектива первым в области переноса приложений, написанных на HPS*Rules, поэтому еще до начала проекта мы имели специально написанное средство реинжиниринга, которое автоматически генерировало по исходным текстам приложения соответствующие программы на Коболе, Java или Visual Basic. Максимальной степени автоматизации удалось достигнуть при конвертации серверной компоненты из HPS*Rules в Кобол: только 50,000 строк из 465,000 были написаны вручную или требовали исправлений в сгенерированном тексте, т.е. уровень автоматизации был приблизительно равен 90%. В общем-то, этого и следовало ожидать, так как HPS*Rules и Кобол имеют сходную структуру и собственный транслятор
HPS*Rules
предусматривал,
как
один
из
возможных
вариантов,
автоматическую генерацию Кобола по HPS для серверной части. Однако еще до начала проекта было ясно, что уровень автоматизации при конверсии клиентской части окажется значительно
ниже из-за ограничений Кобола и
взаимодействия с пользователем (подробнее этот вопрос освещен в следующем разделе, см. 3.1). Поэтому еще до начала проекта была инициирована работа по повышению уровня автоматизации в процессе генерации Кобола/BMS. Целью этих работ было достижение 60%-ной автоматизации при конвертации системы. Эту задачу удалось успешно решить — 215,000 из 318,000 строк системы были сгенерированы автоматически, что соответствует уровню автоматизации почти в 60%. К сожалению, практически невозможно посчитать процент автоматизации, достигнутый при переводе из HPS*Rules в VB, так как в этом проекте изначально не были приняты специальные меры для различения автоматически сгенерированных строк и ручных исправлений. Поэтому мы можем дать только приблизительную оценку автоматизации — от 30 до 40%. Это значительно меньше, чем при конвертации в Кобол и значительно ниже, чем следовало ожидать, так как структура языка HPS*Rules во многом схожа с Visual Basic: оба языка используют одинаковую схему взаимодействия с пользователем (событийно-управляемый подход), используют формализованные экранные формы, похожую структуру вызовов/возвратов и т.д.
3
Аналогичный метод используется в Visual Basic'е для вызова функций из библиотек, написанных на С++
или другом языке программирования, более мощном, чем VB. Вообще говоря, HPS*Rules является непосредственным предшественником VB, как по времени возникновения, так и по идейному содержанию.
64
Видимо, основной причиной невысокого уровня автоматизации стал недостаток опыта и времени у команды разработчиков средства автоматизации. Описываемый проект был для нашего коллектива первым в области перевода с HPS*Rules на Visual Basic. В предыдущих проектах нашим основным целевым языком был Кобол, поэтому наши средства для генерации Кобола были более отработанными. К началу проекта у нас уже был написанный конвертор в VB, но он еще не был протестирован на реальных примерах, и потому его выход не соответствовал требованиям заказчика. Конвертор пришлось улучшать по ходу проекта, но существенного увеличения процента автоматизации (до 80–90%) нам удалось достигнуть уже только по его окончании. Наконец,
отметим,
что,
как
выяснилось,
далеко
не
всегда
обязательно
автоматически генерировать законченный и не требующий исправлений код. В некоторых случаях достаточно просто сгенерировать шаблон, который затем можно массово размножить или настроить понятным образом. В описываемом проекте такое решение использовалось достаточно часто. В нашей статистике каждое исправление сгенерированного шаблона считалось ручным исправлением, хотя исправить готовый шаблон, как правило, значительно легче, чем написать весь код с нуля. Так
как
перед
создателями
средств
автоматизации
ставилось
требование
компилируемости сгенерированного кода, такие шаблоны генерировались под комментарием специального вида. Затем группа консалтинга исправляла и дополняла эти шаблоны вручную или при помощи специальных скриптов.
3.4. Процесс конвертации и его трудности 3.4.1. Преобразование в Кобол Как мы уже говорили, конвертация в Кобол состояла из двух частей — из генерации серверной и клиентской частей. Перевод серверной части оказался весьма простой и прямолинейной задачей, но при преобразовании клиентской части мы столкнулись с различными трудностями. Самым существенным отличием между исходным и целевым языками стала схема взаимодействия с пользователем (диалоговая в HPS*Rules и транзакционная в Коболе/CICS). Это привело, например, к тому, что в сгенерированном Коболе пришлось различать вызов правила с окном и вызов правила без окна. В первом случае USE RULE надо было заменять на EXEC CICS LINK, а во втором случае — на EXEC CICS XCTL:
65
PERFORM 4000-XCTL-X. EXEC CICS XCTL PROGRAM('RTGN726') COMMAREA(XCTL-DATA) RESP (R2C-RESP) END-EXEC.
К сожалению, архитектура нашего конвертора не позволяла проводить совместный анализ нескольких программ и потому всегда генерировался EXEC CICS LINK, который затем при необходимости вручную заменялся на XCTL. Это решение породило новые проблемы: XCTL возвращает значение не туда же, откуда был вызван, а в начало программы. Похожая трудность также возникает при отображении окна: когда мы отображаем BMS командой SEND MAP, мы тоже должны закончить работу программы – иначе управление никогда не попадет на экран! По окончании обработки экрана нам придется перезапустить нашу программу снова и получить данные явным образом, с помощью оператора RECEIVE MAP. Поэтому мы решили вставить в начало каждой программы соответствующий обработчик, определяющий, впервые ли мы попали в эту программу или нет. Если мы вернулись в данную программу после вызова другой программы, то нам нужно вернуться в место исходного вызова и продолжить выполнение оттуда. Такая техника "инвертирования программы" была описана в книге [46, c. 169–193] как метод организации взаимодействия между двумя сопрограммами. Проиллюстрируем этот метод на следующем примере, взятом из рассматриваемого приложения: 0000-START SECTION. EVALUATE TRUE WHEN (EIBCALEN OF DFHEIBLK = 0) * Т.е. мы в первый раз в этой программе MOVE LOW-VALUES TO TGAZ505O SET COMM-SET-CURSOR TO TRUE PERFORM INIT-SECTION PERFORM 0000-MAINLINE PERFORM 1000-MAIN-LOOP WHEN (EIBCALEN OF DFHEIBLK = LENGTH OF XCTL-DATA OF DFHCOMMAREA) * Если мы вернулись из другой программы * Вся информация о программе находится в XCTL-DATA. PERFORM 4000-FROM-XCTL SET COMM-SET-CURSOR TO TRUE EVALUATE TRUE WHEN COMM-XCTL-RTGN507-1
66
* Если мы вызвали программу из этой точки в программе PERFORM 3000-FROM-RTGN507-1 PERFORM 1000-MAIN-LOOP WHEN COMM-XCTL-RTGN507-2 * Или если мы вызвали программу из другой точки в программе PERFORM 3000-FROM-RTGN507-2 PERFORM 1000-MAIN-LOOP WHEN COMM-XCTL-RTGN501 PERFORM 3000-FROM-RTGN501 END-EVALUATE WHEN OTHER * Если мы вернулись после SEND MAP * В этом случае мы используем только DFHCOMMAREA, а не временные очереди MOVE DFHCOMMAREA TO RTGN505-COMMAREA IF (EIBAID OF DFHEIBLK = DFHCLEAR OF DFHAID) THEN * Если пользователь нажал клавишу CLEAR, то нам не надо ничего анализировать * (вываливаемся наружу) MOVE LOW-VALUE TO TGAZ505O MOVE -1 TO CUST-SRCH-CRTR-SD-SSN-CL OF TGAZ505-AT SET COMM-FIRST-SCN OF COMM-FIRST-SCN-SW OF RTGN505-COMMAREA TO TRUE SET COMM-CURSOR-IS-SET OF COMM-RTGN505-SW OF RTGN505-COMMAREA TO TRUE PERFORM 5000-CONVERSE-WINDOW END-IF * Если пользователь не нажимал клавишу CLEAR, * то нам надо получить информацию с экрана PERFORM R000-RECEIVE-MAP EVALUATE TRUE * Теперь нам нужно узнать, какая клавиша была нажата; мы будем обрабатывать * только события, определенные в panel-файле, остальные игнорируются... WHEN (EIBAID OF DFHEIBLK = DFHENTER OF DFHAID) MOVE 'ENTER' TO WINDOW-RETCODE PERFORM 2000-AFTER-CONVERSE WHEN (EIBAID OF DFHEIBLK = DFHPF3 OF DFHAID) MOVE 'RETURN' TO WINDOW-RETCODE PERFORM 2000-AFTER-CONVERSE WHEN (EIBAID OF DFHEIBLK = DFHPF9 OF DFHAID) MOVE 'REFRESH' TO WINDOW-RETCODE PERFORM 2000-AFTER-CONVERSE END-EVALUATE END-EVALUATE. * Если мы добрались до этой точки в программе, то надо отображать окно... 5000-CONVERSE-WINDOW. PERFORM S000-SEND-MAP.
67
EXEC CICS RETURN TRANSID
('TGSC')
COMMAREA (RTGN505-COMMAREA) END-EXEC. * Далее идет остальной текст программы...
Подобный заголовок, определяющий использование окон или других программ, автоматически генерируется в начале каждой программы. Это решение несколько затемняет исходный поток управления, но с другой стороны, изменения затрагивают только начало и конец программы; вся остальная логика остается неизменной. Таким образом, сгенерированная программа сохраняет подобие исходной программе. Это улучшает сопровождаемость программы, так как позволяет инженерам сопровождения быстрее ориентироваться в ней. Кроме того, подобный прием типичен для программ, написанных в среде CICS. Прочие проблемы, возникшие при переводе в Кобол, в основном связаны с ограничениями целевого языка. Например, вначале мы планировали использовать стандартный для CICS механизм передачи параметров между программами – использование DFHCOMMAREA. Однако во многих случаях суммарный объем параметров превышал 32 килобайта, являющихся максимумом для данной глобальной области. По этой причине мы решили использовать DFHCOMMAREA только для передачи данных между программой и ее окнами (т.е. только для операторов SEND MAP и RECEIVE MAP). Все остальные параметры передавались только через временные очереди, уникальные для каждой программы. Точнее, для каждой программы создавалось две временные очереди: одна для сохранения своих собственных данных, а другая для передачи и получения параметров при взаимодействии с другими программами. После прочтения данных из очереди, сама очередь всегда уничтожалась. Это делалось для обеспечения корректности данных и для того, чтобы избежать утечки памяти на "зависших" очередях. Другая неприятная особенность Кобола заключается в том, что в нем запрещены промежуточные вычисления "на ходу", например, внутри оператора присваивания. Это пришлось учитывать при трансляции выражений следующего вида: map SUBSTR(L_CHAR_DATE,6,5) ++ '-' ++ SUBSTR(L_CHAR_DATE,1,4) to
L_CHAR_DATE_2
В Коболе по данному фрагменту необходимо сгенерировать следующее:
68
STRING L-CHAR-DATE(6:5) '-' L-CHAR-DATE(1:4) DELIMITED BY SIZE INTO R2C-TEMPVAR-017 MOVE R2C-TEMPVAR-017 TO L-CHAR-DATE-2
Естественно, такое решение порождает множество временных переменных в конечной программе. Интересно также отметить, что в результате перевода программ на Кобол пришлось явным образом записывать многие операции, "прозрачные" для пользователя HPS*Rules. Например, пользователю HPS*Rules необязательно знать, как именно реализована функция SUBSTR. В последнем примере последствия этого не очень очевидны, так как результат трансляции лишь немногим длиннее и сложнее, чем исходный вызов встроенной функции SUBSTR. Однако многие другие встроенные функции HPS*Rules требуют значительно более запутанной реализации. Например, HPS*Rules поддерживает арифметические операции над переменными типа "дата", как в следующем примере: dcl SubmissionDeadline, LastVersion, AcceptanceNotification date; DaysToWait integer; enddcl map Date ('Monday, January 15th, 2001', '%W, %M %D, %Y') to SubmissionDeadline map SubmissionDeadline - 14 to LastVersion map Date ('06/01/01', '%0m/%0d/%0y') to AcceptanceNotification map NotificationAcceptance - SubmissionDeadline to DaysToWait
Некоторые диалекты Кобола поддерживают арифметику над датами с помощью специальных встроенных функций, преобразующих числа в даты и наоборот. В Коболе/370 этой цели служат функции INTEGER-OF-DATE и DATE-OF-INTEGER. Но для успешной компиляции сгенерированного кода в других диалектах Кобола необходимо добавлять параграфы, реализующие эту функциональность. В результате, большой объем кода, спрятанного в исходных программах в библиотеках динамической поддержки, заново появляется в целевых программах. Конечно же, это отрицательно сказывается на качестве порождаемого кода, но нет никакого другого способа поддержать встроенные конструкции исходного языка, отсутствующие в целевом языке (этот вопрос уже обсуждался в главе 2).
69
Любопытно, что эта проблема менее критична для VB, так как VB содержит большее количество встроенных конструкций, чем Кобол. Это позволяет поддержать многие функции HPS*Rules более простым и коротким образом. В частности, примеры, приведенные выше, не представляют никакой проблемы при переводе в VB, так как VB содержит богатый набор функций работы со строками и встроенный тип Date, поддерживающий арифметику над датами.
3.4.2. Преобразование в VB Несмотря на то, что HPS концептуально близок к VB, разница между ними все-таки оказалась достаточно серьезной. Главное отличие между ними заключается в том, что в VB события обычно привязываются к какому-либо конкретному элементу формы (кнопка, выпадающий список и т.п.), в то время как в HPS у конкретных элементов окна никакой динамики нет – все элементы окна являются пассивными контейнерами для данных. Единственным способом получить информацию о том, что произошло какое-то событие, является вызов специальной системной компоненты (типа GET_SELECTED_FIELD, GET_ALTERED_FIELD и т.п.). Вернемся к нашему примеру программы на HPS из раздела 3.2. Понятно, что содержательно этот пример похож на большинство программ на VB, но в действительности, если бы мы сразу писали данную систему на VB, то мы, скорее всего, написали бы большинство процедур по-другому. Обычным для VB стилем является локализация всех возможных реакций на нажатие кнопок, в отдельных процедурах следующего типа: Private Sub Refresh_Click() CustSrchCrtrSd.Text = "" CustSrchCrtrSd.SetFocus End Sub
Подобные процедуры вполне возможно генерировать автоматически путем аккуратного анализа приложения в целом, но для этого необходимо одновременно разбирать различные типы исходных программ (rules-, bind- и panel-файлы). Такое решение было реализовано только после окончания описываемого проекта. Во время пилотного проекта системные компоненты были поддержаны более прямолинейно. Многие вызовы компонент привязываются к конкретной форме, например, компонента CLEAR_WINDOW_CHANGES специфична для каждой конкретной формы и содержит инициализацию всех полей. Однако не во всех случаях такая привязка возможна (см. след. пример):
70
map L_WNDW_LONG_NM
to WINDOW_LONG_NAME of CLEAR_SELECTED_FIELDS_I
use component CLEAR_SELECTED_FIELDS
Проблема с переводом CLEAR_SELECTED_FIELDS заключается в том, что эта компонента должна очищать поля во всех окнах, показываемых в данный момент. Для минимизации
изменений
в
программах
мы
решили
реализовать
данную
функциональность в отдельном классе со следующим методом Run: Public Sub ClearSelectedFields.Run() Dim frmF As Object Dim boolRetCode As Boolean Dim lngN As Long For lngN = colOpenedForms.Count To 1 Step -1 Set frmF = colOpenedForms.Item(lngN) boolRetCode = False On Error Resume Next boolRetCode = frmF.ClearSelectedFields(strWINDOW_LONG_NAME) On Error GoTo 0 If boolRetCode Then intRETURN_CODE = 1 Exit Sub End If Next lngN intRETURN_CODE = 0 End Sub
Затем в каждом классе мы определили небольшую процедуру ClearSelectedFields, очищающую все поля в окне (возможно, вызывая другие ClearSelectedFields для составных полей), и затем возвращающую управление. Обратим внимание, что поддержка системных компонент в Коболе была несколько другой. Для большинства системных компонент нам удалось найти краткие (от одной до десяти строк) эквиваленты в CICS. Например, вместо вызова SET_CURSOR_FIELD в Коболе достаточно сгенерировать следующую команду: MOVE -1 TO FIELD_TO_HIGHLIGHT.
71
Тем не менее, несколько системных компонент пришлось реализовывать в виде отдельных параграфов. Так как таких компонент было немного, мы решили скопировать
код,
реализующий
эту
функциональность,
во
все
программы,
использующие эти системные компоненты. Конечно же, каждую системную компоненту можно было бы реализовать в виде отдельной программы, но это еще больше усложнило бы заголовок программы. В заключение, хотелось бы подчеркнуть, что основные различия между конвертацией в Кобол и конвертацией в VB заключались в различии используемых в этих языках моделей данных, вызовов процедур и взаимодействия с пользователем. Поэтому основной проблемой при конвертации стало "сближение" исходного и целевого языков именно в этих областях. Тем не менее, несмотря на существенные различия между Коболом и VB, нам удалось воспользоваться одинаковым подходом для обоих целевых языков, так как в обоих случаях изменения были локализованы в начале программы (см. подробный пример на Коболе в разделе 4.1) и в конце программы (обработка ошибок). Создание подобных "вступлений" и "окончаний" ставит своей целью аккуратную эмуляцию вычислительной модели исходного языка и, естественно, проводится по разным сценариям для различных целевых языков. В то же время, преобразование собственно алгоритмов программы обычно производится сходным образом для всех целевых языков. В результате использования такого подхода, нам удалось создать универсальную схему преобразования, которая сохраняет структуру исходных программ и тем самым упрощает последующее сопровождение.
3.5. Обсуждение 3.5.1. Программные факторы, влияющие на уровень автоматизации при языковых преобразованиях Существует
множество
программных
факторов,
влияющих
на
уровень
автоматизации при языковых преобразованиях. Назовем наиболее заметные из них: •
Парадигмы программирования исходного и целевого языков
•
Выразительная сила исходного и целевого языка
•
Способы взаимодействия с пользователем.
1. В самом начале работ необходимо определить, насколько сильно различаются парадигмы исходного и целевого языков. Например, перевод из Java в С++ проще, чем перевод из С++ в Java, но оба этих случая значительно проще, чем перевод из Java в VS
72
COBOL II, так как последний требует среди прочего и изменения парадигмы программирования. Дело в том, что упомянутый стандарт Кобола не поддерживает объектов, и в этом смысле Java "богаче", чем VS COBOL II. В нашем проекте перевод из Rules в VB был концептуально проще, чем трансляция в Кобол (даже несмотря на то, что нам не удалось подтвердить это утверждение по ходу проекта), так как Rules может быть представлен как "строгое подмножество" VB. В случае Кобола это утверждение было бы неверно, так как Кобол страдает от целого ряда ограничений. 2. Полезным параметром для сравнения является выразительная сила языков, т.е. количество ассемблерных команд, соответствующих одному оператору языка [49]. Перевод из более выразительного языка в менее выразительный обычно приводит к "многословному" коду, который обычно оказывается трудным в поддержке. В нашей практике этот тезис уже проверялся – как упоминалось в главе 1, система RescueWare изначально предназначалась для перевода именно из Кобола в HPS*Rules! Однако программы на HPS*Rules, порождавшиеся созданным нами тогда средством были во много раз длиннее исходных и практически несопровождаемыми. В нашем проекте мы пытались измерять выразительную силу языков с помощью самой простой из существующих метрик – количеством строк в программе до и после перевода. В данном проекте оказалось, что объем, выраженный в количестве содержательных (т.е. не пустых и не закомментированных) строк, практически не изменился в процессе перевода с Rules на VB – и до, и после перевода количество строк было близко к 160,000. На самом деле, общее количество строк в HPS*Rules было значительно больше из-за традиционного соглашения разделять все операторы пустой строкой. Код на VB также содержал множество комментариев, призванных обеспечить полноценную привязку к исходному тексту. Тем не менее, в общем и целом оказалось, что эти языки имеют очень похожую структуру и приблизительно одинаковую выразительную силу. Было бы интересно сравнить выразительную силу Rules и Кобола, но в нашем случае это было затруднительно проделать из-за влияния следующих факторов: •
в программах на исходном языке допускалось использование неполной квалификации переменных при условии однозначности определения, но в сгенерированных программах на Коболе использовалась уже только полная квалификация (стандарт Кобола этого не требует, но наше средство автоматизации всегда генерировало только полную квалификацию);
•
программы на Коболе имеют фиксированный формат, поэтому зачастую приходится переносить утверждение или идентификатор на следующую строку.
73
Существуют и другие, более точные способы измерения объема программ и выразительной силы языка программирования (например, уже упоминавшиеся в главе 1
функциональные
точки
входа).
Кроме
того,
для
большинства
общеупотребительных языков их выразительная сила уже известна, поэтому еще до начала проекта можно воспользоваться этим параметром для оценки выполнимости проекта. 3. Достижимый уровень автоматизации зависит и от объема взаимодействия с пользователем. Серверные приложения лучше всего поддаются автоматизации, что было подтверждено и в нашем проекте. С клиентской частью дело обстоит труднее, так как с большой вероятностью общение с клиентом в исходном и целевых языках программирования производится по-разному. Например, сравним интерфейсные решения в таких языках, как VB, Java и Кобол. В Коболе (по крайней мере, в наиболее распространенных его стандартах) отсутствует графический интерфейс, но есть формальные описания текстовых экранных форм (BMS или MFS или AS400 screens). В Java нет отдельных описаний экрана, но есть графический интерфейс. Лишь в VB есть и то, и другое. В Коболе общение с клиентом ограничено одним окном и производится в "псевдоразговорном" (pseudoconversational) режиме, а в VB и Java наблюдается модальное и немодальное диалоговое взаимодействие, событийное управление и т.д. Трудности преобразования клиентских интерфейсов хорошо изучены; в промышленных средствах реинжиниринга преобразование экранных форм, как правило, является одним из наиболее слабых мест. Один из подходов по автоматизации преобразования устаревших интерфейсов представлен в статье [31]. С другой стороны, базы данных почти никогда не вызывают серьезных проблем. Повсеместное использование реляционного подхода и SQL привели к тому, что базы данных и запросы к ним в большинстве случаев практически не изменяются. Однако и здесь могут возникнуть некоторые проблемы, например, в том случае, когда исходная база данных иерархическая или сетевая, а не реляционная (на эту тему см. статью [73]), или в тех случаях, когда для доступа к базе данных используется не SQL, а какие-либо более специфические стандарты (например, преобразования между ODBC, JDBC, DAO, ADO и т.п.).
3.5.2. Экономические соображения при разработке автоматизированных средств преобразования языков До сих пор мы рассматривали только "теоретические" меры сложности написания конвертора. В реальной жизни необходимо рассматривать не только эти отвлеченные параметры, но и объем рынка для создаваемого конвертора. Отметим, что обычно
74
стоимость реинжиниринга обсуждается с точки зрения заказчика (например, хорошая метрика для определения стоимости сопровождения и реинжиниринга приведена в [85]), но в данном случае мы рассматриваем этот вопрос с позиций организации, предоставляющей услуги по реинжинирингу. Еще до начала разработки средств автоматизации
необходимо
задаться
следующим
вопросом:
какой
уровень
автоматизации экономически оправдан для рассматриваемого проекта? Понятно, что любая автоматизация лучше, чем полностью ручной процесс, так как средства автоматизации могут существенно снизить стоимость проекта. Кроме того, инструментальные средства обычно помогают более точно оценить общий объем работ в проекте – при условии, что эффективность средства автоматизации оценивается реалистично.
Недооценка
сложности
написания
или
использования
средства
автоматизации является очень распространенной ошибкой, ведущей к провалу проектов.
К
сожалению,
ограничения
средств
автоматизации
постоянно
недооцениваются. С другой стороны, по достижении некоторого уровня, увеличение автоматизации на каждый следующий процент может привести к радикальному увеличению сложности (а, следовательно, и стоимости) средства. Например, часто встречается следующий сценарий: вначале транслятор настраивается на некоторое подмножество языка, и это ограничение возможных входных данных отражается в архитектуре средства. Затем оказывается, что выбранного подмножества языка недостаточно для выполнения реальных проектов по реинжинирингу. Это неизбежно означает необходимость фундаментальной переделки средства – возможно даже необходимость полного переписывания! Поэтому более грамотно заранее выбрать экономически оправданный уровень автоматизации и внести поддержку этого уровня в требования к средству автоматизации. Это позволит разработчикам средства принимать осознанные решения об ограничении функциональности. Затем код, сгенерированный средством, может быть уточнен или улучшен вручную. Автоматизация снижает количество ошибок и делает полученные результаты более однообразными, но лишь за счет уменьшения гибкости, необходимой для достижения оптимального решения. Еще одно соображение, которое необходимо учитывать при создании средства автоматизации – это объем рынка конвертации с исходного языка программирования. К сожалению, очень трудно заранее оценить, получится ли оправдать вложения в разработку средства во время выполнения проектов по реинжинирингу реальных систем. Это сложная проблема, не относящаяся напрямую к программированию, но именно из-за этих соображений 100% автоматизация реинжиниринга крайне редко рассматривается даже как идеальная цель.
75
3.5.3. Индустриальная проблема: нахождение компромисса между поставщиком услуг по реинжинирингу и заказчиком Понятно, что увеличение уровня автоматизации может привести и к увеличению объема "чужеродного" кода (например, в примерах из раздела 4 мы видели следы исходной платформы HPS в порожденном коде на Коболе). По этой причине стороны, участвующие в проекте по реинжинирингу, должны заранее оговорить принципы преобразования, приемлемые для обеих сторон. Другими словами, необходимо ответить на следующие вопросы: •
Какие части и/или особенности исходной системы должны быть преобразованы?
•
Какие части и/или особенности исходной системы не нужны на целевой платформе?
•
Какие фрагменты кода требуется полностью переписать?
•
Насколько конечная система доллжна соответствовать исходной?
Эти вопросы критичны для компаний, предоставляющих услуги по реинжинирингу программ, так как их целью является выполнение поставленных заказчиком задач при вложении минимально возможного объема усилий. Очевидно, что задачи заказчика и исполнителя в данном случае сильно противоречат. Именно по этой причине необходимо добиться приемлемого компромисса заранее. Это сложное техническое решение обычно принимается отдельно для каждого проекта. Для того, чтобы все эти компромиссы были оправданы, стоимость и риски реинжиниринга должны быть существенно ниже стоимости разработки заново, и заказчик должен быть осведомлен об этих компромиссах.
3.6. Заключение В данной главе мы показали, как средства автоматизации повлияли на крупномасштабный проект по трансформации устаревшей системы. Автор диссертации принимал непосредственное участие в создании рассматривавшихся инструментальных средств, а также в самом преобразовании устаревшей системы. Мы показали, что наличие средств автоматизации может существенно уменьшить время исполнения и стоимость проекта путем увеличения эффективности процесса. В нашем проекте эмпирические результаты противоречили нашим ожиданиям, т.к. преобразование в Кобол оказалось более автоматизированным, чем преобразование в VB. Однако это было вызвано сугубо практическими причинами (недостаток времени, повлекший за собой соответствующее решение заказчика) и потому не снижает значения сделанных нами выводов.
76
Мы утверждаем, что 100% автоматизация процесса преобразования недостижима и даже нежелательна при выполнении промышленных проектов по реинжинирингу. На сегодняшний день, область преобразования устаревших систем остается трудоемкой областью. Наличие некоторой автоматизации критично для того, чтобы сделать масштабные проекты по реинжинирингу экономически осуществимыми. В то же время чрезмерная автоматизация ведет к суб-оптимальным решениям, которые с большой вероятностью
будут
отвергнуты
заказчиком.
Полностью
автоматизированный
реинжиниринг неизбежно ведет к конфликтам с программистами, принимающими сгенерированные программы на сопровождение. Мы считаем, что данная область реинжиниринга требует дополнительных исследований, особенно в области экономических и промышленных аспектов языковых преобразований, однако, такие исследования выходят за рамки данной диссертации. В следующих главах мы предложим некоторые методы и приемы для улучшения качества кода, порождаемого в процессе языковых преобразований.
77
ГЛАВА 4. Извлечение классов из устаревшей системы В данной главе описывается методология создания объектно-ориентированных программ по устаревшим системам. Описывается несколько возможных подходов, отличающихся степенью автоматизации и полнотой получаемых результатов. Приведен пример перевода программы, написанной на языке Кобол, в объектноориентированную систему на С++.
Одной из самых перспективных на сегодняшний день технологий разработки программ является объектно-ориентированный подход. Основные преимущества объектно-ориентированного подхода, согласно [107], таковы: •
объекты представляют собою хорошую модель реального мира и позволяют упростить проектирование системы (абстрагирование, декомпозиция);
•
объекты достаточно независимы друг от друга и взаимодействуют друг с другом через явно прописанные интерфейсы и методы (модульность);
•
механизм наследования позволяет с легкостью дополнять набор свойств существующих объектов (иерархия);
•
при внесении изменений чаще всего оказывается, что необходимо изменить только внутреннее поведение объекта, что практически не оказывает влияния на остальные объекты (следствие инкапсуляции).
Представляется
логичным
предположить,
что
достоинства
объектно-
ориентированного подхода во многом могут быть использованы и при реинжиниринге. В данной главе мы рассмотрим различные подходы к созданию классов по исходным устаревшим системам. При этом особое внимание будет уделено возможностям автоматизации и повторного использования тех или иных компонент исходной системы, так как без таких средств автоматизации реинжиниринг сводится к переписыванию новой объектно-ориентированной системы заново (в этом случае разработчик может использовать устаревшую систему только в качестве справочника по функциональности). Еще раз подчеркнем, что переход на объектно-ориентированную парадигму не является самоцелью и должен производиться только в тех случаях, когда этот шаг оправдан с точки зрения уменьшения стоимости дальнейшего сопровождения. В дальнейшем мы больше не будем останавливаться на экономической мотивации реинжиниринга, а сосредоточимся исключительно на технических деталях.
78
В рамках данной главы мы будем рассматривать задачу переноса устаревших программ, написанных на языке Кобол, в объектно-ориентированные программы на современных языках программирования, таких как C++, Java или Visual Basic. Глава организована следующим образом. В первой части кратко формулируются основные идеи предлагаемого подхода. Во второй части описан этап предварительной реструктуризации программ. Предметом третьей части является собственно процесс перехода от структурированной программы к объектам. Четвертая часть состоит из небольшого примера, показывающего основные этапы трансформации программы на Коболе
в
объектно-ориентированную
программу.
В
пятой
части
приведены
альтернативные подходы, позволяющие добиться частичных решений при большей автоматизации. Наконец, заключение посвящено подведению итогов и определению перспективных направлений исследований.
4.1. Краткое изложение предлагаемого подхода В исходных программах на Коболе достаточно трудно увидеть контуры будущей объектно-ориентированной системы, так как между исходным и целевыми языками программирования существует принципиальная идеологическая разница. Как мы показывали в главе 2, при прямолинейной конвертации программ из одного языка программирования в другой сохраняются все проблемы, присущие исходному языку, а также появляются дополнительные сложности, вызванные различиями между исходным и целевым языками. Можно сказать, что преодоление этой разницы и есть наиболее сложная проблема реинжиниринга. Действительно, решая проблему автоматизированного создания объектно-ориентированных программ по исходным текстам на Коболе, мы тем самым неявно пытаемся ответить на вопрос "Как могла бы выглядеть данная программа, если в распоряжении ее автора были бы более современные языки программирования и средства разработки?". Процесс перехода к объектно-ориентированной технологии удобно разделить на два этапа. На первом этапе происходит реструктуризация программ, ставшая в последнее время вполне традиционным этапом реинжиниринга. В результате реструктуризации программа существенно упрощается и становится легче в сопровождении. Следующим шагом является трансформация структурированной программы в объектно-ориентированную. Наиболее естественным подходом является разбиение на классы всей программы целиком, но этот процесс оказывается трудным в формализации и автоматизации. По этой причине в данной главе рассмотрены и другие подходы, при которых автоматически создаваемые классы могут соответствовать не всей функциональности исходной системы или, наоборот, в один класс может быть помещена вся программа целиком и т.д.
79
4.2. Предварительная структуризация программ Задача реструктуризации известна достаточно давно. Обычно она решается путем проведения
структурирующих
преобразований
в
рамках
исходного
языка
программирования. Наиболее распространенной задачей является структуризация программ на Коболе [80] и Фортране [10], так как изначально эти языки не поддерживали
структурных
конструкций.
Мы
уже
рассматривали
вопрос
реструктуризации в рамках одного языка в разделе 1.2.3, поэтому здесь мы не будем рассматривать этот вопрос подробнее. В то же время задача реструктуризации при переводе из одного языка программирования в другой относительно нова. Как уже говорилось в главе 2, различия между
исходным и целевым языком программирования усложняют процесс
преобразования языков, поэтому мы предложили производить реструктуризацию приложения в несколько этапов. Одним из достоинств такого подхода является возможность
использования
каждого
реструктуризирующего
просмотра
как
самостоятельного продукта. Однако в процессе преобразования из одного языка в другой необходимо также выполнить
ряд
дополнительных
реструктуризирующих
просмотров,
которые
приближают исходную программу к конструкциям целевого языка. В RescueWare все преобразования подобного рода производятся над промежуточным представлением – деревом разбора исходной программы. Дальнейшие преобразования программы к объектно-ориентированной форме во многом опираются на результаты таких реструктуризирующих просмотров, поэтому ниже мы дадим краткое описание основных этапов реструктуризации, выполняемых над промежуточным представлением программ в RescueWare.
4.2.1. Выделение процедур В исходных программах, написанных на Коболе, отсутствовало понятие процедуры; единственным средством структуризации, доступным программисту, был параграф. Семантика параграфов и их вызовов заметно отличается от семантики процедур в современных С++-подобных языках. Поэтому в процессе языковых преобразований необходимо провести разбиение параграфов на процедуры. Основная идея такого преобразования заключается в определении процедур как последовательностей параграфов, используемых в операторах PERFORM. При реализации
такого
подхода
необходимо
преодолеть
определенные
проблемы,
связанные с тем, что в Коболе в операторе PERFORM можно явно задать начальный и конечный параграфы выполняемого кода, причем в различных операторах PERFORM
80
допустимо использование пересекающихся "подпрограмм". Таким образом, в целевых языках приходится моделировать процедуры со множественными точками входа, что заметно усложняет логику программы и, следовательно, ее сопровождаемость. В RescueWare за решение этой задачи отвечает отдельный просмотр, описанный в статье [114].
4.2.2. Локализация или полное уничтожение GOTO Во времена создания Кобола оператор безусловного перехода расценивался как вполне приемлемая конструкция языка. Однако позже широко распространилось мнение, что операторы GOTO усложняют понимание программы и потому следует избегать их широкого применения [36]. В некоторых современных языках (например, в Java) от использования goto отказались совсем. Поэтому в процессе реинжиниринга необходимо заменить операторы безусловного перехода на эквивалентные им структурные операторы. Для целевых языков, содержащих ограниченные возможности применения операторов безусловного перехода (например, С/С++), можно не уничтожать операторы goto совсем, а ограничиться их локализацией. Решение данной задачи осложняется тем, что операторы безусловного перехода могут нарушать структурность обычных конструкций, таких как условные и циклические
операторы.
Из-за
этого
зачастую
приходится
использовать
преобразования, изменяющие внешний вид программы, в том числе и в структурных конструкциях, что может привести к заметному изменению внешнего вида программ. В RescueWare задача локализации или уничтожения goto решается с помощью алгоритма, описанного в статье [115]. Недавно также появилось предложение по дальнейшему усовершенствованию этого алгоритма путем использования копирования фрагментов кода исходной программы [116].
4.2.3. Локализация данных Одно из существенных отличий Кобола от современных языков заключается в том, что в Коболе все данные носили глобальный характер. Тривиальным решением является сохранение глобальности всех данных и в преобразованных программах, но в таком случае во многом теряется смысл конвертации. Для того чтобы полученные программы более точно соответствовали идеологии целевых языков, необходимо провести
процесс
локализации
данных.
Его
задачей
является
оптимальное
распределение переменных по процедурам и передача переменных к месту их использования в качестве параметров. В RescueWare этим занимается специальный просмотр – анализ потоков данных (Data Flow Analysis, DFA), описанный в статье [112].
81
4.2.4. Оптимизирующие преобразования Время жизни систем, для которых проводится реинжиниринг, исчисляется годами и десятилетиями. За этот срок программы, составляющие систему, могут претерпеть настолько существенные изменения, что порою перестают напоминать исходный вариант даже отдаленно. Особенно тяжелы последствия сопровождения в тех случаях, когда сопровождающий коллектив не принимал участия в проектировании и начальной разработке
системы.
В
результате
длительного
сопровождения
в
системе
накапливаются наслоения и не используемые участки кода. Именно поэтому применение оптимизирующих просмотров в задачах реинжиниринга дает значительно больший эффект по сравнению с обычной оптимизацией программ. Отметим, что далеко не весь набор традиционных оптимизаций разумно применять при реинжиниринге, так как при обычной оптимизации не поднимается вопрос последующего сопровождения оптимизированных текстов – все
преобразования
программ происходят непосредственно перед генерацией объектного кода. Но после реинжиниринга оптимизированные программы должны в дальнейшем сопровождаться и развиваться, поэтому в RescueWare используются только такие преобразования, которые повышают сопровождаемость сгенерированных текстов. Наиболее полезными из них оказались следующие: •
уничтожение неиспользуемых переменных;
•
уничтожение недостижимого кода;
•
замена полной квалификации выборки из структуры на частичную с помощью использования макросов
Более подробно вопросы улучшения исходных текстов освещены в статьях [110, 113]. Итак, в результате реструктуризации мы имеем программу, эквивалентную по функциональности
исходной,
но
записанную
в
духе
структурных
языков
программирования (таких, как С, Pascal, Ada). Как уже говорилось выше, такой промежуточный результат может представлять и самостоятельную ценность. Тем не менее, не остановимся на этом и покажем, как автоматизировать следующий шаг – от структурных программ к объектно-ориентированным.
82
4.3. Переход к объектно-ориентированным программам В результате работы описанных выше просмотров мы имеем структурированную программу, состоящую из процедур и принадлежащих им переменных. Эта программа хранится в виде промежуточного представления, получившего название логической модели. Следующим шагом является собственно переход от структурированной формы к объектно-ориентированной. Эта задача оказалась значительно менее тривиальной, чем ожидалось. Прежде всего, это связано с большим количеством возможных решений и связанной с этим нечеткостью постановки задачи (достаточно трудно сравнивать различные решения, так как нет четких критериев оценки качества объектноориентированных систем, существуют только эмпирические правила проектирования, которые, как будет показано ниже, иногда даже противоречат друг другу). В этом смысле показательно, что работы над созданием просмотра, разбивающего программу на классы, велись в нашем коллективе в течение двух лет и, тем не менее, функциональность, описываемая ниже, существует только в виде прототипа и не входит в основную версию RescueWare. По-видимому, встреченные нами проблемы носят далеко не случайный характер, и потому нам кажется, что даже отрицательный опыт, накопленный нами в ходе работ, заслуживает подробного изложения. В связи с этим в данной части главы описывается не только окончательное решение, но и процесс движения к нему в хронологическом порядке.
4.3.1. Попытка создания автоматического решения Первоначальная
попытка
решения
поставленной
задачи
исходила
из
предположения, что задача разбиения структурированной программы на классы может быть
решена
автоматически,
без
вмешательства
человека,
подобно
задаче
реструктуризации программы. В качестве такого решения А. Ивановым и Е. Леденевой был предложен следующий алгоритм. На основе информации, полученной от просмотра DFA, строился граф вызовов процедур. Узлами графа являлись процедуры, а дугами — переходы из одной процедуры в другую. При этом дуги нагружались информацией о количестве передаваемых параметров. После этого начинал работу остроумный алгоритм моделирования, основанный на аналогиях с реальным миром. Полученный граф воспринимался как система грузов, связанная пружинами различной жесткости в условиях вязкой среды. Далее эмулировалось поведение этой системы во времени. Процесс заканчивался, когда
83
система достигала устойчивого состояния, т.е. разница между двумя состояниями становилась незначительной. В результате, система распадалась на несколько областей сильной связанности, т.е. в некоторых достаточно малых окрестностях образовывались группы связанных между собой процедур, достаточно слабо взаимодействующих с остальными процедурами. Из таких областей сильной связности и организовывались классы. В качестве исходных параметров пользователь мог задать желательное количество классов (например, 1, 5, по числу процедур) и "силу", необходимую для разрыва связи. К сожалению, этот подход обладал существенным недостатками, затруднявшими его использование. Прежде всего, сомнительным является сам принцип разбиения на классы по принципу связанности по управлению. Дело в том, что при традиционном программировании классы обычно создаются на основе других принципов, а именно, на основе совместно используемых данных, а не с целью сбора в одну кучу всех процедур, вызывающих друг друга. Применение описанного алгоритма на больших примерах приводило к тому, что классы
получались
несоответствующими
пользовательским
представлениям
об
идеальном разбиении. Это осложнялось тем, что встроить в структуру данного алгоритма реальное взаимодействие с пользователем не представлялось возможным. Таким образом, данный подход оказался недостаточно жизнеспособным. Это вызвало потребность в создании нового варианта решения этой проблемы.
4.3.2. Некоторые эвристики для разбиения устаревших программ на классы В связи с недостатками описанного выше алгоритма автором была проделана работа по разработке новых эвристик для разбиения устаревших программ на классы. После дополнительных исследований было выделено три основных принципа разбиения: •
по данным;
•
по вызовам;
•
по переменным первого уровня.
Поясним причины возникновения этих принципов. При создании классов "с чистого листа" обычно в расчет принимается только связанность по данным. Поэтому и при реинжиниринге мы будем считать этот принцип основополагающим. Связанность по вызовам (по "потоку управления") возникает из особенностей задачи. Так как в процессе реинжиниринга мы имеем дело со сложившейся программой
84
на Коболе, то вполне допустимо предположение о том, что для упрощения процесса сопровождения имеет смысл принимать во внимание и существующий поток управления, как более слабый тип связи. Наконец, имеет смысл учитывать организацию данных с помощью переменных первого уровня. Напомним, что в Коболе все данные являются структурами с иерархической подчиненностью, поэтому логично предположить, что при создании исходной системы программист мог объединять близкие по смыслу данные в рамках единых структур (хотя, конечно, не исключено, что в одну структуру могли быть объединены и обычные временные переменные). К сожалению, специфика Кобола не позволяет применить другие принципы идентификации объектов, разработанные для более структурированных языков программирования – анализ типов данных и анализ параметров процедур [41]. Анализ типов данных затруднен из-за того, что в Коболе нет типов в традиционном понимании, так как нет возможности объявить новый тип данных и впоследствии использовать его при описании новых переменных. Переменные могут быть описаны либо с помощью базового типа, либо с помощью создания новой структуры, но даже если переменные имеют одинаковый тип, описание типа все равно придется повторить два раза. Для преодоления этих трудностей возможно использование более сложного анализа программы, основанного на реальном использовании переменных. Например, если две переменные сравниваются друг с другом или участвуют в присваивании, то мы будем считать, что они имеют одинаковый тип и т.д. Приблизительно такой же метод искусственного введения типов используется при анализе на наличие проблемы 2000 года. Что же касается анализа параметров процедур, то в данном случае он дает не слишком правдоподобные результаты, так как в Коболе процедуры отсутствовали и порождаются только в процессе реструктуризации, а их параметры возникают в результате процесса локализации данных. Такие параметры процедур не несут никакой информации, и зачастую их наличие может быть вызвано чисто техническими причинами, как в следующем примере (см. рис. 9):
P1
v
v
P2
Рис. 9. Передача "транзитной" переменной.
85
P3
Пусть после реструктуризации исходной программы мы получили три процедуры – P1, P2 и P3, причем P1 вызывает P2, а P2 вызывает P3. Далее в процессе локализации данных мы выясняем, что переменная v используется в процедурах P1 и P3. Для того, чтобы обеспечить локальность данных и легальность программы, переменная v передается из P1 в P3 параметром через P2 (так называемая "транзитная" переменная). Получается, что, несмотря на то, что переменная v не используется в P2, она все-таки является параметром этой процедуры. Поэтому необходимо как-то отличать реально используемые параметры процедур от "транзитных". Приведенная проблема является хорошей иллюстрацией того, что реструктуризация не всегда ведет к улучшению сопровождаемости и ясности программы, поэтому необходимо критически изучать реальные результаты любых преобразований, производимых на этом этапе.
4.3.3. Диалоговый процесс выделения классов Основой процесса разбиения программы на классы является интерактивная среда, визуализирующая внутреннее представление программы. Среда состоит из двух основных окон: в первом из них содержатся данные и процедуры исходной программы, полученные после реструктуризации ("окно исходной программы"), а второе окно предназначено для построения в нем целевых классов ("окно целевой программы"). Изначально целевая программа предполагается состоящей из всего одного пустого класса. В дальнейшем пользователь может создавать новые классы, а также пополнять их данными и методами. Основным методом для этого является "перетаскивание" (drag & drop) данных и процедур из первого окна во второе. Кроме того, пользователь может воспользоваться просмотром, выполняющим автоматическое разбиение исходной программы на классы согласно принципам, описанным выше. При этом пользователь может предварительно зафиксировать некоторые классы с их данными и методами, чтобы эти классы не участвовали в процессе разбиения. Пользователь также может начать с создания одного класса, включающего в себя все данные и методы исходной программы. Затем из такого универсального класса можно "отщеплять" данные и методы в новые классы, повторяя процесс до тех пор, пока полученные классы не станут самодостаточными, оставаясь при этом относительно независимыми. С помощью такой методики процесс становится инкрементальным и последовательным, а результаты каждого шага процесса могут быть оценены как положительные или отрицательные (в последнем случае необходимо произвести откат одного или нескольких изменений). В любой момент разбиения на классы пользователь может запустить генерацию конечного кода. Если результаты генерации неприемлемы, то процесс разбиения на
86
классы можно продолжить с того же места, на котором он был прерван. Таким образом, процесс разбиения на классы является итеративным.
4.3.4. Недостатки предложенного подхода и возможности дальнейшего усовершенствования Одним из простых путей к улучшению существующей реализации является предоставление пользователю возможностей численно задавать степень важности связей по данным, по вызовам или по переменным первого уровня. Кроме того, возможно
введение
некоторых
других
численных
показателей,
например,
максимальное количество переменных в классе, максимальный объем класса глобальных данных и т.д. Другим
предложением
по
улучшению
является
работа
с
“транзитными”
переменными. При достаточной удаленности по цепочке управления использования переменной от ее описания такой способ передачи себя не оправдывает даже в необъектной
программе.
Поэтому
такие
переменные
являются
очевидными
кандидатами в глобальные переменные, а в дальнейшем – кандидатом в данные класса, содержащего глобальные переменные.
4.4. Пример преобразования программы к объектноориентированному виду Проследим процесс перевода устаревшей программы в объектно-ориентированную программу на C++ на примере простой записной книжки. К сожалению, не представляется возможным привести здесь пример реальной программы, подлежащей разбиению на классы, так как ее текст занял бы значительно больше места, чем весь текст диссертации. Таким образом, продемонстрировать предложенный подход на реальных задачах не представляется возможным. Тем не менее, основные приемы (например, генерация одного общего класса и разбиение его на два независимых) могут быть прослежены вполне отчетливо. PhoneBook.cbl 00001
IDENTIFICATION DIVISION.
00002
PROGRAM-ID. PHONEBOOK.
00003
AUTHOR. ALEXANDRE DRUNIN.
00004 00005
ENVIRONMENT DIVISION.
00006
DATA DIVISION.
00007
WORKING-STORAGE SECTION.
87
00008 00009
01 PHONE PIC 9999999 .
00010
01 ROOM PIC S9999 SIGN IS LEADING SEPARATE.
00011
01 CHOICE PIC S9 SIGN IS LEADING SEPARATE.
00012 00013
PROCEDURE DIVISION.
00014
DISPLAY "Demo Program for RescueWare".
00015
DISPLAY "Simple phonebook for TERKOM".
00016
PERFORM MENU UNTIL CHOICE = 4 .
00017
GOBACK.
00018
MENU.
00019
DISPLAY "Enter 0 to Create Phonebook".
00020
DISPLAY "
1 to Delete Phonebook".
00021
DISPLAY "
2 to Add New Number".
00022
DISPLAY "
3 to Search for Number".
00023
DISPLAY "
4 to Quit.".
00024 00025
ACCEPT CHOICE.
00026
EVALUATE CHOICE
00027
00028
WHEN 0 PERFORM CREATE-TABLE
WHEN 1 PERFORM DROP-TABLE
00029
WHEN 2 PERFORM ADD-NUMBER
00030
WHEN 3 PERFORM SEARCH-NUMBER
00031
END-EVALUATE.
00032 00033
CREATE-TABLE.
00034
EXEC SQL
00035
CREATE TABLE Phonebook (ROOM NUMERIC, NUM NUMERIC)
00036
END-EXEC.
00037
DROP-TABLE.
00038
EXEC SQL
00039
DROP TABLE Phonebook
00040 00041
END-EXEC. ADD-NUMBER.
00042
DISPLAY "What room do you want to add phone number for?".
00043
ACCEPT ROOM.
00044
DISPLAY "Enter phone number".
00045
ACCEPT PHONE.
00046
EXEC SQL
00047
INSERT INTO Phonebook (ROOM, NUM)
00048
VALUES (:room, :phone)
00049
END-EXEC.
00050
SEARCH-NUMBER.
00051
DISPLAY "What room do you want to call to?".
00052
ACCEPT ROOM.
88
00053
EXEC SQL
00054
SELECT NUM
00055
INTO :phone
00056
FROM Phonebook
00057
WHERE ROOM = :room
00058
END-EXEC.
00059
DISPLAY PHONE.
Как видно из текста, данная программа состоит из пяти параграфов, вызывающих в процессе работы друг друга и использующих общие данные. Функциональность, реализуемая данной программой, достаточно проста: в зависимости от выбора пользователя, выполняется создание или уничтожение таблицы базы данных, содержащей записи о телефонных номерах, добавление записей в эту таблицу или поиск записи по ключу. В процессе реинжиниринга происходит реструктуризация этой программы (в данном случае оно сводится к разбиению исходной программы на процедуры), затем создается универсальный класс Main, содержащий в себе все методы и переменные. Затем пользователь производит разбиение класса на два новых – Main, реализующий запуск программы, и AccessMethods, реализующий методы доступа к базе данных. Отметим, что на этом этапе производится и локализация данных (переменные Room и Phone перемещаются в AccessMethods, а переменная Choice остается в классе Main). Затем инициируется генерация конечного текста на C++, результатом чего является следующая программа (в тексте данной главы мы ограничимся только заголовочным файлом; полный текст самой программы на С++ приведен в Приложении 2): PhoneBook.h // This file was generated using RescueWare(R)TM version 3.00 // Copyright(C) 1997 Relativity Technologies, Inc. // Generated on Mon May 11 16:47:24 1998 // // File: PhoneBook.h // Description: // Revision: // Date: #ifndef __PHONEBOOK__H__ #define __PHONEBOOK__H__ /* Cobol types */ class AccessMethodsInstance_struct : public CobolStruct
89
{ public: Numeric Room; Numeric Phone; void Search_Number (); void Add_Number (); void Drop_Table (); void Create_Table (); void Menu (); AccessMethodsInstance_struct( unsigned long Start ) : Room( Start + 7, "S9999", "DISPLAY", True, True ), Phone( Start + 0, "9999999", "DISPLAY", False, False ), CobolStruct( Start, 12 )
{ }
CloneFunction( AccessMethodsInstance_struct ) } class MainInstance_struct : public CobolStruct { public: Numeric Choice; void Cobol_Main (); MainInstance_struct( unsigned long Start ) : Choice( Start + 0, "S9", "DISPLAY", True, True ), CobolStruct( Start, 2 )
{ }
void Init( String ); CloneFunction( MainInstance_struct ) } /* Global variables */ DllImport Dispatcher CICS; extern AccessMethodsInstance_struct AccessMethodsInstance; extern MainInstance_struct MainInstance; /* Function declarations */ void Main( void ); void InitFunction( void ); #define Choice MainInstance.Choice #endif
90
Таким образом, с помощью автоматизированного конвертора RescueWare было проведено преобразование исходной неструктурированной программы на Коболе в ее объектно-ориентированный аналог на C++, выполняющий те же действия.
4.5. Другие подходы к созданию объектов Итак, мы проследили процесс создания объектно-ориентированной программы из устаревшей системы и убедились, что движение по этому пути наталкивается на множество проблем, которые вряд ли могут быть решены автоматически. Практически у каждого из упомянутых решений имеются достоинства и недостатки, и поиск решений в данной области по-прежнему продолжается. Вопрос об извлечении объектов из устаревших программ периодически обсуждается на конференции OOPSLA (см., например, [48]). Далее мы перечислим другие подходы к решению данной проблемы, иногда демонстрирующие более высокую степень автоматизации, хоть и дающие меньшие результаты.
4.5.1. Генерация класса, соответствующего всей программе Наиболее простым решением является трансляция каждой программы на Коболе в один класс (например, оформленный в виде COM-объекта или Java bean), возможно, имеющий входные и выходные параметры. Естественно, при таком подходе удается добиться 100% автоматизации, но возможности последующего использования такой компоненты неясны, так как в большинстве случаев программы на Коболе состояли из нескольких тысяч строк и более. Последующее массовое использование таких классов сомнительно (еще более сомнительна возможность их применения в распределенной по сети системе).
4.5.2. Создание объектных интерфейсов к устаревшим программам Другим широко используемым методом является создание объектных интерфейсов к устаревшим программам. После применения такого подхода устаревшая система рассматривается в дальнейшем как "черный ящик", и обращения к нему производятся только через созданные интерфейсы. Существует множество исследований на данную тему. Так, в статье [35] приведен пример реализации объектного интерфейса к устаревшей системе геометрического моделирования и утверждается, что при таком подходе снижаются расходы на сопровождение системы. В статье [111] описывается процесс генерации интерфейсов на языке IDL с целью дальнейшего использования устаревшей системы в
91
распределенной архитектуре, построенной по стандарту CORBA (кстати, генерация IDL существует и в RescueWare). Тем не менее, такое решение далеко не универсально. Например, наличие интерфейса нисколько не помогает, когда исправления приходится вносить не только в программы, использующие устаревшую систему, но и в сами алгоритмы системы (а к сожалению этот случай наиболее распространен). На самом деле, решение, приведенное в статье [35], удачно только по той причине, что математические алгоритмы, положенные в основу описанной системы, действительно могут меняться достаточно редко.
4.5.3. Генерация классов по срезам программ Интересной темой для дальнейшего исследования является автоматизированное построение классов по срезам программ. При этом подходе пользователь вначале определяет наиболее интересные ему участки программы, создает срез программы, содержащий эти участки (основные идеи построения срезов программ отражены в статье [108]), а затем по полученным срезам генерирует классы на целевом языке. Например, одним из средств, предоставляемых RescueWare, является выделение в отдельный срез некоторых базисных данных программы и набора параграфов, реализующих работу с ними (такое средство носит название Range Extraction). Параграфы, работающие с выбранными структурами данных, преобразуются в отдельную процедуру (при этом глобальные данные подменяются на входные параметры), а их текст в Коболе заменяется на вызов полученной процедуры. Вполне возможно, что выбранный набор параграфов “потянет” за собой всю программу (например, если в выбранном участке текста были операторы PERFORM или GOTO). В таких случаях надо пытаться проделать те же действия, но при других начальных данных. Понятно также, что с помощью такого просмотра можно выделить несколько разных процедур из одной программы.
Процедуры, таким образом,
становятся отдельными внешними программами и в дальнейшем из них автоматически генерируются классы, работающие с наборами данных. В принципе, на основе таких классов удобно вести разработку приложений на новых платформах; при этом прочие части программы можно попросту отбросить и переписать их в дальнейшем на целевом языке. Если полученные классы покрывают существенную часть логики программы (на это можно рассчитывать, например, при реинжиниринге финансовых систем или других приложений, предназначенных для хранения и обработки данных), то можно достичь существенной экономии времени по сравнению с ручным переводом.
92
Этот метод аналогичен по своей сути методу создания CRUD-библиотек (Create– Read–Update–Delete) при разработке приложений на SQL, только получающиеся классы имеют более сложную структуру.
4.5.4. Перепроектирование с помощью CASE-средств Наконец, одним из наиболее распространенных средств для создания объектов по устаревшей системе является использование различных CASE-средств. В процессе работ происходит повышение уровня системы до уровня CASE-средства, система приобретает надежный проектировочный фундамент, а дальнейшая разработка системы чаще всего производится уже на платформе самого CASE-инструмента. Понятно, что наиболее проблематичным шагом здесь является "поднятие" исходных текстов системы до уровня CASE-средства. Этот этап достаточно трудно полноценно автоматизировать, так как для каждого исходного языка программирования в CASEсредстве должен быть реализован соответствующий анализ. Мы не будем останавливаться на этом вопросе, так как данная тема значительно шире рамок данной диссертационной работы. Мы ограничимся ссылкой на работы [8, 26, 83, 86].
4.6. Заключение В данной главе описывается предложенная автором методология создания объектно-ориентированных программ по устаревшим системам, которая может быть использована в процессе преобразования языков. Эта методология была разработана в результате анализа и улучшения предыдущих попыток создания средства извлечения классов из устаревшей программы. Кроме того, предлагаемая методология является расширением подхода, описанного в предыдущей главе, и состоит из следующих этапов: •
анализ исходных программ и построение промежуточного представления;
•
предварительная структуризация программ, включающая в себя выделение процедур, локализацию или уничтожение операторов безусловного перехода, локализацию данных и некоторые оптимизирующих преобразований;
•
выделение классов на основе пользовательской информации или различных эвристик (связанность по данным, по вызовам и по структурным переменным);
•
генерация программ на целевых объектно-ориентированных языках.
Схематически
уточненный
процесс
преобразования
проиллюстрирован следующим образом (см. рис. 10):
93
языков
может
быть
Реструктуризация
Реструктуризация
Оригинальная программа
Анализ
Целевая программа
Внутреннее представление
Генерация кода
Выделение объектов
Рис. 10. Уточненная схема преобразования языков, включающая выделение объектов
В данной главе приведен пример перевода программы, написанной на языке Кобол, в объектно-ориентированную систему на С++. Приведено краткое описание других подходов к выделению классов, отличающихся степенью автоматизации и полнотой получаемых результатов. Обсуждены границы применимости описанных методов и различные сценарии их использования.
94
ГЛАВА 5. Использование проектно-ориентированных неформальных знаний при реинжиниринге В данной главе предлагается методика настройки средств реинжиниринга на конкретные проекты, основанная на использовании неформальных знаний о системе. В качестве основного инструмента настройки предлагается использовать знания о регулярном использовании некоторых последовательностей исходного языка в качестве более крупных шаблонов ("язык, специфичный для данного проекта"). Процесс настройки средства реинжиниринга иллюстрируется на примерах из промышленного проекта по конвертации приложения из PL/I в Java. Практически устаревшим
во
всех
системам
случаях
перед
применения
нами
вставали
RescueWare новые
к
промышленным
требования
к
средству
реинжиниринга, связанные со спецификой данной конкретной устаревшей системы, требованиями клиента и т.п. Во всех случаях общая схема реинжиниринга оставалась неизменной, но сами средства реинжиниринга обычно подвергались некоторой доработке, предназначенной для настройки средства реинжиниринга на данную задачу. Это заставило нас внимательнее изучить процесс реинжиниринга и определить причины основных трудностей. Обычно проблемы возникали уже на этапе возвратного проектирования, так как нам не удавалось автоматически извлечь достаточно информации из исходных текстов программ. Людям всегда удается узнать о программе значительно больше, чем любому средству реинжиниринга, так как они привлекают для понимания программ различные сторонние знания о программах (например, люди принимают во внимание значение имен переменных, комментариев, разбиения на файлы и т.п.), причем эти "посторонние"
знания
меняются
от
проекта
к
проекту.
Естественно,
такие
неформальные знания не влияют на исполняемую семантику программы и потому будут отброшены при трансляции и анализе с помощью традиционных методов. Однако в области реинжиниринга подобные знания зачастую оказываются более ценными, нежели точная семантика конструкций исходного языка программирования. Именно по этой причине стандартные подходы оказываются недостаточными для решения задач реинжиниринга. В данной главе мы рассмотрим один вид подобных неформальных знаний, а именно повторяющиеся использования одинаковых шаблонов программирования в проекте. Наша практика показала, что большинство крупных систем содержат множество различных шаблонов. Эти шаблоны можно формализовать, понять (т.е. нагрузить
95
смыслом) и использовать в качестве самостоятельных конструкций в новом языке. Мы утверждаем, что подобные неформальные знания индивидуальны для каждого конкретного проекта или, возможно, для каждого конкретного коллектива, но не привязаны к языку программирования, программно–аппаратной платформе и т.п. В качестве иллюстрации одного возможного применения предложенного подхода, рассмотрим следующий сценарий, зачастую возникающий в процессе трансформации устаревших систем. Предположим, что мы имеем дело с системой, написанной на устаревшем языке программирования и предназначенной для переписывания в современном
языке
программирования.
Такие
системы
всегда
испытывают
ограничения, связанные с несовершенством исходного языка. Например, исходный язык может обладать недостаточными выразительными возможностями, и в этом случае программисты будут вынуждены самостоятельно реализовывать возможности, не поддержанные напрямую в языке. В этом случае, система будет содержать большой процент "вспомогательного" кода, не имеющего непосредственного отношения к задачам, решаемым данным приложением. Такой вспомогательный код может быть сконцентрирован в едином "ядре" системы или разбросан по всей системе. Однако в процессе реинжиниринга может оказаться, что в целевом языке существуют языковые средства или специальные функции, реализующие ту же функциональность, что и вспомогательный код. Например, Java и Visual Basic предоставляют значительно больше стандартных функций по обработке строк, чем С, поэтому при конвертации программ из С в любой из этих языков можно было бы ожидать существенного упрощения этой части программ. Для достижения этой цели конвертор должен анализировать код, работающий с переменными типа char*, и преобразовывать некоторые стандартные наборы операторов в соответствующие методы класса String. Таким образом, нашей задачей является поиск шаблонных наборов операторов, выполняющих функции, отсутствующие в исходном языке, но присутствующие в целевом языке. Пользуясь терминологией, введенной в главе 2, можно говорить, что мы имеем дело с преобразованием конструкций, эмулированных в исходном языке, во встроенные
конструкции
конструкции
можно
целевого
представить
языка. как
Теоретически,
встроенные
эти
эмулированные
конструкции
некоторого
виртуального языка более высокого уровня. Очевидно, такой виртуальный язык лучше (проще) отражает суть работы устаревшей системы, чем ее исходный язык. Мы воспользуемся данным наблюдением в процессе реинжиниринга. В данной главе мы описываем попытку описания такого виртуального языка, специфичного
для
конкретного
проекта,
в
процессе
переноса
приложения,
содержащего 70,000 строк, из PL/I в Java. Излагаемый здесь подход был предложен для
96
того, чтобы существенно улучшить качество генерируемого текста. В заключение данной главы описывается наше видение "идеального" процесса реинжиниринга, в котором найденные человеком неформальные знания учитываются в дальнейших преобразованиях исходной системы.
5.1. Связанные работы В процессе трансформации программ зачастую возникает желание записать формальные правила преобразования программ для последующего их использования в других проектах. Существует несколько практически эквивалентных подходов к записи таких правил — program plans [74], transformation rules [66] и clichés [78]. Во всех этих подходах накопленные правила можно впоследствии автоматически применить к программе путем использования той или иной разновидности сопоставления с образцом [75]. В большинстве перечисленных выше работ для записи шаблонов используется некоторый промежуточный язык. Вместо такого подхода, в работе [81] предложен формализм для записи шаблонов в терминах исходного языка (native patterns), так как предполагается, что такая форма записи будет понятнее и удобнее для программистов, чем любое специально разработанное промежуточное представление. Однако такой подход ограничивает возможности программиста, так как шаблоны могут представлять собой абстракции более высокого уровня, чем те, что присутствуют в языке. В данной главе предлагается механизм, развивающий идею native patterns. Основное отличие заключается в том, что мы предлагаем не только записывать простые синтаксические шаблоны, но и рассматривать их как самостоятельные языковые единицы с последующим применением этих конструкций в последующем процессе реинжиниринга. Насколько нам известно, данный подход ранее не использовался на практике. К сожалению, большинство работ, посвященных методам извлечения неформальных знаний [59, 60, 12, 39], никак не обсуждают возможности использования этих знаний при преобразовании программ. По нашему мнению, это вызвано тем фактом, что неформальные знания и форма их записи меняются от системы к системе и, следовательно, вопрос об автоматическом применении неформальных знаний может быть привязан только к конкретному проекту. Вопрос уточнения знаний об исходной программе в процессе трансформации приложения уже поднимался в статье [98] и в более поздней работе, посвященной проекту Programmer's Apprentice [77]. В отличие от подходов, изложенных в упомянутых работах, мы предлагаем расширять исходный язык не сам по себе, а только применительно к одному рассматриваемому проекту.
97
5.2. Формальная семантика и неформальные знания Алгоритмические языки высокого уровня и их трансляторы были сконструированы для преодоления разрыва между способом человеческого мышления и способом функционирования вычислителя. Несмотря на то, что язык программирования ограничивает возможности использования средств вычислительной среды напрямую, это ограничение возможностей программиста окупается за счет приближения модели вычислений, предлагаемой языком программирования, к уровню абстракции, удобному для человека. В процессе трансляции вся избыточная информация, не влияющая на исполнение программы, отбрасывается транслятором. Поэтому можно говорить, что формальные знания — это все элементы языка программирования, влияющие на исполняемую семантику программы, а неформальные знания — это все прочие элементы программы, призванные улучшать понимаемость программы человеком. Действительно, для программиста исходные тексты представляют собой нечто большее,
чем
просто
операционная
семантика.
Досконально
понять
всю
функциональность любой сколько-нибудь объемной программы, опираясь только на формальные знания, человеку практически не под силу. Поэтому программисты изучают
программы,
опираясь
на
комментарии,
имена
переменных,
стиль
программирования и прочие неформальные знания, так как эта информация значительно ближе к человеческому уровню мышлению, чем примитивные команды языка. Неформальная часть программы важна не только при первичном написании программы, но и при последующем ее сопровождении и реинжиниринге. Практически все события, так или иначе связанные с программой после ее написания, например, внесение изменений или появление в коллективе нового сотрудника, требуют процесса ее понимания. В данной главе мы сконцентрируем наше внимание на одном виде неформального знания — на использовании особого синтаксиса, специфического для данного конкретного проекта. Такой синтаксис может возникнуть в следующих случаях: •
Исходная программа написана на диалекте, не распознаваемом нашим средством реинжиниринга.
•
Исходная
программа
написана
с
использованием
некоторого
хорошо
определенного стиля, например, реализованного с помощью специальных макросов
или
путем
характерного
использования
шаблонов.
98
некоторых
языковых
Продемонстрируем это с помощью небольшого примера на языке PL/I. Приведенная ниже программа эмулирует поддержку объектно-ориентированных конструкций с помощью использования макропроцессорных средств языка PL/I. Эта идея может показаться несколько искусственной, но при этом она не является новой: известно множество языков программирования, созданных как препроцессорные надстройки над другими языками — назовем Ratfor [54] и Objective C [32]4. Поэтому подобный подход иногда встречается в реальных устаревших приложениях для реализации того или иного языкового механизма, отсутствующего в исходном языке. %include maclib; p: proc options (main); CLASS (point); PROPERTY (x) AS (fixed bin); PROPERTY (y) AS (fixed bin); ENDCLASS; MEMBER(get_x) OF (point) RETURNS (fixed bin); return (THIS.x); ENDMEMBER; MEMBER(get_y) OF (point) RETURNS (fixed bin); return (THIS.y); ENDMEMBER; MEMBER(set_x) OF (point) TAKES (value); dcl value fixed bin; THIS.x = value; ENDMEMBER; MEMBER(set_y) OF (point) TAKES (value); dcl value fixed bin; THIS.y = value; ENDMEMBER; dcl pnt like point; INVOKE(set_x) FROM (pnt) PASSING (1);;
4
Воспользуемся случаем для того, чтобы развеять широко распространенный миф: в отличие от
упомянутых выше языков, С++ с самого начала развивался как самостоятельный язык, а не препроцессорная надстройка над С [117, с.77–78]
99
INVOKE(set_y) FROM (pnt) PASSING (2);; end;
В данном примере имена макросов и используемых ими параметров набраны большими буквами. Пример начинается с подключения файла maclib, в котором реализованы все макросы, использованные в данной программе (см. Приложение 1). Затем мы определяем класс point, содержащий две координаты x и y, а также методы доступа к этим переменным. Наконец, мы объявляем переменную типа point и используем определенные нами функции set_x и set_y с некоторыми параметрами. Очевидно, что обычные универсальные средства реинжиниринга не могут учесть подобных особенностей исходных программ. В худшем случае, подобные особенности синтаксиса исходной программы исчезнут еще на стадии препроцессирования. Но даже в лучшем случае, все современные средства реинжиниринга не предоставляют программисту возможностей зафиксировать подобные знания о системе или, тем более, использовать их. Можно сформулировать несколько типов проблем, возникающих при попытке создания общей схемы анализа устаревших программ: •
Исходная программа, как правило, написана на некотором специальном подмножестве
входного
языка.
Это
подмножество
может быть плохо
характеризуемо с точки зрения формального описания, однако, тем не менее, легко распознаваемо с помощью простых эвристик. Термин "подмножество" в данном случае обозначает не столько некоторый фиксированный набор конструкций, сколько специальный способ их использования. •
Исходные тексты системы в задачах реинжиниринга очень часто бывают неполны (отсутствующие файлы, использование конкретных предположений об окружении и т.п.). В связи с этим формальный анализ систем может оказаться невозможным.
•
Исходные тексты могут содержать фрагменты, не имеющие отношения ко входному языку (например, директивы "стороннего" препроцессора), что также не дает возможность анализировать их, опираясь исключительно на синтаксис и семантику входного языка.
Таким образом, можно говорить о том, что каждый проект написан на своем собственном языке, который складывается из особенностей использованного языка, окружения программы и среды ее разработки, а также соглашений о стиле написания
100
программы, Все эти особенности как раз и представляют собой форму хранения неформальных знаний, которые и должны быть извлечены в процессе реинжиниринга. Важно понимать, что таковые знания "неформальны" не потому, что они вообще не допускают какого-либо формального представления и автоматической обработки, а в силу того, что нельзя загодя сказать, какие именно конструкции входного языка образуют новые идиомы. Таким образом, в ситуации, когда мы от задачи реинжиниринга языка переходим к задаче реинжиниринга проекта, прежде всего возникает этап извлечения информации о проекте в терминах языка проекта, то есть формулировка синтаксиса, семантики и прагматики такого способа спецификации и реализации
исходной
задачи,
который
наиболее
уместен
с
точки
зрения
рассматриваемого приложения. После этого результатом понимания устаревшей системы должно стать описание приложения в терминах извлеченного таким образом формализма.
5.3. Интерактивное извлечение языка, характерного для данного проекта Особенностью предлагаемого подхода является то, что распознавание языка проекта происходит одновременно с использованием самого этого языка. В начальный момент язык проекта – это исходный язык программирования, на котором написана программа. Затем на каждом следующем шаге мы все точнее приближаемся к языку проекта. При этом вновь найденные конструкции должны отражаться в самих средствах анализа, так как эти средства опираются на описание языка, с которым они работают. Таким образом, каждая следующая итерация анализа программы производится во все более высокоуровневых терминах и тем самым дает более точные результаты. Процесс доопределения языка заканчивается когда новые конструкции перестают находиться, хотя сам процесс изучения программы может продолжаться. Дальнейший анализ позволит нам обнаружить знания, разбросанные по всему проекту и потому плохо поддающиеся структуризации. Можно сказать, что процесс извлечения языка проекта сопровождается созданием более крупных конструкций на базе языка реализации. Этот процесс можно описать как замену одних конструкций на другие, эквивалентные им с точки зрения приложения (а не языка реализации!), и отбрасывание всей неиспользованной части языка реализации (например, неиспользуемых типов данных, функций стандартного вступления и т.д.) Язык, характерный для данного проекта, необходимо сформулировать в таком виде, который был бы наиболее удобен для дальнейшей работы с ним. Таким образом, нам необходимо решить, где мы собираемся хранить информацию о неформальных знаниях
101
– в самих исходных программах или в инструментальном средстве. Исходя из того, что эта информация будет использоваться только во время процесса реинжиниринга, было решено, что нет необходимости в полномасштабном описании проектно-специфичного языка, и достаточно будет отразить особенности этого языка непосредственно в самом средстве
реинжиниринга.
Естественно,
при
этом
приходится
смириться
с
необходимостью специальной настройки средства перед каждым новым проектом, использующим такой подход. Однако, с нашей точки зрения, это решение экономически оправданно, поскольку большинство проектов по реинжинирингу и так начинается с настройки инструментальных средств (например, для того, чтобы поддержать различные особенности того диалекта, на котором написана система), а на фоне огромного объема систем, подлежащих реинжинирингу, подобные разовые вложения становятся незначительными. Существуют и подходы, в которых вся дополнительная информация о системе записывается непосредственно в самих исходных текстах программы. Например, в работе [82] описывается методика создания так называемых "строительных лесов" (scaffolding), с помощью которых можно записать и расширения исходного языка.
5.3.1. Понимание устаревших программ и настройка инструментальных средств Знания о проектно-специфичном языке могут быть выражены в системе как минимум в двух различных формах. В первом случае мы имеем дело с синтаксическими расширениями языка. Мы уже приводили пример расширений, записанных с помощью макросов, но существуют и другие виды подобных расширений. Например, рассмотрим следующий фрагмент реального приложения на PL/I: DCL ISPLINK ENTRY EXTERNAL OPTIONS(ASM,INTER,RETCODE); DCL TABEL CHAR(08); CALL ISPLINK ('TBBOTTOM', TABEL); ... CALL ISPLINK ('CONTROL ','ERRORS
','RETURN
');
... CALL ISPLINK ('TBSORT
', TABEL, '(OPKRNR,N,D)');
В данном случае мы имеем дело с типичным примером "незамкнутого" приложения, для которого извлечение знаний в терминах языка реализации лишено смысла.
102
Используемая здесь ассемблерная функция ISPLINK представляет собой "волшебную палочку", семантику которой невозможно извлечь, опираясь только на свойства исходного языка — PL/I. В то же время довольно очевидно, что данная функция просто дополняет исходный язык некоторой дополнительной функциональностью, то есть вызов данной функции несет совершенно иную смысловую нагрузку по сравнению с вызовами других функций, хотя с точки зрения языка реализации они ничем не отличаются. Синтаксические расширения обычно локальны и легко формализуются, поэтому они могут
быть
представлены
в
виде
дополнительных
языковых
конструкций,
ортогональных исходному языку программирования. Существуют и более сложные случаи, в которых неформальные знания рассредоточены по тексту программы и не поддаются простому синтаксическому определению.
Назовем
такой
тип
неформальных
знаний
семантическими
расширениями. В частности, они могут выражаться в специальной организации потока управления, правил передачи данных от одной конструкции к другой и т.д. Естественно, подобные идиомы не удается выразить в чисто синтаксической форме (или, по крайней мере, такое представление было бы неудобным и слишком запутанным). Проиллюстрируем это следующим примером: p: proc; ... IF I<1 OR I>HBOUND(B) THEN GOTO ERR_HANDLER; ... IF SQLCODE ~= 0 & SQLCODE ~= 100 THEN GOTO END_DEL; ... END_DEL: CALL B_1001_PROCESS_ERROR; ERR_HANDLER: /* some code to handle OutOfBounds exception */ ... end p;
Понятно, что в данном фрагменте кода реализуется обработка исключительных ситуаций с помощью операторов безусловного перехода. Изучение этой и других
103
программ показало, что в данном приложении вся обработка ошибок сконцентрирована в конце соответствующих процедур. Кроме того, по данному примеру видно, что используются также и вызовы самостоятельных сторонних процедур, предназначенных для обработки ошибок. Более детальный анализ показал, что ни в одной из программ обработка ошибок не производится непосредственно в месте возникновения, поэтому можно сделать вывод, что обработка ошибок в данном приложении носит весьма структурированный характер. На самом деле, такой подход к обработке ошибок весьма типичен для устаревших языков: такие языки, как Кобол, PL/I и Basic содержат специальные конструкции вида On Error GoTo, хотя использование этих конструкций обычно ограничено некоторыми заранее определенными ситуациями (т.е. невозможно определить новую исключительную ситуацию). Полученной информацией о проекте можно воспользоваться путем уточнения наших средств реинжиниринга. К сожалению, нам не удастся записать эти знания в виде синтаксического правила (за исключением конструкции On Error GoTo), так как планируемые преобразования программ носят нелокальный характер. Со всеми процедурами, содержащими обработку ошибок, мы будем производить следующие действия: удалим проверки ошибок после всех SQL-ных операторов, а также проверки индекса перед вырезками из массива. Вместо этого мы добавим в программу новые конструкции: ExceptionToCheck в начало процедуры и ExceptionToHandle, в конец процедуры. Эти конструкции используют несколько параметров, описывающих тип исключительной ситуации, возможные условия ее возникновения и действия, необходимые для правильной обработки. Понятно, что подобные преобразования требуют использования более мощного механизма, чем простые синтаксические правила. Поэтому мы решили использовать две различные формы для хранения извлеченного проектно-зависимого языка. В первом случае мы можем просто добавить новые правила, описывающие расширения синтаксиса языка, в грамматику языка – можно сказать, что в этом случае мы имеем дело с проектно-зависимым языком в его явной форме. Во втором случае мы будем представлять новые идиомы в форме правил переписывания (rewriting rules), применяемых к промежуточному представлению программы. Такую форму записи можно назвать неявным представлением языка проекта. Естественно, второй подход позволяет описывать более широкий класс конструкций, но при этом требует больших усилий. В обоих случаях, процесс формализации языка проекта неизбежно носит характер ручного труда, так как невозможно предсказать заранее, какие конструкции исходного языка составят новые идиомы. Таким образом, процесс "расширения" языка сильно
104
зависит от программистов, занимающихся нахождением и записью подобных правил для устаревшей системы. Тем не менее, процесс настройки средства реинжиниринга на данный проект представляется нам вполне простым, особенно по сравнению со сложностью создания самого средства реинжиниринга и сложностью обычного проекта по реинжинирингу устаревшей системы. Кроме того, процесс настройки носит чисто технический характер
и
требует
знания
типичных
приемов
написания
компиляторов
и
реинжиниринга. Например, синтаксические расширения добавляются в анализатор путем добавления новых конструкций в грамматику исходного языка или расширения уже имеющихся. Этот процесс очень похож на реализацию поддержки нового диалекта в существующем анализаторе, поддерживающем стандартную версию языка. Одной из наиболее сложных проблем, возникающих на этом пути, обычно является разрешение конфликтов, появляющихся после добавления новых синтаксических правил в грамматику. Однако в нашем случае такой проблемы не возникло, так как синтаксический анализатор PL/I был написан методом рекурсивного спуска. Такой метод написания анализатора был выбран из-за того, что PL/I содержит целый ряд особенностей, которые не могут быть должным образом поддержаны традиционными средствами построения синтаксических анализаторов, таких как YACC: •
Для успешного разбора некоторых конструкций PL/I требуется потенциально бесконечное заглядывание вперед, невозможное в анализаторах, порождаемых YACC'ом
•
Другой проблемной особенностью PL/I является использование ключевых слов как идентификаторов
Более подробно проблемы реализации анализатора языка PL/I изложены в статье [104]. Конечно же, анализатор, написанный методом рекурсивного спуска, значительно проще уточнять и дополнять, чем порожденный YACC'ом. Однако и в обобщенном случае, существуют различные механизмов описания и модификации синтаксиса, которые могут быть использованы для быстрой поддержки новой функциональности в проектно-зависимом языке. Например, в статье [18] предлагается гибкая схема, позволяющая легко уточнять анализаторы устаревших языков. Описанный там подход был разработан для упрощения поддержки новых диалектов в анализаторе, но его можно использовать и в нашем случае. Переписывание термов также может считаться вполне устоявшейся технической методикой, хотя описание методов, используемых в данной области, выходит за рамки
105
данной диссертации. Сошлемся на работу [56], содержащую подробное изложение этого вопроса.
5.3.2. Уточненная схема процесса извлечения языка проекта Итак, теперь мы можем описать процесс извлечения языка в более общей схеме (см. рис. 11).
Syntax extensions
Rewrite rules
extracted constructions Frontend customization
Customized frontend
Pass of language converter
Rewriter
IR1
Legacy system
IR2
Рис. 11. Схема извлечения проектно-зависимого языка
Создание проектно-зависимого языка производится в несколько этапов. Вначале производится настройка front-end'а, во время которой мы расширяем грамматику путем добавления новых синтаксических правил. Применение этого уточненного front-end'а дает нам исходное внутреннее представление (IR1), в котором все идиомы, допускающие запись в виде синтаксического правила, уже свернуты. Затем это внутреннее представление подается на вход просмотру переписывания термов, завершающему
описание
языка
проекта.
Результат
переписывания
термов,
обозначенный IR2, и есть окончательное внутреннее представление проекта в терминах его собственного языка. Все дальнейшие работы по пониманию программ understanding)
или
преобразованию
системы
(language
conversion)
(legacy должны
основываться именно на этом внутреннем представлении. Заметим, что с теоретической точки зрения можно выразить все особенности языка, в том числе и синтаксические, на этапе переписывания термов и тем самым
106
совершенно избавиться от процесса настраивания анализатора, но по нашему мнению, это только усложнит процесс.
5.3.3. Настраиваемая генерация Формализация языка проекта и сохранение знаний в средстве реинжиниринга не имеет большого смысла, если мы не планируем воспользоваться этим. В данном разделе мы покажем, как знания о языке проекта могут быть использованы в процессе преобразования исходного приложения на новые языки программирования. Как обсуждалось в главе 2, одной из основных задач при преобразовании языков является создание стратегии перевода для всех конструкций исходного языка. Обычно такой список проекций раз и навеки фиксируется для любого набора входных и выходных языков. Понятно, что при таком подходе невозможно учесть какие-либо соглашения, характерные для данного проекта, или обнаружить использование какихто надстроек над исходным языком, так как набор конструкций, которым оперирует языковый конвертор, сводится ко встроенным конструкциям исходного языка. Это кажется естественным, но при таком подходе мы чаще всего "измельчаем" смысл программы, опуская его на уровень исходного языка. Вместо этого мы можем попробовать настроить генерацию целевого языка на основе полученных знаний о языке проекта. Вполне возможно, что для "более крупных" конструкций языка проекта нам удастся найти более точные эквиваленты и в целевом языке, особенно, если целевой язык обладает более богатым набором конструкций (например, целевой язык может поддерживать объекты, сложные типы данных или просто содержать какие-то полезные функции в своем стандартном окружении). В целях иллюстрации приведенных выше соображений вернемся к примеру, рассмотренному на стр. 101–102. Предположим, что во время работ по пониманию программ мы увидели, что язык проекта является объектно-ориентированным, и отразили это знание в синтаксическом анализаторе. В таком случае результат конвертации в Java будет обладать всеми чертами "родного" (native) Java-кода: class Point { private int x, y; public int get_x()
{ return x;
}
public int get_y()
{ return y;
}
public void set_x(int value) { x = value; } public void set_y(int value) { y = value; } }
107
class p { public static void main(String args[]) { Point pnt = new Point(); pnt.set_x(1); pnt.set_y(2); System.out.println("(" + pnt.get_x() + "," + pnt.get_y() + ")"); } }
В то же время "дословная" генерация (транслитерация) исходной программы в Java приведет к коду, в котором несущественные технические детали исходной программы до неузнаваемости затемнят ее смысл. Похожего результата можно достигнуть и в примере с обработкой ошибок. Общая идея преобразования в этом примере ясна из следующего скелета программы: class p { public static void main(String[] args) { try { ... /* statements such as IF I<1 ... and IF SQLCODE <> 0 simply disappeared */ } catch (SQLException e) { ... } catch (ArrayIndexOutOfBoundsException a) { ... } } }
Таким образом, в результате наших работ по настройке средства реинжиниринга, нам удалось сгенерировать программы, наиболее естественно отражающие идиомы, эмулированные в исходном языке, но являющиеся встроенными в целевом языке. Из этого можно также сделать следующий вывод – в том случае, когда мы заранее знаем, в какой язык мы собираемся перевести устаревшую систему, мы можем специально искать в исходных программах шаблоны конструкций, имеющих удобное представление в целевом языке. Но, конечно же, мы не должны ограничиваться в наших поисках только этим сценарием.
5.4. Обсуждение При оценке применимости предложенного подхода, наибольшее опасение вызывает соотношение затрат на извлечение проектно-зависимого языка к преимуществам, получаемым при их использовании. В нашем случае значительные преимущества в
108
качестве генерируемого кода перевесили стоимость уточнения средств реинжиниринга. В то же время необходимо признать, что предположение, неявно подразумеваемое в нашем подходе – а именно, что программисты могут изменять само средство реинжиниринга в процессе преобразования устаревшей системы – является весьма сильным и может выполняться не во всех проектах по реинжинирингу. Однако, как мы уже подчеркивали в разделе 1.2.4, специфика задач реинжиниринга такова, что в большинстве проектов по преобразованию устаревших систем все равно приходится изменять сами инструментальные средства, поэтому наше предположение кажется вполне оправданным. Тем не менее, мы не ожидаем, что подход, изложенный в данной главе, будет эффективным во всех случаях. Более того, для небольшого по объемам приложения стоимость настройки средств реинжиниринга на проект может оказаться чрезмерно высокой и потому данный подход окажется неприменимым. Однако необходимо отметить, что наиболее сложной проблемой при реинжиниринге обычно является именно величина приложения, подлежащего преобразованию. Одна из особенностей предложенного подхода заключается в его хорошей масштабируемости: чем больше объем исходного приложения, тем более эффективен такой подход, так как в этом случае время, потраченное на уточнение инструментальных средств, может быть впоследствии более чем скомпенсировано во время последующего применения этих средств. Например, после написания этой главы у нас появилась возможность оценить применимость
нашего
подхода
на
примере
крупной
устаревшей
системы,
насчитывающей около двух миллионов строк кода на PL/I. Внимательное изучение этой системы показало, что бóльшая часть кода в данном приложении была получена путем препроцессирования каких-то шаблонов. Естественно, такое наблюдение целиком изменило сам подход к решению проблемы, так как в этом случае исходные шаблоны могут предоставить значительно больше информации, чем результаты их "расшивки" в обычный код. Таким образом, после определения структуры этих шаблонов, мы можем сразу же добавить их в язык данного проекта и применить полученные определения во время последующего преобразования системы в новый язык программирования. Другое возражение к предложенному подходу заключается в том, что в промышленных системах, написанных большим коллективом без четкой дисциплины разработки, может не найтись устойчивых и общеупотребительных шаблонов. Действительно, нам приходилось сталкиваться с "лоскутными" приложениями, в которых последовательности конструкций, пригодные для выделения в язык проекта, были беспорядочно разбросаны по исходным текстам.
109
Указанная проблема действительно сложна, но по нашему мнению, с этой проблемой также можно успешно бороться. В таких случаях необходимо начать с предварительной
реструктуризации
приложения
с
целью
выделения
часто
встречающихся конструкций. Однако, для подобных приложений реструктуризация все равно является необходимой частью процесса трансформации. Кроме того, даже в таких неструктурированных системах мы можем найти полезные шаблоны, только в этом случае шаблоны будут более сложными и, скорее всего, не смогут быть выражены в виде простых синтаксических расширений. В качестве стороннего замечания отметим, что практически любые приемы реинжиниринга
значительно
труднее
применить
именно
к
подобным
неструктурированным системам. Можно утверждать, что все техники реинжиниринга значительно успешнее работают именно на программах, написанных в хорошем стиле программирования. Даже реструктуризация программ, которая теоретически должна успешно работать с любыми входными данными, может производить на свет более или менее удачные результаты в зависимости от входной программы. Наконец,
для
дальнейшего
развития
предложенного
подхода
необходимо
значительно улучшить инструментальные средства поддержки процесса построения языка проекта. В частности, необходимы средства для проверки гипотез о тех или иных наборах конструкций. Например, в примере на стр. 101-102 гипотезами были попытки трактовать некоторые описанные в программе данные как внутренние данные объектов, а функции – как их функции-члены. При конвертации этого примера мы просто приняли эти гипотезы на веру, однако в общем случае хотелось бы убедиться, что объектно-ориентированная семантика действительно вполне поддержана в данном приложении. В идеале, проверка подобных гипотез может быть (частично) автоматизирована, хотя формальная проверка гипотезы может оказаться весьма дорогостоящей [99]. На данный момент этот этап поддержан только штатными средствами понимания программ, имеющимися в RescueWare (например, средствами построения диаграмм и средствами навигации по программе [106]).
5.5. Заключение В
данной главе описывается методика настройки средств реинжиниринга на
конкретные проекты, основанная на использовании неформальных знаний о системе. Подобные неформальные знания не могут быть извлечены автоматически, но после выявления человеком они могут быть записаны, формализованы и в дальнейшем использованы в процессе преобразования языков.
110
В качестве основного инструмента настройки предлагается использовать знания о регулярном использовании некоторых последовательностей исходного языка в качестве более крупных шаблонов. Такие использования определяют язык данного проекта. После определения языка проекта выполняется настройка средства реинжиниринга на данный проект, и процесс преобразования языков продолжается. Предложенная методология иллюстрируется на примерах из промышленного проекта по преобразованию приложения из PL/I в Java. Обсуждается применимость предложенной методологии, в том числе, в промышленных проектах и в проектах, содержащих слабоструктурированные программы. В заключение диссертации, приведем общую и уточненную схему методологии реинжиниринга, предложенной автором в данной диссертации (см. рис. 12): Реструктуризация
Реструктуризация
Оригинальная программа
Целевая программа Уточненное внутреннее представление
Внутреннее представление
Анализ
Формализация языка проекта
Генерация кода
Выделение объектов
Рис. 12. Окончательный вариант методологии реинжиниринга, предложенной в диссертации
Предложенная
методология
реинжиниринга
состоит
из
следующей
последовательности шагов: 1. Анализ исходных программ, реструктуризация в терминах исходного языка и построение промежуточного представления (см. главу 2) 2. Настройка средства реинжиниринга на данный проект (см. главу 5). 3. Предварительная структуризация программ, включающая в себя выделение процедур, локализацию или уничтожение операторов безусловного перехода, локализацию данных и некоторые оптимизирующих преобразований (см. главу 4). 4. Выделение классов на основе пользовательской информации или различных эвристик (см. главу 4).
111
5. Генерация программ на целевых объектно-ориентированных языках (см. главу 2). Отметим, что этапы 2 и 4 являются необязательными. Эти шаги могут выполняться для улучшения качества порождаемого кода. Все
этапы
предложенной
автором
методологии
были
реализованы
в
инструментальном средстве реинжиниринга RescueWare и были успешно применены на промышленных проектах по преобразованию устаревших систем (см. главы 3 и 5).
112
Литература 1. H. Agrawal, J. R. Horgan "Dynamic Program Slicing", In Proceedings of the ACM SIGPLAN Conference on Programming Language Design and Implementation, New York, June 1990, pp. 246–256. 2. R. S. Arnold (ed.) "Software Reengineering", IEEE Computer Society Press, 1993, 676 pp. 3. R. S. Arnold "Software Restructuring", Proceedings of IEEE, Vol. 77, No. 4, April 1989, pp. 607-617. 4. R. S. Arnold, W. B. Frakes "Software Reuse and Reengineering", In [2], pp. 476–484. 5. E. Ashcroft, Z. Manna "The translation of 'goto' programs in 'while' programs", Proceedings of the 1971 IFIP Congress, Amsterdam, pp. 250-260 6. C. Babcock "Restructuring eases maintenance", Computerworld, November 1987, pp. 19, 22. 7. D. Baburin "Using Graph Representations in Reengineering", In Proceedings of the 6th Conference on Software Maintenance and Reengineering, Budapest, Hungary, March 2002. 8. C. Bachmann "A CASE for Reverse Engineering", Datamation, July 1988 9. J. W. Bailey, V. R. Basili "Software Reclamation: Improving Post-Development Reusability", In Proceedings of the 8th National Conference on Ada Technology, US Army Communications-Electronics Command, Fort Monmouth, N.J., 1990, pp. 477–480 and 489–499. 10. B. S. Baker "An Algorithm for Structuring Flowgraphs", Journal of the ACM, Vol. 24, No. 1, 1977, pp. 98–120. 11. K.H. Bennett "Automated support of software maintenance", Information and Software Technology, Vol. 33, No. 1, 1991, pp. 74–85. 12. T.J. Biggerstaff, B.G. Mitbander, D.E. Webster "Program Understanding and the Concept Assignment Problem", Communications of the ACM, May 1994, pp. 72–82. 13. B. W. Binkley, M. Harman, L. R. Raszewski, C. Smith "An Empirical Study of Amorphous Slicing as a Program Comprehension Support Tool", In Proceedings of the 8th IEEE International Conference on Program Comprehension, Limerick, Ireland, June 2000, pp. 161-170. 14. B. Boehm "Software Engineering Economics", Englewood Cliffs, N.J., Prentice-Hall, 1981. 15. B. Boehm et al. "A Software Development Environment for Improving Productivity", Computer, June 1984, pp. 30-44.
113
16. C. Bohm, G. Jacopini "Flow diagrams, Turing machines and languages with only two formation rules", Communications of the ACM, Vol. 9, No. 5, May 1966, pp. 366-371. 17. D. Boulychev, D. Koznov, A.A. Terekhov "On Project-Specific Languages and Their Application in Reengineering", In Proceedings of the Sixth European Conference on Software Maintenance and Reengineering, Budapest, Hungary, March 2002, pp. 177– 185. 18. M.G.J. van der Brand, M.P.A. Sellink, C. Verhoef "Current Parsing Techniques in Software Renovation Considered Harmful", In Proceedings of the International Workshop on Program Comprehension, Ischia, Italy, 1998, pp. 108–117. 19. M.G.J. van den Brand, M.P.A. Sellink, C. Verhoef "Generation of Components for Software Renovation Factories from Context-Free Grammars", Science of Computer Programming, Vol. 36, No. 2–3, Mar. 2000, pp. 209–266. 20. M. L. Brodie, M. Stonebraker "Migrating Legacy Systems: Gateways, Interfaces and the Incremental Approach", Morgan-Kaufmann, 1995, 210 pp. 21. G. Caldiera, V. R. Basili "Identifying and Qualifying Reusable Software Components", Computer, Vol. 24, No. 2, February 1991, pp. 61-70. 22. F.W. Calliss "Problems with Automatic Restructurers", ACM SIGPLAN Notices, Vol. 23, No. 3, Mar. 1988, pp. 13–23. 23. G. Canfora, A. Cimitile, A. De Lucia "Conditioned program slicing", In Information and Software Technology Special Issue on Program Slicing, Elsevier-Science B.V., Vol. 40, 1998, pp. 595–607. 24. C. Cerf and V. Navasky "The Experts Speak — The Definitive Compendium of Authoritative Misinformation", Villard Books, New York, 1998 25. Y. Chae and S. Rogers "Successful COBOL Upgrades: Highlights and Programming Techniques", John Wiley and Sons, New York, 1999, 288 pp. 26. E. Chikosfky "CASE and Reengineering: From Archeology to Software Perestroika", Proceedings of the 12th International Conference on Software Engineering, 1990, p. 122 27. E. Chikofsky, J. H. Cross II "Reverse Engineering and Design Recovery: A Taxonomy", IEEE Software, Vol. 7, No. 1, January 1990, pp. 13–17. 28. C. Cifuentes, K.J. Gough "Decompilation of Binary Programs", Software: Practice and Experience, Vol. 25, No. 7, July 1995, pp. 811–829. 29. C. Cifuentes, D. Simon, A. Fraboulet "Assembly to High-Level Language Translation", In Proceedings of the International Conference on Software Maintenance, Washington DC, November 1998, pp 228–237. 30. "COBOL/370 Migration Guide", release 1, IBM Corp., Armonk, N.Y., 1992.
114
31. I. Claßen, K. Hennig, I. Mohr, M. Schulz "CUI to GUI Migration: Static Analysis of Character-Based Panels", Proceedings of the 1st Euromicro Working Conference on Software Maintenance and Reengineering, Berlin, 1997. 32. B. Cox "Object-Oriented Programming: An Evolutionary Approach", Addison-Wesley, Reading, MA. 1976. 33. M. A. Cusumano, R. W. Selby "Microsoft Secrets", Simon & Schuster, New York, 1998, 512 pp. 34. T. DeMarco, T. Lister "Peopleware – Productive Projects and Teams", Dorset House, New York, 1987, p. 30. 35. W. C. Dietrich jr., L. R. Hackman, F. Gracer "Saving a Legacy with Objects", Proceedings of the Conference on Object-Oriented Programming Systems, Languages, and Applications (OOPSLA'89), pp. 77-83. 36. E. Dijkstra "Go to statement considered harmful", Communications of the ACM, vol. 11, no. 3, pp. 147-148, March 1968 37. M. F. Dunn, J. C. Knight "Software Reuse in an Industrial Setting: a Case Study", In Proceedings of the International Conference on Software Engineering, 1991, pp. 329– 338. 38. S. G. Eick, T. L. Graves, A. F. Karr, J. S. Marron, A. Mockus "Does Code Decay? Assessing the Evidence from Change Management Data", IEEE Transactions on Software Engineering, Vol. 27, No. 1, January 2001, pp. 1–12. 39. L. H. Etzkorn, C. G. Davis "Automatically Identifying Reusable OO Legacy Code", Computer, October 1997. P. 66–71. 40. D. P. Freedman, G. M. Weinberg "Handbook of Walkthroughs, Inspections and Technical Reviews", Dorset House, 3rd edition, 1990 41. E. S. Garnett, J. A. Mariani "Software Reclamation", Sofware Engineering Journal, May 1990, pp. 185-191 42. R.L. Glass "Computing Calamities—Lessons Learned from Products, Projects, and Companies That Failed", Prentice Hall, Englewood Cliffs, N.J., 1999, pp. 190–191. 43. R. L. Glass, R. A. Noiseux "Software Maintenance Guidebook", Englewood Cliffs, NJ: Prentice-Hall, 1981 44. R. Gray, T. Bickmore, S. Williams, “Reengineering Cobol Systems to Ada,” Proc. Seventh Annual Air Force/Army/Navy Software Technology Conf., US Dept. of Defense, Hill Air Force Base, 1995. 45. M. Harman, S. Danicic "Amorphous Program Slicing", In Proceedings of the 5th IEEE International Workshop on Program Comprehension, Dearborn, Michigan, USA, May 1997, pp. 70-79. 46. M. A. Jackson "Principles of Program Design", Academic Press, London, 1975
115
47. I. Jacobson, F. Lindström, "Re-engineering of Old Systems to an Object-Oriented Architecture", In Proceedings of the Conference on Object-Oriented Programming Systems, Languages and Applications (OOPSLA'91), ACM, New York, 1991, pp. 340– 350. 48. Y. Jang "Legacy Systems and Object Technology Workshop Summary", Addendum to the Proceedings of the Conference on Object-Oriented Programming Systems, Languages and Applications (OOPSLA'95), ACM, New York, 1995, pp. 176–179. 49. C. Jones "Programming Productivity: Issues for the Eighties", 2nd edition, Los Angeles, IEEE Computer Society Press, 1986. 50. C. Jones "Estimating Software Costs", McGraw-Hill, 1998. 51. C. Jones “Applied Software Measurement”, N.Y.: McGraw-Hill, 1991. 52. C. Jones "Assessment and Control of Software Risks", Prentice Hall, Englewood Cliffs, N.J., 1994. 53. C. Jones "The Year 2000 Software Problem – Quantifying the Costs and Assessing the Consequences", Addison-Wesley, 1998 54. B. Kernighan, P. J. Plauger "Software Tools", Addison-Wesley, Reading, MA. 1976. 338 pp. 55. W.M. Klein, OldBOL to NewBOL: A COBOL Migration Tutorial for IBM, Merant Publishing, 1998. 56. J. W. Klop "Term rewriting systems", In Handbook of Logic in Computer Science, Vol. II, Oxford University Press, 1992, p. 1–116. 57. K. Kontogiannis et al., “Code Migration through Transformations: An Experience Report,” Proc. IBM Center for Advanced Studies Conf. (CASCON ’98), IBM, Armonk, NY, 1998, pp. 1–12. Available at www.swen.uwaterloo.ca/~kostas/migration98.ps. 58. B. Korel, J. Laski "Dynamic Program Slicing", Information Processing Letters, Vol. 29, No. 3, October 1988, pp. 155–163. 59. W. Kozaczynski,
J. Q. Ning,
A. Engberts
"Program
Concept
Recognition
and
Transformation", IEEE Transactions on Software Engineering, Vol. 18, No. 12, December 1992, pp. 1065–1075. 60. W. Kozaczynski,
J. Q. Ning
"Automated
Program
Understanding
by
Concept
Recognition", Automated Software Engineering Journal, 1(1): 61--78, March 1994. 61. R. Lämmel, C. Verhoef "Cracking the 500 Language Problem", IEEE Software, Vol. 18, No. 6, pp. 78–88. 62. F. Lehner "Software Life Cycle Management Based on a Method for Phase Distinction", Euromicro Journal, August 1991, pp. 603–608. 63. B. Lientz, E. B. Swanson “Software Maintenance Management”, Addison-Wesley, 1980.
116
64. B. Lientz, E. B. Swanson, G. E. Tompkins "Characteristics of application software maintenance", Communications of the ACM, Vol. 21, No. 6, 1978, pp. 466–471. 65. A. de Lucia "Program Slicing: Methods and Applications", In Proceedings of the 1st IEEE International Workshop on Source Code Analysis and Manipulations, Florence, Italy, 2001, pp. 142-149. 66. Z.-Y. Liu, M. Ballantyne, L. Seward, "An Assistant for Re-Engineering Legacy Systems", In Proceedings of the 6th Conference on Innovative Applications of Artificial Intelligence, 1994. P. 95–102. 67. A. J. Malton "The Migration Barbell", First ASERC Workshop on Software Architecture, August 2001, http://www.cs.ualberta.ca/~kenw/conf/awsa2001/papers/malton.pdf 68. J. Martin, H. A. Müller "C to Java Migration Experiences", In Proceedings of the Sixth European Conference on Software Maintenance and Reengineering, Budapest, Hungary, March 2002, pp. 143–153. 69. H. W. Miller "Reengineering Legacy Software Systems", Digital Press, 1997, 280 pp. 70. J.C. Miller, B.M. Strauss "Implications of Automated Restructuring of COBOL", ACM SIGPLAN Notices, Vol. 22, No. 6, June 1987, pp. 76–82. 71. S. Oualline, Practical C Programming, 3rd ed., O’Reilly & Assoc., Cambridge, Mass., 1997. 72. D.L. Parnas "Software Aging", In Proceedings of the 16th International Conference on Software Engineering, May 1994, pp. 279 287. 73. W. Polak, L.D. Nelson, T.W. Bickmore "Reengineering IMS databases to relational systems", In Proceedings of the 7th Annual Air Force/Army/Navy Software Technology Conference, Salt Lake City, April 1995. 74. A. Quilici "A Memory-Based Approach to Recognizing Programming Plans", Communications of the ACM, Vol. 37, No. 5, May 1994, pp. 84–93. 75. A. Quilici, S. Woods, Y. Zhang "Program Plan Matching: Experiments with a ConstraintBased Approach", Science of Computer Programming, Vol. 36, 2000, pp. 285—302 76. V. Rajlich, K. H. Bennett "A Staged Model for the Software Life Cycle", Computer, Vol. 33, No. 7, July 2000, pp. 66–71. 77. C. Rich, R. C. Waters "Programmer's Apprentice", ACM Press, 1990. 238 pp. 78. C. Rich, L. M. Wills "Recognizing a Program's Design: A Graph-Parsing Approach", IEEE Software, Vol. 7, No. 1, January 1990, pp. 82–89. 79. N. F. Schneidewind "The State of Software Maintenance", IEEE Transactions on Software Engineering, Vol. 13, No. 3, 1987, pp. 303–310. 80. M.P.A. Sellink, H.M. Sneed, and C. Verhoef, “Restructuring of COBOL/CICS Legacy Systems,” Proc. Third European Conf. Maintenance and Reengineering, IEEE Computer Soc. Press, Los Alamitos, Calif., 1999, pp. 72–82.
117
81. A. Sellink, C. Verhoef "Native Patterns", In Proceedings of the 5th IEEE Working Conference on Reverse Engineering, Honolulu, Hawaii, USA, 1998. 82. A. Sellink, C. Verhoef "Scaffolding for Software Renovation", In Proceedings of the 4th Conference on Software Maintenance and Reengineering, Zurich, Switzerland, 2000, pp. 151–160. 83. M. C. Smith, D. E. Mularz, T. J. Smith "CASE Tools Supporting Ada Reverse Engineering: State of the Practice", Proceedings of the Eighth Annual National Conference on Ada Technology, 1990, pp. 157-164 84. H. M. Sneed "Economics of Software Re-engineering", Journal of Software Maintenance: Research and Practice, Vol. 3, No. 3, Sept. 1991, pp. 163–182. 85. H. M. Sneed "Planning the Reengineering of Legacy Systems", IEEE Software, January 1995, Vol. 12, No. 1, pp. 24–34. 86. H. M. Sneed "Reverse Engineering as a Bridge to CASE", Proceedings of the Seventh International Workshop on Computer-Aided Software Engineering (CASE'95). P. 304317 87. H. M. Sneed, Objektorientierte Softwaremigration [Object-Oriented Software Migration], Addison Wesley Longman, Bonn, Germany, 1998. 88. H. M. Sneed "Risks Involved in Reengineering Projects", Proceedings of WCRE, 1999, IEEE Computer Society Press, Atlanta, October 1999, pp. 204–212. 89. H. M. Sneed, C. Verhoef "Reengineering the Corporation – A Manifesto for IT Evolution", Encyclopedia of Software Engineering, 2001 (pages ??). 90. A. A. Terekhov "Automated Extraction of Classes from Legacy Systems", In Proceedings of TOOLS EE, 2000. 91. A. A. Terekhov "Automating Language Conversion: A Case Study", In Proceedings of the IEEE International Conference on Software Maintenance, 2001. P. 654–658. 92. A. A. Terekhov, C. Verhoef "The Realities of Language Conversions", IEEE Software, November/December 2000, Vol. 17, No. 6, pp. 111–124. 93. B. Toeter "Reuse of ABN-AMRO PowerBuilder Applications", M.Sc. thesis, University of Amsterdam, July 2001, 63 pp. 94. W. Ulrich "The evolutionary growth of reengineering and the decade ahead", American Programmer, Vol. 3, No. 11, pp. 14–20. 95. US National Bureau of Standards "Guidance on software maintenance", Special Publication No. 500-106, Washington, DC, 1983. 96. C. Verhoef "Software Development is a Special Case of Maintenance", In Proceedings of the 3rd Annual Conference on Software Engineering and Applications, Scottsdale, Arizona, USA, October 1999.
118
97. VS COBOL II. Application Programming Language Reference, 4th ed., IBM Corp., Armonk, N.Y., 1993 98. R. C. Waters "Program Translation via Abstraction and Reimplementation", IEEE Transactions on Software Engineering, Vol. SE-14, No. 8, August 1988, pp. 1207–1228. 99. B. W. Weide, W. D. Heyrn "Reverse Engineering of Legacy Code Exposed", In Proceedings of International Conference on Software Engineering, Seattle, WA, 1995, pp. 327--331. 100.
M. Weiser "Program Slicing", IEEE Transactions on Software Engineering, Vol. 10,
No. 4, 1984, pp. 352-357. 101.
R. Widmer, COBOL Migration Planning, Edge Information Group, 1998.
102.
K. Yasumatsu and N. Doi, “SPiCE: A System for Translating Smalltalk Programs
Into a C Environment,” IEEE Transactions on Software Engineering, Vol. 21, No. 11, 1995, pp. 902–912. 103.
S. Yau, J.S. Collofello, T. MacGregor “Ripple effect analysis of software
maintenance” // Proc. IEEE COMPSAC, 1978. P. 492-497. 104.
А. В. Береснева, А. А. Терехов "Анализ языка PL/I в системе RescueWare", в сб.
"Автоматизированный реинжиниринг программ", СПб, изд-во С.-Петербургского университета, 2000. С. 268–277. 105.
Ф. П. Брукс-мл. "Мифический человеко-месяц, или как создаются программные
системы", 2-е изд., СПб.: Символ-плюс, 1999, 304 стр. 106.
М.А. Бульонков, Д.Е. Бабурин "HyperCode – открытая система визуализации
программ", в сб. "Автоматизированный реинжиниринг программ", СПб, изд-во С.Петербургского университета, 2000. С. 165–183. 107.
Г. Буч "Объектно-ориентированный анализ и проектирование с примерами
приложений на C++", 2-е издание, М.: "Издательство Бином", СПб.: "Невский диалект", 1999. 560 с. 108.
А. В. Друнин "Построение срезов программ в задачах реинжиниринга", в сб.
"Автоматизированный реинжиниринг программ", СПб, изд-во С.-Петербургского университета, 2000. С. 184–205. 109.
А.П. Ершов "Организация АЛЬФА-транслятора", в сб. "АЛЬФА — система
автоматизации программирования", Сиб.отд.изд-ва “Наука” – Новосибирск, 1967 110.
Б.
Казанский
"Генерация
программ
на
целевых
языках
в
задачах
реинжиниринга", в сб. "Автоматизированный реинжиниринг программ", СПб, издво С.-Петербургского университета, 2000. С. 118–144. 111.
В. Е. Каменский, А. В. Климов, С. Г. Манжелей, Л. Б. Соловская "Применение
стандарта CORBA для унаследованных систем", в сб. "Вопросы кибернетики. Приложения системного программирования", выпуск 3, Москва, 1997
119
112.
С. В. Кукс "Алгоритм анализа потоков данных", в сб. "Автоматизированный
реинжиниринг программ", СПб, изд-во С.-Петербургского университета, 2000. С. 110–117. 113.
М.
Мосиенко
"Построение
динамической
поддержки
для
задач
реинжиниринга", в сб. "Автоматизированный реинжиниринг программ", СПб, издво С.-Петербургского университета, 2000. С. 145–164. 114.
О. А. Плисс, К. Д. Волошин "Трансляция вызовов и переходов из Кобола в
C++", в сб. "Автоматизированный реинжиниринг программ", СПб, изд-во С.Петербургского университета, 2000. С. 83–102. 115.
О. А. Плисс, К. Д. Волошин "Устранение локальных GOTO", в сб.
"Автоматизированный реинжиниринг программ", СПб, изд-во С.-Петербургского университета, 2000. С. 103–109. 116.
М. В. Попов "Преобразование программы к структурной на основе метода
расклейки графа управления", дипломная работа, математико-механический факультет СПбГУ, 2000. 32 с. 117.
Б. Страуструп "Дизайн и эволюция языка С++", М.: ДМК Пресс, 2000. 448 с.
118.
A. А. Терехов "Автоматизированное разбиение устаревших программ на
классы", в сб. "Автоматизированный реинжиниринг программ", СПб, изд-во С.Петербургского университета, 2000. С. 229–250. 119.
А. А. Терехов, К. Верхуф "Проблемы языковых преобразований", Открытые
системы, №5-6, 2001, с. 54–62. 120.
А. Н. Терехов, А. А. Терехов "Перенос приложений и проблема 2000 года",
Компьютер-Пресс, №8, 1998, с. 92–96. 121.
А. Н. Терехов, А. А. Терехов (ред.) "Автоматизированный реинжиниринг
программ", СПб, изд-во С.-Петербургского университета, 2000, 332 с. 122.
А. Н. Терехов, Л. А. Эрлих, А. А. Терехов "Перспективы реинжиниринга",
Компьютер-Пресс, №8, 1999. 123.
А. Н. Терехов, Л. А. Эрлих, А. А. Терехов "История и архитектура проекта
RescueWare", в сб. "Автоматизированный реинжиниринг программ", СПб, изд-во С.-Петербургского университета, 2000. С. 7-19.
120
Приложение 1. Пример реализации объектноориентированного макрорасширения PL/I Данное приложение содержит пример реализации элементарного объектноориентированного макрорасширения языка PL/I, использованного в примере 1. Мы приводим эти технические детали исключительно для того, чтобы показать осуществимость данного приема на практике. %PROPERTY: proc (NAME, AS) statement returns (char); dcl (NAME, AS) char; return ('2 ' || NAME || ' ' || AS || ','); %end; %MEMBER: proc (NAME, OF, TAKES, RETURNS) statement returns (char); dcl (NAME, OF, TAKES, RETURNS) char; dcl parmset
builtin;
dcl parmlist char; parmlist = '_this'; if parmset(takes) & takes ^= '' then parmlist = parmlist || ', ' || TAKES; return ( NAME || ': proc (' || parmlist || ') returns (' || RETURNS || ');' || 'dcl _this pointer, _data like ' || OF || ' based (_this); ' ); %end; %ENDMEMBER: proc statement returns (char); return ('end;'); %end; %CLASS: proc (NAME) statement returns (char); dcl NAME char, classname char; return ('dcl 1 ' || NAME || ','); %end;
121
%ENDCLASS: proc statement returns (char); return (';'); %end; %INVOKE: proc (NAME, FROM, PASSING) statement returns (char); dcl (NAME, FROM, PASSING) char; if PASSING ^= '' then PASSING = ', ' || PASSING; return ('CALL ' || NAME || '(ADDR(' || FROM || ')' || PASSING || ')'); %end; %dcl THIS char; %THIS='_this->_data'; %act PROPERTY, CLASS, ENDCLASS, MEMBER, ENDMEMBER, THIS, INVOKE norescan;
122
Приложение 2. Результат извлечения классов в процессе переноса программы с Кобола на С++ PhoneBook.cpp // This file was generated using RescueWare(R)TM version 3.00 // Copyright(C) 1997 Relativity Technologies, Inc. // Generated on Mon May 11 16:47:24 1998 // // File: PhoneBook.cpp // Description: // Revision: // Date: // Author: Alexandre Drunin #include "StdAfx.h" #include "PhoneBook.h" AccessMethodsInstance_struct AccessMethodsInstance (0); MainInstance_struct MainInstance (12); void AccessMethodsInstance_struct :: Search_Number( void ) { Display( "What room do you want to call to?" ); Input( Room ); { SQLHSTMT Default; ODBCAllocStmt( Default ); unsigned char szQuery[] = "SELECT ", "NUM FROM ", "PHONEBOOK WHERE ROOM = ?"; ODBCPrepare( Default, szQuery ); ODBCBindParameter(Default, 1, ODBCColType("PHONEBOOK", "ROOM" ), Room ); ODBCExecute( Default ); ODBCBindCol( Default, 1, Phone ); ODBCFetch( Default ); ODBC_EXPORT_DATA( Phone ); ODBCFreeStmt( Default ); } Display( Phone.ToString() ); return; } void AccessMethodsInstance_struct :: Add_Number( void ) {
123
Display( "What room do you want to add phone number for?" ); Input( Room ); Display( "Enter phone number" ); Input( Phone ); { SQLHSTMT Default; ODBCAllocStmt( Default ); unsigned char szQuery[] = "INSERT INTO PHONEBOOK (" "ROOM, " "NUM )
VALUES ( "
"?, " "? )"; ODBCPrepare( Default, szQuery ); ODBCBindParameter(Default, 1, ODBCColType("PHONEBOOK", "ROOM" ), Room ); ODBCBindParameter(Default, 2, ODBCColType("PHONEBOOK", "NUM" ), Phone ); ODBCExecute( Default ); ODBCFreeStmt( Default ); } return; } void AccessMethodsInstance_struct :: Drop_Table( void ) { { SQLHSTMT Default; ODBCAllocStmt( Default ); unsigned char szQuery[] = "DROP TABLE PHONEBOOK"; ODBCExecDirect( Default, szQuery ); ODBCFreeStmt( Default ); } return; } void AccessMethodsInstance_struct :: Create_Table( void ) { { SQLHSTMT Default; ODBCAllocStmt( Default ); unsigned char szQuery[] = "CREATE TABLE PHONEBOOK ( " "" "ROOM NUMERIC, " "NUM NUMERIC" " ) "; ODBCExecDirect( Default, szQuery ); ODBCFreeStmt( Default );
124
} return; } void AccessMethodsInstance_struct :: Menu( void ) { Display( "Enter 0 to Create Phonebook" ); Display( "
1 to Delete Phonebook" );
Display( "
2 to Add New Number" );
Display( "
3 to Search for Number" );
Display( "
4 to Quit." );
Input( Choice ); switch (ToInt( Choice )) { case 0 : AccessMethodsInstance.Create_Table(); break; case 1 : AccessMethodsInstance.Drop_Table(); break; case 2 : AccessMethodsInstance.Add_Number(); break; case 3 : AccessMethodsInstance.Search_Number(); break; } return; } void MainInstance_struct :: Cobol_Main( void ) { { ODBCInitFunction(); ODBCConnect( "DSN" ); } Display( "Demo Program for RescueWare" ); Display( "Simple phonebook for TERKOM" ); while ( Choice != "4" ) { AccessMethodsInstance.Menu(); } ODBCDisconnect(); CICS.Return(); }
125
void Main( void ) { MainInstance.Cobol_Main(); }
126
Приложение 3. Список иллюстраций Рис. 1. Варианты развития устаревшей системы................................................................ 15 Рис. 2. Зависимость производительности разработки и сопровождения от размера системы ........................................................................................................................... 17 Рис. 3. Общая схема этапной модели жизненного цикла программ................................. 29 Рис. 4. Отображение языковых конструкций при языковых преобразованиях............... 36 Рис. 5. Две похожие программы на Коболе: (а) OS/VS Cobol; (б) VS Cobol II ............... 44 Рис. 6. Простейший процесс языковых преобразований................................................... 53 Рис. 7. Пример программы на Коболе: (a) исходный текст; (b) сильно реструктурированный текст .......................................................................................... 55 Рис. 8. Общая схема преобразования приложений в рассматриваемом проекте ............ 61 Рис. 9. Передача "транзитной" переменной........................................................................ 85 Рис. 10. Уточненная схема преобразования языков, включающая выделение объектов 94 Рис. 11. Схема извлечения проектно-зависимого языка.................................................. 106 Рис. 12. Окончательный вариант методологии реинжиниринга, предложенной в диссертации .................................................................................................................. 111
127