Как построить утилиту анализа кода
и сделать это правильно с первого раза

(Building a Code-Analysis Utility and Doing It Right the First Time,
by Steven Feuerstein)

Стивен Фейерштейн
(steven.feuerstein@quest.com)
ORACLE ACE

 

Продолжение серии – статьи 4, 5.
Начало публикации серии (статьи 1-3)
FORS Magazine #3 2012.

[От редакции интернет-журнала "FORS Magazine": всемирно признанный классик Oracle-литературы Стивен Фейерштейн (Steven Feuerstein) в 2003-2004 г. на сайте журнала Oracle Magazine разместил серию из 8-ми статей «Как построить утилиту анализа кода и сделать это правильно с первого раза» . Единственный перевод на русский язык и публикация этой серии статей был предпринят в интернет-журнале «Oracle Magazine/Русское Издание». С тех пор язык PL/SQL, для изучения, освоения, применения и пр. которого, как представляется была написана С. Фейерштейном эта серия статей, позднее переименованная в “Построение утилиты анализа кода”, получил серьезнейшее развитие. PL/SQL обогатился многими механизмами и возможностями, стал стандартом программного языка для Oracle-приложений, стал предметом изучения в столь многих книгах, что перечислить их даже нет возможности. И хотя прошло много времени, по нашему мнению, работа С. Фейерштейна “Построение утилиты анализа кода” значения не потеряла, ни сколько не устарела и вполне может использоваться современными PL/SQL-программистами и как учебное пособие по PL/SQL, и как методологическое руководство по правильному проектированию приложений.

Но в настоящее время разыскать и английский оригинал, и перевод этих статей на русский язык не представляется возможным. Поэтому в интернет-журнале FORS Magazine принято решение о перепубликации этой серии в виде единого произведения, без повторений общих фрагментов, характерных для отдельно публиковавшихся статей.

За прошедшее время, возможно и даже естественно, изменилась терминология, изменились в какой-то степени взгляды на программирование и построение приложений. Все мы стали немножко умнее и более информированы. Поэтому наиболее критически настроенных читателей этой публикации просим рассматривать этот проект чем-то вроде литературного памятника, отразившего свою область общей картины развития нынешней информационной цивилизации. А для молодых PL/SQL-программистов, мы надеемся, эта работа С. Фейерштейна может стать увлекательным чтением и даже пригодиться в качестве своего рода учебника по разработке приложений на Oracle PL/SQL.

Перевод всех статей серии был выполнен Анной Парамоновой (УЦ ФОРС), а научное редактирование – Анатолием Бачиным (компания ФОРС). ]

Содержание статей всей серии.

Шаги по созданию утилиты

Статья 1. Формулировка проблемы: неоднозначные перегрузки в пакетах.
Я начну с подробного изучения важной задачи, которую должна будет решить утилита Codecheck: проблемы неоднозначных перегрузок в пакетах. Знаете ли вы, что весьма реально, и даже очень просто, перегружать программы в пакетах таким образом, что пакет будет компилироваться, но вы не сможете вызвать эти программы? Иногда такая неоднозначность очевидна, а иногда она едва уловима. Моей целью является написание утилиты, которая будет находить эти неоднозначности. На этом первоначальном шаге я покажу, что вовлекается в создание утилиты, как выполнять необходимый анализ, и как переводить результаты этого анализа в форму, полезную для разработчиков.

Статья 2. Начинаем, начинаем с тестирования
Я меня есть основная идея относительно того, что Codecheck должна делать. Я нашел некоторые встроенные пакеты и представления словаря данных, которые мне помогут. Что дальше? Моим естественным желание является открыть мой любимый IDE (интегрированную среду разработки - integrated development environment) и начать писать код – писать быстро и неистово, увлекаясь волнением творческого процесса, продумывая детали по мере продвижения, решая возникающие задачи, заставляя что-то работать, и затем настраивая это. Однако, этот путь не ведет к высококачественной реализации. Поэтому, я отложу написание кода и, вместо этого определю, как я буду тестировать мой код, чтобы убедиться, что он удовлетворяет всем требованиям.

Статья 3. Создание дизайна высокого уровня
Хотя я не собираюсь использовать никаких инструментов проектирования для построения Codecheck, все же необходимо посвятить некоторое время продумыванию общей архитектуры приложения, которое я собираюсь конструировать. Должно ли оно состоять из одной большой толстой процедуры? Едва ли. Нуждается ли оно во вспомогательных структурах данных? В этой статье я буду создавать фундаментальную, но осуществимую архитектуру Codecheck, стараясь избежать перепроектирования.

Статья 4. Реализация Codecheck: фаза конструирования
Наконец-то! Я начинаю писать код. Однако, это странный сорт кода, потому что я собираюсь строго следовать нисходящему дизайну, или стратегии пошаговой детализации. На каждом этапе этого пути, я буду прятать сложности и подробности реализации. Другими словами, я буду чрезвычайно мешкать, но ради стоящей цели: улучшение читаемости кода и уменьшение количества ошибок. Я буду также следовать одной из моих 10 заповедей для высококачественного PL/SQL-кодирования: ограничить размер исполняемой секции не более, чем 50 или 60 строками. Звучит невероятно? Последуйте моему совету, и это существенно улучшит ваш код.

Статья 5. Улучшение информации об аргументах
Забираясь дальше в дебри Codecheck, я примусь за наиболее сложную логику, связанную с анализом потенциально неоднозначных перегрузок. Я подробно рассмотрю многоуровневые коллекции и коллекции, индексированные строками (string-indexed), и покажу, насколько они важны для того, чтобы спрятать сложность структур такого рода в процедуру или функцию.

Статья 6. Создание “Поставщика сервисов”: пакетов с концентрированной функциональностью.
На нижнем уровне иерархии программного обеспечения Codecheck находятся несколько пакетов, являющихся относительно маленькими концентрированными модулями. Они предоставляют сервисы пакетам более высоких уровней. Одним из важных элементов дизайна программного обеспечения является определение таких отдельных сервисов и создание индивидуальных пакетов (или, может быть, объектных типов) для объединения всего, относящегося к этому сервису.

Статья 7. Создание “Поставщика сервисов”, Часть 2.
Я объясню, как использовать преимущества очень полезной процедуры DBMS_UTILITY.NAME_RESOLVE и рассмотрю отдельный механизм отчетности для Codecheck. Я покажу также общий пакет обработки ошибок, который включает механизм утверждений и “изящную” процедуру RAISE.

Статья 8. Повторная настройка.
Теперь, имея работающую версию Codecheck, настало время использовать механизм тестирования utPLSQL и подготовленные мной тестовые данные, чтобы выполнить тестирование. Когда я обнаружу проблемы в коде, я буду не только исправлять их, но и открывать возможности улучшения внутренней работы моих программ, этот процесс называется рефакторинг.

Стивен Фейерштейн
(steven.feuerstein@quest.com)
ORACLE ACE

Статья 4.

Реализация Codecheck: фаза построения
(Implementing Codecheck: The Construction Phase,
by Steven Feuerstein)

Время шаг за шагом создавать код пакетов

В первых статьях этой серии (см. обзор) показано, как я разрабатывал план тестирования и определял случаи тестирования для своей утилиты контроля качества. Я также руководствовался здравым смыслом при общем проектировании Codecheck. И вот наконец-то настало время начать писать код. Как это лучше сделать?

Одним из подходов, который можно избрать, является конструирование снизу вверх: начать с наиболее низкого уровня или элементарных пакетов. Создавать и тестировать их, чувствуя уверенность, что их можно будет использовать в более сложных пакетах, не внося ошибок. Затем перейти на следующий уровень иерархии, принимаясь за более сложные задачи, но основываясь на предыдущей работе.

Это звучит ужасно благоразумно. Однако существует одна большая проблема. Я планирую следовать девизу Экстремального Программирования – “проектировать только то, что нужно сейчас” - то есть, делать все как можно проще. Если я начну с элементов низкого уровня, и буду подниматься вверх, как я узнаю, что “нужно сейчас” и что потребуется позже или не потребуется вовсе?

Например, можно начать с cc_types, пакета с конкретным назначением: объединить все знания о различных типах данных, известных и поддерживаемых PL/SQL. Прекрасно. Какую функциональность я должен предложить в спецификации моего пакета? Теперь я вхожу в режим мозгового штурма и генерирую множество идей:

Вот так так! Этого, пожалуй, достаточно, чтобы дать вам повод к размышлению. Даже для такого базового пакета, как cc_types можно придумать массу интересных идей. Тем не менее, это не выход. Разве я строю этот пакет, потому что я люблю писать код, чем больше, тем лучше? Допустим так, но это уже задача для моего психотерапевта. Шутки в сторону, ответ – НЕТ. Я строю cc_types, для того, чтобы написать Codecheck самым простым, удобным для чтения и изменения способом. С этой точки зрения не имеет значения, что я могу придумать 10 или 20 или 50 интересных идей. Имеет значение следующее: какие фрагменты функциональности требуются Codecheck? На этом этапе я еще не знаю.

Должен ли я просто написать все эти программы? Ничего подобного. Осознание этого неизбежно приводит меня к необходимости прекратить процесс снизу-вверх. Вместо этого, я предпочту подход сверху-вниз. Я начну на самом высоком уровне Codecheck (а именно, с выполняемого раздела процедуры codecheck.overloadings) и буду продвигаться вглубь. Вместо того чтобы писать программу в виде одного огромного блока, состоящего из сотен строк исполняемого кода, я разобью его на более мелкие модули. Кроме того, я детально обдумаю логику, необходимую на каждом уровне детализации, перед тем, как идти дальше. В этом процессе, как вы увидите, я определю конкретно, что мне нужно в cc_types, cc_names, cc_arguments и во всех других пакетах.

Когда я определю функциональность, необходимую в заданном пакете, можно будет реализовать те и только те программы из этого пакета, которые будут использоваться. Я не хочу делать лишнюю работу. Если позже, в процессе разработки, мне потребуется что-то еще, я просто изменю спецификацию программы так, чтобы она соответствовала этим требованиям, и затем добавлю эту программу в соответствующий пакет. И вовсе нет необходимости делать это линейно. Я могу писать слой логики в главной процедуре и затем перейти к cc_types, чтобы реализовать новую функцию (можно, например, думать о том, как реализовать эту функцию и записать идеи, пока они не забылись).

Высокоуровневое описание codecheck.overloadings

Первой программой, которую я хочу реализовать в Codecheck, является программа, которая будет анализировать перегрузки пакета на предмет возможных неоднозначностей. Иначе говоря, я хочу проверять перегрузки пакета. Поскольку имя моего пакета уже содержит слово check, я назову эту процедуру overloadings, то есть codecheck.overloadings. (Для меня это звучит лучше, чем codecheck.check_overloadings.)

Перед тем, как начать писать код, стоит подумать, как будет вызываться эта процедура? Какие параметры необходимо указать? Только имя пакета, содержащего перегрузки, которые я хочу анализировать, ничего более. С учетом этого, вот пример вызова моей процедуры: codecheck.overloadings (package_in => l_name);

Это начало. У меня есть заголовок (имя и список параметров) программы, поэтому я могу начать конструировать ее. Снова пришло время перейти в режим “мозгового штурма”, но с другой точки зрения. Вместо того чтобы придумывать список всех возможных программ, которые, возможно, потребуются, я буду думать о больших кусках логики, которые будут нужны, чтобы сделать работу – и ни о чем кроме этого.

Во-первых, мне необходимо проверить аргументы и инициализировать структуры данных, которые будут использоваться в программе. Вам почти всегда приходиться делать некоторого рода инициализацию, не так ли? Когда я выполню инициализацию, настанет время анализировать перегрузки для каждого отдельного имени программы в пакете. После выполнения анализа, я покажу результаты на экране. И это все? На этом этапе игры, да.

В псевдокоде у меня получится нечто вроде этого:

Для каждого имени программы в пакете
loop
   If эта программа перегружаемая
   then
      Для каждой перегрузки этой программы
      loop
         проверить на соответствие все другие перегрузки 
      end loop для каждой перегрузки
   end if программа перегружаемая
end loop для каждой программы

Показать результаты анализа

Кажется, достаточно ясно. На Листинге 4.1 показан PL/SQL код. (Далее в этой статье я буду пропускать псевдокод, и буду выражать свою высокоуровневую логику непосредственно на PL/SQL.)

Как вы можете видеть, я начал объявлять элементы пакетов cc_smartargs и cc_report. В таблице 6 объясняется ход рассуждений, шаг за шагом.

Теперь я закончил с первым уровнем спецификации процедуры overloadings. Я знаю гораздо больше о том, что мне нужно из cc_smartargs, чем до этого, но я стараюсь избегать любых подробностей реализации этого пакета. На следующем этапе у меня есть некоторый выбор. Я могу реализовать следующий уровень детализации в overloadings (initialize и compare_with_others), или могу переключиться на cc_smartargs, и начать искать, что ему потребуется, чтобы предоставить всю эту информацию overloadings. Пожалуй, я повожусь еще немного с overloadings. Мне необходимо знать больше о требованиях к cc_smartargs (и кто знает о чем еще?).

Логика инициализации

Какие этапы инициализации необходимы overloadings? Снова, оставаясь на самом высоком уровне, насколько это возможно, я сформулировал следующие этапы:

Что привело меня к следующей процедуре инициализации:

 1  PROCEDURE initialize
 2  IS
 3  BEGIN

 4     cc_smartargs.load_arguments (package_in);
 5
 6     cc_error.assert (
 7        cc_smartargs.ispackage
 8       ,cc_error.c_not_a_package
 9       ,package_in);
10
11     cc_report.initialize (package_in);
12* END initialize;

В строке 4 я прошу cc_smartargs выполнить трудную задачу: загрузить всю информацию об аргументах этого пакета в – ладно, в то, что будет лучше работать, это я решу позже. Я не собираюсь беспокоиться об этом прямо сейчас.

В строках с 6 по 9 я вызываю мою первую программу, связанную с ошибками: программу утверждений (assertion routine). Я вызываю эту программу, утверждая, что конкретное выражение является истинным. Если оно ложное, я остановлю выполнение программы. Программы утверждений являются нормальной частью программирования в других языках, таких как Java и C, и должны чаще использоваться в мире PL/SQL. В этом случае я хочу убедиться, что package_in действительно является именем пакета, поэтому вызываю функцию в cc_smartargs, чтобы получить эту информацию; определяю ошибку, которую я получу, если это не пакет; и также предоставляю имя этого не пакета, которое появится в сообщении для пользователя.

Я еще не знаю, что cc_error будет делать со всей этой информацией, но, используя подход передачи основных данных отдельной программе утверждений, я полагаю, что дам cc_error все необходимое для выполнения ее работы, не диктуя, как нужно выполнять эту работу. Это даст мне максимум гибкости позднее, когда я перейду к реализации пакета.
Возможно, вы удивитесь, зачем обращаться к cc_smartargs, чтобы узнать существует или нет указанный пакет. Как это относится к “удобной информации” об аргументах? Чтобы ответить на этот вопрос нам необходимо задать другой: Как можно легко выяснить, является ли программа пакетом? DBMS_UTILITY.NAME_RESOLVE может сказать мне это. И эта программа будет вызываться из cc_names, которая, как мне кажется, будет вызываться из cc_smartargs или cc_arguments в процессе загрузки всей информации об аргументах. Итак, я собираюсь принять решение, что мне необходимо брать информацию из cc_smartargs.

Обратите внимание, что я не передаю никаких параметров в cc_smartargs.is_package. Это означает, что я предполагаю, что cc_smartargs будет хранить или помещать информацию об указанном пакете в некоторую постоянную структуру данных. Один раз я вызову cc_smartargs.load_arguments, затем вся информация, которую вернет cc_smartargs, будет относиться к этому пакету и его программам и аргументам.

Наконец (возвращаясь к коду инициализационной программы), строка 11 вызывает отчетную программу пакета initialize, чтобы определить, работает ли она в тестовом режиме. Еще раз, вообще говоря, я не знаю, что этот тестовый режим может означать внутри cc_report, и, возможно, мне потребуется изменить его позднее, но сейчас, включение этой возможности кажется мне правильным шагом.

Сравнение одной перегрузки с другой

Следующей по порядку программой, необходимой мне, чтобы завершить codecheck.overloadings является compare_with_others. Она будет несколько более интересной и перспективной, чем initialize. Вот заголовок программы, в качестве напоминания:

PROCEDURE compare_with_others (
   program_in IN all_arguments.object_name%TYPE
  ,check_this_ovld_in IN PLS_INTEGER)

Что конкретно эта программа должна делать? Мне необходимо видеть, может ли текущая перегрузка быть вызвана способом, который был бы неотличим от вызова другой перегрузки. Поэтому, я должен сравнить ее с N—1 другими перегрузками. Это приводит меня непосредственно к более интересному, глубокому вопросу, какая нужна логика, чтобы выполнить это сравнение? Какие условия я должен проверить? Два основных фактора приходят в голову:

Первое, если две перегрузки имеют различный тип (одна - функция, а другая – процедура), то тут нет неоднозначности. Способы, которыми они вызываются из кода, достаточно различны (функция вызывается как часть оператора, а вызов процедуры – это отдельный оператор), чтобы быть уверенным, что компилятор не запутается.

Второе, мне может потребоваться проверять вызовы с различным количеством параметров для каждой перегрузки. Предположим, например, что список параметров перегрузки N содержит три аргумента, последний из которых имеет значение по умолчанию. Эта программа может быть вызвана с двумя или тремя значениями в списке параметров. А если список параметров перегрузки M имеет один или более замыкающий аргумент со значением по умолчанию, ну, тогда существует два или более корректных вызова этой перегрузки.

Примечание: когда я обсуждал возможные варианты правильных вызовов программы, я решил, что буду работать только с позиционной нотацией. Проблема неоднозначности перегрузок в большой степени исчезает, при использовании именованной нотации (parameter_name => parameter_value).

Поэтому, когда я говорю “сравнить перегрузку N со всеми другими перегрузками”, на самом деле я имею в виду (и собираюсь сделать) “сравнить все корректные варианты вызовов перегрузки N со всеми корректными вызовами всех других перегрузок”. Реализация compare_with_others представлена в Листинге 4.2.

Границы цикла FOR loop гарантируют, что я не сравниваю никакую перегрузку с самой собой, и не делаю повторно сравнений, уже сделанных на предыдущем шаге цикла, из которого вызывается compare_with_others.

Можно также заметить, что я определил еще две программы в Codecheck - same_program_types и compare_all_invocations – чтобы обрабатывать нетривиальные аспекты compare_with_others. Это начало кажется похожим на русскую матрешку, в которой вы открываете одну матрешку только для того, чтобы внутри нее найти другую? Я уверяю вас, что я не оттягиваю и не откладываю трудную работу. Напротив, я тружусь в поте лица, чтобы сделать все простым и понятным. Это не только обеспечивает избавление от ненужной работы (и переделывания изначально неверного кода), но также минимизирует возможность возникновения ошибок, поскольку я проверяю и корректирую логику на каждом этапе.

Надеюсь, что я вас убедил, на Листинге 4.3 представлена реализация same_program_types. Таблица 7 описывает эту реализацию.

Теперь давайте возьмем compare_all_invocations и другие аспекты логики Codecheck, необходимые для идентификации неоднозначных перегрузок.

Алгоритмы для идентификации неоднозначности

Я обнаружил, что мне необходимо сравнить все корректные вызовы каждой различной пары перегрузок. Сompare_all_invocations делает это (см. Листинг 4.4). В таблице 8 объясняется ее логика.

Покажите мне результат?

К этому времени, я полагаю, вы уже потеряли со мной терпение. Зачем нужна еще другая программа? Когда мы наконец увидим какой-нибудь реальный код? Я понимаю вашу реакцию. Но позвольте вас спросить: трудно ли следовать за логикой (сейчас состоящей более чем из 100 строк кода), которую я представлял до настоящего времени? Или она кажется настолько прозрачной – фактически упрощенной – что вам не терпится идти дальше?

Если последнее верно, то подумайте, как часто вы испытывали похожие чувства, когда смотрели на чей-нибудь код в первый раз (или на свой собственный - во второй раз)? Не чаще ли приходиться испытывать трудности, и даже негодование, читая чей-либо код, из-за непонятности программы? Поэтому, пожалуйста, оставайтесь со мной. Отложите свою неудовлетворенность многочисленными уровнями инкапсуляции и окольными путями. Обратите внимание на прозрачность кода приложения, и вскоре, наконец, вы увидите достаточно сложную логику.

Проверка сходства

Процедура check_for_similarity является последней или находится на самом низком уровне детализации, необходимом Codecheck, чтобы выполнить анализ неоднозначности. Она принимает в качестве параметров информацию, необходимую для идентификации двух перегрузок и специфическое подмножество списка параметров для каждой перегрузки. Вы можете описать это как сравнение перегрузки 1, вызываемой с ее первыми тремя параметрами, с перегрузкой 2, вызываемой с шестью параметрами. Если check_for_similarity обнаруживает, что две перегрузки являются неоднозначными, она сообщает об этом cc_report.

Как узнать, являются ли они неоднозначными? Давайте обсудим некоторую необходимую логическую схему:

Это дает мне достаточно подробностей, чтобы начать реализацию процедуры (см. Листинг 4.5). Я описал код в таблице 9.

Это все, что необходимо написать (по крайней мере, мне так кажется) в процедуре codecheck.overloadings, чтобы выполнить анализ. Могу ли я теперь запустить программу, чтобы увидеть работает ли она? Размечтался. Все, что я сделал, это, в сущности, мозговой штурм (с использованием методологии нисходящего дизайна), чтобы заставить codecheck.overloadings работать.

Я точно осведомлен о том, что мне необходимо на следующем уровне улучшения этой логики. В следующей статье этой серии я перейду к следующему уровню детализации (в программистских кругах это называется пошаговая детализация) и реализую каждую из программ в cc_arguments и cc_smartargs, которые определены в Codecheck.

Таблица 6: Процесс обдумывания для описания элементов

Строки

Пояснение

2

Вызов программы инициализации. Что она делает? Я вернусь к этому позже, потому что это следующий уровень детализации, и сейчас это не важно.

3

Мне необходимо пройти по всем различным (distinct) именам программ в пакете. Как их узнать? Эта информация доступна в ALL_ARGUMENTS:

SELECT DISTINCT object_name FROM all_arguments
 WHERE owner = <the owner> and package_name = <the package>

Но я уже решил, что вся такая информация будет обрабатываться в cc_arguments или, возможно, в cc_smartargs. Действительно, поскольку предполагается, что cc_arguments будет делать нечто большее, чем просто отображать содержимое ALL_ARGUMENTS, я могу сказать, что список различных имен программ – это “удобное” дополнение, и поэтому оно принадлежит cc_smartargs. Это позволяет мне сразу думать в терминах API или набора программ, которые дают мне информацию об этом наборе различных имен: первый, следующий, последний. Поэтому строка 4 использует первое имя.

5-6

Начинаем цикл и выясняем, как его завершить (когда я пройду по всем различным именам, имя программы будет NULL).

8

Является ли это имя именем перегружаемой программы в пакете? Только cc_smartargs может сказать мне, и поэтому я объявляю другую функцию в API: булевскую has_overloadings.

10-13

Теперь мне необходимо посмотреть на каждую перегрузку для этой программы. Похоже, что мне снова необходима информация, которую я собираюсь получить из данных, извлекаемых из ALL_ARGUMENTS, поэтому я буду полагаться на новые программы в cc_smartargs, чтобы определить границы: от первой до последней перегрузки для текущей программы.

14

Мне необходимо сравнить каждую найденную перегрузку с другой перегрузкой, чтобы увидеть, являются ли они различными или слишком похожими вызовами. Это похоже на логику процедуры codecheck.overloadings, поэтому я просто помешаю здесь заглушку для процедуры compare_with_others, принадлежащей overloadings.

18

Когда я покончу с этой программой, я перейду к следующей, снова полагаясь на функцию из cc_smartargs.

21

Время показывать результаты. Я объявил заголовок для show_ambig_ovld_results, первой процедуры в пакете cc_report. Мне определенно необходимо передать ей имя программы, но на этом этапе я не знаю, что еще нужно. Я вернусь к этому, когда буду готов.

Таблица 7: Описание реализации same_program_types

Строки

Пояснение

1

Хотя этого нельзя сказать из Листинга 4.3, эта функция не является локальным модулем, объявленным внутри пакета, в отличии от initialize и compare_with_others. Я решил объявить ее на том же уровне, что и overloadings внутри пакета. Я сделал так, потому что мне кажется, что содержание same_program_types характерно для логики overloadings. Другая высокоуровневая процедура проверки в Codecheck может пожелать узнать, принадлежат ли две перегрузки одному программному типу.

8-11

Как мне выяснить тип конкретной перегрузки? Вы не будете очень удивлены, что я могу взять, и возьму эту информацию из cc_smartargs. Она имеет список перегрузок, поэтому также может отслеживать информацию о каждой перегрузке. Вот так так. Похоже, что cc_smartargs собирается стать самым трудным пакетом для реализации. Будем надеяться, что если я буду делать по одному уровню за раз, все будет не так страшно.

13-17

Основной логикой same_program_types является простое булевское выражение. Типы являются одинаковыми, если они оба функции, или оба “не функции” (т.е. процедуры).

Таблица 8: Логика compare_all_invocations

Строки

Пояснение

2-4

Параметры. У меня есть перегрузка (check_this_in), которую я сравниваю с другой перегрузкой (against_this_in). Перегрузка идентифицируется просто числом, N-ая перегрузка заданной программы.

9-16

Внешний цикл для логики сравнения. Я использую метку, чтобы дать имя этому (и внутреннему) циклу, чтобы улучшить читабельность кода. Еще раз, я обращаюсь к cc_smartargs, чтобы выбрать первый и последний вызов для "check this" перегрузки. Как определяются эти вызовы? Я не знаю, и сейчас не беспокоюсь об этом. Я просто знаю, что существует N корректных вызовов, и что cc_smartargs проведет меня через них.

8-25

Внутренний цикл, который проводит нас через вызовы "against this".

26-32

Наконец, “действие” - тело вложенных циклов. И здесь я обнаруживаю, что я просто вызываю другую процедуру - check_for_similarity. Я передаю этой программе всю информацию, необходимую для сравнения этих двух вызовов.

Таблица 9: Описание реализации check_for_similarity

Строки

Пояснение

1-4

Заголовок и список параметров. Аргументы overload1_in и overload2_in ссылаются на конкретные перегрузки program_in. perm1_in и perm2_in – это конкретные вызовы со списками параметров для каждой соответствующей перегрузки.

7-13

Чтобы надлежащим образом сравнить каждую пару аргументов в двух перегрузках, мне необходимо получить список информации о параметрах “верхнего уровня” (те строки в ALL_ARGUMENTS, которые действительно появляются в списке параметров программы) для cc_smartargs. Но я буду выбирать список параметров, как объявлено в cc_arguments. Поэтому, я объявляю две локальных переменных, как коллекции или списки параметров, объявленных в cc_arguments. И вызываю функцию parameter_list в cc_smartargs, которая возвращает только необходимую информацию – а именно, от первого до N-го аргумента, как задано в этом вызове.

15-16

Здесь я получаю количество строк в каждом списке параметров и присваиваю их локальным переменным.

21-27

Простая часть этой процедуры: я смотрю количество аргументов в каждом списке параметров и принимаю решение, закончил ли я (явно различны или явно похожи – нет аргументов).

29-39

Время сравнивать соответствующие параметры в каждом из списков. Я использую простой цикл для прохода по каждой позиции списка. Я остановлюсь, когда закончатся параметры в первой перегрузке или когда выяснится, что существует достаточное различие между перегрузками, чтобы закончить проверку. В теле цикла я вызываю функцию из cc_arguments, чтобы выяснить, принадлежат ли типы данных двух аргументов к одному “семейству”. Строка 38 переводит меня к следующему параметру в списке.

42-49

Если две перегрузки слишком похожи, наступает время использовать cc_report, чтобы сообщить об этом факте. Мне необходима процедура, чтобы сообщить о неоднозначной перегрузке – и ей нужно передать всю информацию, необходимую для идентификации этих двух отдельных вызовов: имя программы, порядковый номер перегрузки и, наконец, позицию аргумента.

LISTING 4.1

1  BEGIN -- main overloadings
 2     initialize;
 3     l_program_name := cc_smartargs.first_program;
 4
 5     LOOP
 6        EXIT WHEN l_program_name IS NULL;
 7
 8        IF cc_smartargs.has_overloadings (l_program_name)
 9        THEN
10           FOR overloading IN
11              cc_smartargs.first_overloading (l_program_name) ..
12              cc_smartargs.last_overloading (l_program_name)
13           LOOP
14              compare_with_others (l_program_name, overloading);
15           END LOOP;
16        END IF;
17
18        l_program_name := cc_smartargs.next_program (l_program_name);
19     END LOOP;
20
21     cc_report.show_ambig_ovld_results (program_in); -- Parameters?
22  END overloadings;

LISTING 4.2

 1  PROCEDURE compare_with_others (
 2     program_in IN all_arguments.object_name%TYPE
 3    ,check_this_ovld_in IN PLS_INTEGER
 4  )
 5  IS
 6  BEGIN
 7     FOR against_this_ovld IN
 8           check_this_ovld_in
 9         + 1 .. cc_smartargs.last_overloading (program_in)
10     LOOP
11        IF same_program_types (program_in
12                              ,check_this_ovld_in
13                              ,against_this_ovld
14                              )
15        THEN
16           compare_all_invocations (program_in
17                                    ,check_this_ovld_in
18                                    ,against_this_ovld
19                                    );
20        END IF;
21     END LOOP;
22  END compare_with_others;

LISTING 4.3

 1  FUNCTION same_program_types (
 2    program_in IN VARCHAR2
 3   ,overload1_in IN PLS_INTEGER
 4   ,overload2_in IN PLS_INTEGER
 5  )
 6    RETURN BOOLEAN
 7  IS
 8    l_arg1_is_function BOOLEAN
 9             := cc_smartargs.is_function (program_in, overload1_in);
10    l_arg2_is_function BOOLEAN
11             := cc_smartargs.is_function (program_in, overload2_in);
12  BEGIN
13    RETURN (   (    l_arg1_is_function
14                AND l_arg2_is_function)
15            OR (    NOT l_arg1_is_function
16                AND NOT l_arg2_is_function)
17           );
18  END same_program_types;

LISTING 4.4

 1  PROCEDURE compare_all_permutations (
 2     program_in IN VARCHAR2
 3    ,check_this_in IN PLS_INTEGER
 4    ,against_this_in IN PLS_INTEGER
 5  )
 6  IS
 7  BEGIN
 8
 9     «check_this»
10     FOR check_this_perm IN
11        cc_smartargs.first_permutation (
12           program_in,check_this_in)
13        ..
14        cc_smartargs.last_permutation (
15           program_in, check_this_in)
16     LOOP
17
18        «against_this 	»
19        FOR against_this_perm IN
20           cc_smartargs.first_permutation (
21              program_in, against_this_in)
22           ..
23           cc_smartargs.last_permutation (
24              program_in, against_this_in)
25        LOOP
26           check_for_similarity (program_in
27                                ,check_this_in
28                                ,check_this_perm
29                                ,against_this_in
30                                ,against_this_perm
31                                ,show_in
32                                );
33        END LOOP against_this;
34     END LOOP check_this;
35  END compare_all_permutations;

LISTING 4.5

 1  PROCEDURE check_for_similarity (
 2     program_in IN VARCHAR2
 3    ,overload1_in IN PLS_INTEGER, perm1_in IN PLS_INTEGER
 4    ,overload2_in IN PLS_INTEGER, perm2_in IN PLS_INTEGER
 5  )
 6  IS
 7     arglist1 cc_arguments.arguments_tt
 8        := cc_smartargs.parameter_list (
 9              program_in, overload1_in, 1, perm1_in);10
11     arglist2 cc_arguments.arguments_tt
12        := cc_smartargs.parameter_list (
13              program_in, overload2_in, 1, perm2_in);
14
15     l_numargs_perm1 PLS_INTEGER := arglist1.COUNT;
16     l_numargs_perm2 PLS_INTEGER := arglist2.COUNT;
17
18     arg_row PLS_INTEGER := 1;
19     too_similar BOOLEAN;
20  BEGIN
21     IF l_numargs_perm1 != l_numargs_perm2
22     THEN
23        too_similar := FALSE;
24     ELSIF     l_numargs_perm1 = 0
25           AND l_numargs_perm2 = 0
26     THEN
27        too_similar := TRUE;
28     ELSE
29        LOOP
30           EXIT WHEN arg_row IS NULL
31                 OR NOT too_similar
32                 OR arg_row > l_numargs_perm1;
33
34           too_similar :=
35              cc_arguments.in_same_family (
36                 arglist1 (arg_row), arglist2 (arg_row));
37
38           arg_row := arglist1.NEXT (arg_row);
39        END LOOP;
40     END IF;
41
42     IF too_similar
43     THEN
44        cc_report.ambig_ovld (
45           program_in
46          ,overload1_in, arg1_end_in
47          ,overload2_in, arg2_end_in
48          );
49     END IF;
50  END;

Стивен Фернстайн,
член OTN с 2001

Статья 5.

Улучшение информации об аргументах
(Adding Smarts to the Argument Information,
by Steven Feuerstein)

Наконец, настало время погрузиться в детали: как реализовать сложную логику, связанную с анализом потенциально неоднозначных перегрузок.

В статье 5 этой серии, я погружусь в глубины Codecheck и реализую сложную логику, связанную с анализом потенциально неоднозначных перегрузок. Я рассмотрю подробно многоуровневые и индексированные строками коллекции, показывая как важно прятать сложность таких структур за процедурами и функциями.

Имеет смысл сделать это сейчас, потому что я закончил реализацию codecheck.overloadings. Следовательно, настало время перейти к реализации вспомогательных пакетов. Какой пакет будет следующим? Я не хочу пока реализовывать низкоуровневые пакеты, такие как cc_types. Насколько я знаю (и предполагаю) cc_arguments и/или cc_smartargs будут иметь свои требования к этому пакету. Так что лучше подождать. Следуя вниз по иерархии пакетов, пожалуй, следующим должен быть cc_smartargs. Это также пакет, хотя не единственный, который Codecheck использует, чтобы сделать свою работу.

Действительно, в результате работы с Codecheck у меня есть все начальные данные для построения cc_smartargs. Какие элементы из cc_smartargs идентифицированы при поэтапном улучшении как необходимые для реализации codecheck.overloadings (см. Таблицу 10)?

Чтобы определить, какую программу рассматривать первой, можно разбить их все на 2 категории: (1) программы, которые загружают информацию о пакете и (2) программы, которые возвращают информацию об этих аргументах. Только одна программа попадает в первую категорию: load_arguments. Все другие программы относятся ко второй. И, поскольку я не могу реализовать программу, возвращающую данные, до того как я пойму как загружать и хранить эту информацию, понятно, что я должен сначала реализовать load_arguments.

Загрузка данных об аргументах

Настало время для некоторого реального обследования. Чтобы построить cc_smartargs.load_arguments, я энергично возьмусь за процесс аккуратного построения сложной программы из наброска, используя нисходящий дизайн, создавая небольшие и легко читаемые исполняемые секции. Но это вовсе не означает, что я буду писать о каждом из этих шагов. Я полагаю, вы хорошо прочувствовали этот процесс, благодаря предыдущим статьям этой серии, и не стоит вновь обсуждать эту тему.

Что я хочу сделать сейчас, так это быстро пройти по уровням сложности, пока не доберусь до точки, где имеет смысл (наконец-то) спроектировать структуры данных, которые будут хранить информацию об аргументах (как сырую информацию из ALL_ARGUMENTS, так и обработанную версию со всем, что мне необходимо для выполнения анализа неоднозначности). Когда я рассортирую ее, то смогу написать программу, которая делает информацию доступной для Codecheck. Ниже представлено несколько этапов разработки cc_smartargs.load_arguments, с минимальными пояснениями. Хочется верить, что они более-менее говорят сами за себя. Ты, мой терпеливый читатель, конечно, будешь окончательным судьей.

Итак, вот исполняемая секция load_arguments:

1  BEGIN
 2    initialize (program_in, show_in);
 3
 4    g_all_arguments := cc_arguments.fullset (program_in);
 5
 6    IF g_all_arguments.COUNT = 0
 7    THEN
 8       cc_util.pl ('');
 9       cc_util.pl ('No arguments found for "' || program_in || '".');
10       cc_util.pl ('');
11    ELSE
12       compute_derived_information;
13    END IF;
14  END load_arguments;

После инициализации настало время получить сырую информацию об аргументах из ALL_ARGUMENTS и DBMS_DESCRIBE, и это можно сделать, вызвав функцию cc_arguments.fullset (которая пока не написана). Функция возвращает все данные во что-то – и, я полагаю, что этим чем-то будет коллекция записей, в которой каждая строка соответствует строке в ALL_ARGUMENTS.

Если данных об аргументах для этого пакета не найдено, то делать больше нечего, поэтому на экране показывается соответствующее сообщение. Обратите внимание, что в первый раз cc_util появляется в 8-10 строке – в виде процедуры cc_util.pl, что означает "print a line" (“печать строки”). Из многолетнего опыта я знаю, что не стоит напрямую вызывать DBMS_OUTPUT.PUT_LINE. Эта утилита слишком ненадежна. (Объяснение моей точки зрения на DBMS_OUTPUT.PUT_LINE, приведено в книге O'Reilly & Associates' Oracle Built-in Packages.) Вместо этого я создам свою собственную замену этой программы, которую построю на основе этой утилиты.

Если cc_arguments.fullset находит информацию об аргументах, то я вызываю процедуру compute_derived_information для завершения работы. В чем состоит эта работа? Хотя я еще не думал об этом, мои исследования показывают, что данные в ALL_ARGUMENTS очень сложные и многоуровневые (уровни внутри позиций внутри перегрузок внутри имен программ). Я уверен, что придется выполнить некоторую обработку сырых данных, чтобы сделать их более полезными и удобными при анализе Codecheck (например, необходимо идентифицировать эти вызовы). Поэтому, давайте посмотрим, что эти данные должны включать.

Извлеченная информация об аргументах

Прежде всего, какую информацию необходимо извлечь, и какие структуры данных необходимо объявить для хранения этой информации? Заданные виды циклов, уже объявленные в Codecheck, приводят к следующему перечню необходимой информации:

Когда я в первый раз начинал думать о Codecheck, я полагал, что выгружу содержимое ALL_ARGUMENTS в коллекцию, которая будет отражать представление словаря БД, и затем напишу код для обработки этой информации. Это, в сущности, именно то, что нужно от cc_arguments.fullest. Когда я начинаю думать о коде, который необходимо написать, мне приходят в голову некоторые дополнительные соображения. Дело в том, что в строках ALL_ARGUMENTS закодировано ужасное количество специальной информации (подробнее, см. раздел “Исследование: все об ALL_ARGUMENTS” в первой статье этой серии).

Использование “плоского” представления ALL_ARGUMENTS в моей коллекции означает, что мне придется написать большое количество сложного кода, чтобы ответить на такие вопросы как:

Оказывается, ALL_ARGUMENTS отражает эти данные различным образом.

Затем я вспомнил, что недавно узнал о новых возможностях коллекций в базе данных Oracle9i: теперь можно индексировать, используя строки, а также можно объявлять коллекции внутри коллекций. Вероятно, существует способ реорганизовать данные ALL_ARGUMENTS так, чтобы использовать преимущества этих возможностей и упростить результирующий код!

Естественная иерархия в ALL_ARGUMENTS может быть выражена так:

Каждая строка в ALL_ARGUMENTS представляет один параметр (или один подэлемент параметра, если он сложный) одной перегрузки одной программы в пакете (или отдельной процедуры/функции).

Иначе говоря:

Пакет
   Программа
      Перегрузка
         Параметр
           “Фрагмент”

где “фрагмент” (“breakout”) - это отдельная строка в ALL_ARGUMENTS, которая разбивает параметр на подэлементы, такие как поля записи или атрибуты объектного типа.

Это выглядит полезным. Какой код необходимо написать, чтобы реализовать такую иерархию? Я буду подходить к этому постепенно, в два этапа:

  1. Освоиться с технологией;
  2. Спроектировать то, что мне необходимо для Codecheck.

Зачем нужен шаг "освоиться с технологией"? Я обнаружил, что когда начинаешь работать с новыми возможностями, особенно достаточно сложными, действительно выгодно поиграть с ними; почувствовать синтаксис; острые углы; ошибки и недокументированные возможности; и выработать наилучшую практику, которую имеет смысл использовать при работе с этими структурами. Когда у меня есть комфортные рабочие отношения, в данном случае, с многоуровневыми коллекциями, то можно работать с высоким уровнем уверенности в действительно сложном, реальном приложении. Кроме того, с более высокой вероятностью можно избежать ошибок начинающих и решений плохого дизайна.

Освоимся с многоуровневыми коллекциями

Я собираюсь отвлечься от Codecheck и написать утилиту, которая проверяет мою теорию о преобразовании данных из ALL_ARGUMENTS в многоуровневую коллекцию. Окончательный код для этого находится в файле multilevel.sql. В этом файле создается процедура с именем show_all_arguments, которая принимает в качестве своего единственного параметра имя объекта PL/SQL кода (это может быть пакет или отдельная программа) и затем отображает информацию в двух форматах:

Не вдаваясь во все подробности нисходящего подхода проектирования, вот исполняемая секция моей процедуры:

BEGIN
   process_name;
   load_arrays;
   dump_arguments_array;
   dump_multilevel_array;
END show_all_arguments;

Процедура process_name использует DBMS_UTILITY.NAME_RESOLVE для проверки и разбора имени. Мы не будем сейчас обсуждать подробности. Вместо этого, давайте перейдем к загрузке этих массивов. Перед тем, как загружать их, необходимо создать структуры данных (объявить набор типов TABLE). Помните, что я хочу имитировать следующую иерархию:

Package
   Program
      Overloading
         Parameter
            "Фрагмент" 

Давайте посмотрим, какого рода код необходимо написать для создания структуры данных. Прежде всего, я обнаружил, что необходимо работать снизу вверх при проектировании таких структур. Итак, как будет выглядеть коллекция "фрагмент". Используем следующий оператор TYPE:

TYPE breakouts_t IS TABLE OF all_arguments%ROWTYPE
   -- Порядковый номер внутри параметра 
   INDEX BY BINARY_INTEGER;

Иначе говоря, каждая строка этого типа содержит отдельную строку информации из ALL_ARGUMENTS. Индекс будет просто порядковым номером, соответствующим порядку в котором данные находятся в представлении.

Теперь необходимо создать второй уровень коллекций, на этот раз соответствующий каждому параметру, который появляется в заголовке программы:

TYPE parameters_t IS TABLE OF breakouts_t
   -- Позиция параметра
   INDEX BY BINARY_INTEGER;

Каждая строка в этой коллекции содержит набор всех “фрагментов” для этого параметра, в котором ключ или номер строки в коллекции отражает позицию в списке параметров. Если тип данных параметра – скалярный, например NUMBER, то, вообще говоря, будет соответствие один к одному. То есть, будет существовать только одна запись в коллекции типа breakouts_t. С другой стороны, если тип данных третьего параметра – запись с четырьмя скалярными полями, то коллекция фрагментов для третьей строки коллекции параметров будет содержать шесть (вот сюрприз!) строк: одна – для параметра, одна – для записи, и четыре – для полей.

Далее необходимо объявить структуру, которая содержит информацию для конкретной перегрузки. Как насчет этого:

TYPE overloadings_t IS TABLE OF parameters_t
   -- Индекс по номеру перегрузки
   INDEX BY BINARY_INTEGER; 

Каждая строка в этой коллекции является коллекцией параметров, строки которых, в свою очередь, содержат коллекции фрагментов. Следите за мыслью?

Отлично, тогда еще один уровень. (Мне не нужна коллекция для самого пакета; меня интересуют только перегрузки и аргументы для одного пакета в один момент времени.) Теперь необходима коллекция для хранения всех перегрузок каждого отдельного имени программы:

TYPE programs_t IS TABLE OF overloadings_t
   -- Индекс по имени программы
   INDEX BY all_arguments.object_name%TYPE;

Обратите внимание, что в этом случае индексом в коллекции является имя программы, это преимущество новой возможности индексирования на основе строки в БД Oracle9i Release 2.

Имея такие типы коллекций, можно объявить коллекцию верхнего уровня, которая хранит всю структурированную информацию из ALL_ARGUMENTS. На самом деле, в show_all_arguments я объявлю две различных коллекции: коллекцию, в которой хранится просто дамп ALL_ARGUMENTS и вторую, которая использует многоуровневый подход.

-- Дамп ALL_ARGUMENTS
l_arguments      breakouts_t;
-- Многоуровневые данные из ALL_ARGUMENTS 
l_programs       programs_t;

И теперь можно загрузить мои массивы данными из ALL_ARGUMENTS. Давайте посмотрим на разницу в коде, который необходимо написать для этих различных коллекций. (См. Листинг 5.1.)

Имея загруженную многоуровневую коллекцию, легко выбрать необходимую информацию. Предположим, например, что мне необходимо получить список параметров для четвертой перегрузки программы allargs_test.upd. Давайте сравним код, который необходимо написать для различных типов хранения информации об аргументах. Допустим, что для обоих случаев я объявил следующую коллекцию:

upd_4_parameters   breakouts_t;
  1. Выбираем информацию напрямую из представления. Этот метод является относительно прямым, но неэффективным, поскольку он требует неоднократного выполнения запросов, подобных следующему:
    SELECT   *
        BULK COLLECT INTO l_arguments
        FROM all_arguments
       WHERE owner = USER
         AND package_name = 'ALLARGS_TEST'
         AND object_name = 'UPD'
         AND overload = 4
    ORDER BY SEQUENCE, POSITION, LEVEL;
  2. Сканируем “плоскую” коллекцию данных ALL_ARGUMENTS, выбирая необходимые строки:
    FOR argindx IN l_arguments.FIRST .. l_arguments.LAST
    
    LOOP
       IF     l_arguments (argindx).object_name = 'UPD'
          AND l_arguments (argindx).overload = 4
       THEN
          upd_4_parameters (NVL (upd_4_parameters.LAST, 0) + 1) :=
              l_arguments (argindx);
       END IF;
    END LOOP;
  3. Одной строкой выполняем присваивание для выбора данных.
    upd_4_parameters := l_programs ('UPD')(4);

Отлично, я знаю, какой подход предпочесть!

Предположим, что теперь я хочу вывести содержимое многоуровневой коллекции l_programs. Это влечет за собой, что существенно, полный просмотр таблицы, проход через многочисленные уровни структуры. Это делает процедура dump_multilevel_array. Давайте посмотрим на структуру и поток этой программы; это хороший пример использования вложенных локальных процедур для создания кода, максимально удобного для чтения. (Замечание: файл multilevel.sql содержит полную реализацию, включая вызовы для отображения результата на каждом уровне, которые я удалил здесь, чтобы мы могли более внимательно сконцентрироваться на логическом потоке.)

При работе с вложенными программами лучше всего читать программу снизу вверх или изнутри наружу. Чтобы помочь этому процессу, я люблю добавлять в форму комментарии типа -- main <program name> после оператора BEGIN в главной исполняемой секции программы. Затем можно быстро найти начальную точку для анализа кода.

Я ищу "main dump_multilevel_array" и нахожу строки 46-51. Вы видите здесь знакомый код, используемый для сканирования коллекции от FIRST до LAST с методом NEXT; глядя на эти строки, вы не можете сказать, что номер строки является на самом деле строкой – именем программы. Для каждой строки в этом списке программ, я просто вызываю show_overloadings, чтобы показать всю информацию о перегрузках для этой программы.

Давайте посмотрим на show_overloadings в поисках "main show_overloadings". В Листинге 5.2, строки 36-43 показывают, в сущности, ту же логику, что и строки 46-51: проход по каждой строке этой вложенной коллекции и вызов show_parameters, чтобы показать все параметры этой перегрузки. Повторяя этот процесс, я нашел строки 27-33, путешествуя по списку параметров и показывая фрагменты для каждого параметра. И, наконец, строки 20-24 отображают данные об аргументах.

В листинге 5.3 показан результат из show_all_arguments (с временно измененной версией noparms2, чтобы включить третий аргумент, являющийся типом PL/SQL запись). Как можно видеть, скрытая иерархия данных в ALL_ARGUMENTS становится открытой и прозрачной для разработчиков, благодаря присваиванию этих записей четырехуровневой коллекции l_programs.

Имея основное понимание того, как можно преобразовать плоские данные в высокоструктурированную и доступную информацию в многоуровневой коллекции, давайте перейдем к конструированию реальных многоуровневых коллекций, необходимых для Codecheck. Они несколько сложнее тех, что я использовал в show_all_arguments.

Реальная многоуровневая коллекция, которую использует Codecheck

Хотя базовая иерархия, отраженная в конфигурации programs_t-overloadings_t-parameters_t-breakouts_t достаточно хороша, однако, вмешиваются требования реальности. Одной из проблем способа, которым я объявлял эти типы коллекций, является то, что каждая содержит только другую коллекцию в качестве своего типа данных. Это означает, что совершенно невозможно отследить другую информацию, которая может быть полезна на этих уровнях.

Вспоминая свои исследования ALL_ARGUMENTS и требования, появившиеся в результате нисходящего проектирования Codecheck, можно создать следующую таблицу необходимой дополнительной информации:

Сущность

Необходимая информация об этой сущности

Программа

  • Является перегружаемой?

Перегрузка

  • Если это функция, то, что она возвращает? Эта информация существенно отличается по своей природе от списка параметров. Лучше держать ее отдельно.
  • Каков последний параметр, не имеющий значения по умолчанию? Это необходимо знать, чтобы определять различные правильные вызовы перегрузки.

Параметр

  • Каковы характеристики (имя, тип данных, и т.д.) этого параметра верхнего уровня? Эта информация доступна из уровня breakouts_t, но чтобы получить ее придется написать громоздкий код. Лучше идентифицировать эту информацию как качественно отличную от фрагментов и хранить ее отдельно.

Фрагмент

  • Не требуется никакой дополнительной информации, по крайней мере, на этой стадии работы над Codecheck.

Теперь, когда все закончено, можно определить, является ли программа перегружаемой, просто получив количество (COUNT) строк в коллекции перегрузок программ (если больше одной строки, программа перегружаемая), поэтому нет необходимости изменять это описание коллекции. В других случаях, однако, необходимо создать промежуточную структуру, которая может хранить дополнительную информацию и является типом моей коллекции. PL/SQL тип запись является прекрасным решением. Давайте посмотрим, как это меняет дело. Как я уже сказал, самый верхний уровень – коллекция иерархий останется такой же:

CREATE OR REPLACE PACKAGE cc_smartargs
IS
   -- Все неповторяющиеся программы в пакете/программе 
   TYPE programs_t IS TABLE OF overloadings_t
     INDEX BY all_arguments.object_name%type;

Однако коллекция будет основана на записи, поэтому я могу добавить следующее значение:

TYPE overloadings_t IS TABLE OF one_overloading_rt
  INDEX BY /* номер перегрузки; 0 - если не перегружаемая */ 
     BINARY_INTEGER;
А как будет выглядеть запись о перегрузке? Очень просто:
-- Перегрузки для одного имени программы:
TYPE one_overloading_rt IS RECORD (
  parameters parameters_tt,
  return_clause one_parameter_rt,
  last_nondefault_parm  PLS_INTEGER
);

Первое поле записи содержит полный набор параметров, которые в свою очередь являются третьим уровнем моей коллекции иерархий; я определю его дальше. Второе поле содержит всю информацию о возвращаемом значении (если это функция), которое является, в свою очередь, отдельным параметром (верхний уровень плюс фрагмент). Поэтому я основал его на том же самом типе запись, который будет объявлен для представления таких данных. Третье поле – это позиция последнего параметра, не имеющего значения по умолчанию в заголовке программы.

Итак, как будет выглядеть массив информации о параметрах? Как-то так:

TYPE parameters_tt IS TABLE OF one_parameter_rt
  INDEX BY /* Порядковая позиция в списке параметров */ 
     BINARY_INTEGER;

Каждая строка в записи содержит всю необходимую информацию об отдельном параметре, который появляется в заголовке программы. Мне необходим тип запись, потому что я хочу отслеживать отдельно информацию верхнего уровня и фрагменты:

TYPE one_parameter_rt IS RECORD (
  toplevel    cc_arguments.one_argument_rt,
  breakouts   cc_arguments.arguments_tt
);

Обратите внимание, что я перевожу стрелки и основываюсь на структурах данных из пакета cc_arguments (который еще только должен быть построен!). Почему так? Я объявил этот пакет, чтобы он содержал всю “родную” информацию о данных из ALL_ARGUMENTS. С этой точки в структурах данных я спускаюсь ниже до cc_smartargs. Что мне еще нужно, так это основные данные об аргументах и, следовательно, настало время отступить к пакету более низкого уровня. Однако нет ничего страшного в том, что я не знаю какова структура этих cc_arguments типов. Я просто полагаю, что мне нужна запись для хранения всей информации о данных ALL_ARGUMENTS/DBMS_DESCRIBE (поэтому, запись, вероятно, не может быть %ROWTYPE, а должна быть типом, определенным пользователем), и мне необходим тип коллекция для хранения всех фрагментов отдельного параметра.

Примечание: как вы увидите, взглянув на спецификацию пакета для cc_smartargs, я представил выше структуры данных в строго обратном порядке, по отношению к тому, как они появляются (и должны появляться) в коде. Я двигался сверху вниз, чтобы объяснить структуры. Но, объявляя такие типы, вы должны двигаться снизу вверх – иначе компилятор не сможет разрешить ссылки.

Имея такие типы данных, можно объявить реальную коллекцию, которую я буду использовать в cc_smartargs, чтобы загрузить информацию об аргументах (как я делал в show_all_arguments) и сделать данные доступными для Codecheck:

CREATE OR REPLACE PACKAGE BODY cc_smartargs
IS
   -- "Сырые" данные об аргументах
   g_all_arguments   cc_arguments.arguments_tt;
 
   -- "Улучшенный" массив аргументов
   g_programs        programs_t; 

Я использовал префикс "g_", чтобы показать, что это глобальные структуры данных (и доступные только внутри) пакета cc_smartargs.

Заполнение многоуровневой коллекции

Давайте вернемся к процедуре compute_derived_information пакета cc_smartargs и рассмотрим логику, необходимую для заполнения этих новых структур.

Снова, я обойдусь без излишних подробностей моих шагов верхнего уровня в конструировании этой программы. Однако я хочу подчеркнуть важность сокрытия действительно сложных деталей - и синтаксиса - многоуровневых коллекций. Давайте начнем сначала с исполняемого раздела процедуры compute_derived_information. Снова я использую локальные модули, чтобы мои программы были небольшими и удобными для чтения.

Вместо того, чтобы пройтись по коду шаг за шагом, я расскажу вам его историю. Посмотрим, считаете ли вы, что мой рассказ действительно отражает код, который вы видите ниже.

Однажды жила-была прекрасная процедура, которая называлась compute_derived_information. Она отправилась в далекое путешествие по каждой строке списка g_all_arguments. Ее задачей было преобразовать плоские данные об аргументах в высоко-структурированные, многоуровневые данные - не такая простая задача для такой (на вид) маленькой процедуры. Когда она находила строку, представляющую процедуру без аргументов, она записывала этот факт в многоуровневую главную книгу, и продолжала свое путешествие. Когда она встречала информацию о возвращаемом значении, она мудро помещала все строки для этого возвращаемого значения в специальное место. Когда она обнаруживала новый параметр "верхнего уровня" она быстро собирала все фрагменты этого параметра и прятала их в безопасном месте. Скоро процедура завершила свой путь по полному списку данных об аргументах и смогла вернуться домой, чтобы жениться на принце:

1  BEGIN -- main compute_derived_information
 2     l_argindx := g_all_arguments.FIRST;
 3     LOOP
 4        EXIT WHEN l_argindx IS NULL;
 5
 6        IF cc_arguments.procedure_without_parameters (
 7              g_all_arguments (l_argindx))
 8        THEN
 9           record_the_procedure (l_argindx);
10        ELSIF cc_arguments.is_return_clause (
11                 g_all_arguments (l_argindx))
12        THEN
13           separate_return_clause_rows (l_argindx);
14        ELSIF cc_arguments.is_toplevel_parameter (
15                 g_all_arguments (l_argindx))
16        THEN
17           add_new_parameter (l_argindx);
18        END IF;
19     END LOOP;
20  END compute_derived_information;

Как видите, я весьма полагаюсь на программы пакета cc_arguments, чтобы получить ответы на конкретные вопросы, касающиеся строк информации из ALL_ARGUMENTS. И все сложности помещения значений в многоуровневый массив спрятаны за локальными процедурами. Это важный шаг, поскольку код, который вам необходимо написать может быть весьма труден для понимания.

Вместо того, чтобы объяснять одну за другой подпрограммы compute_derived_information, я пройду по add_new_parameter, чтобы продемонстрировать важность (и необходимость) сокрытия информации, когда она связана со сложными коллекциями.

Вот исполняемый раздел add_new_parameter:

1  BEGIN
 2     set_top_level_parameter (argindx_inout);
 3
 4     LOOP
 5        argindx_inout := g_all_arguments.NEXT (argindx_inout);
 6
 7        EXIT WHEN (   argindx_inout IS NULL
 8                   OR cc_arguments.is_toplevel_parameter (
 9                         g_all_arguments (argindx_inout))
10                  );
11
12        add_breakout (argindx_inout);
13
14        l_breakout_pos := l_breakout_pos + 1;
15     END LOOP;
16  END add_new_parameter;

Сначала, я разместил информацию о параметрах верхнего уровня; оглядываясь на описание коллекции TYPE можно догадаться, что она выполняет запись значений в поле верхнего уровня N-ой строки коллекции параметров для конкретной перегрузки. Отлично! Но в этом месте мне не надо беспокоиться о таких деталях. После того, как я загрузил эту информацию верхнего уровня, нужно взять все строки о фрагментах этого параметра и положить их в поля фрагментов N-ой строки … и так далее. Поэтому я начинаю числовой цикл FOR, который заканчивается, когда я достигаю строк аргументов, либо дохожу до следующего параметра верхнего уровня. Для каждой строки о фрагменте, я вызываю add_breakout, чтобы позаботиться о деталях.

Вот код для set_top_level_parameter:

 1  PROCEDURE set_top_level_parameter (argindx_in IN PLS_INTEGER)
 2  IS
 3  BEGIN
 4     g_programs (c_program) (c_overload)
 5        .PARAMETERS (c_position).toplevel :=
 6           g_all_arguments (argindx_in);
 7
 8     IF cc_arguments.not_defaulted (
 9           g_all_arguments (argindx_in))
10     THEN
11        g_programs (c_program) (c_overload)
12           .last_nondefault_parm :=
13               c_position;
14     END IF;
15  END set_top_level_parameter;

Как видите, код выглядит достаточно запутанным. Позвольте мне разобрать часть его для вас. Прежде всего, вы можете удивиться, что такое c_program и c_overload. Я объявил некоторые константы в заголовке add_new_parameter, чтобы: (a) сделать более понятным то, что эти значения не меняются внутри процедуры и (b) упростить код, который необходимо написать при ссылке на элементы коллекции. Вот объявления, начинающиеся с имени программы:

c_program  CONSTANT all_arguments.object_name%TYPE
   := g_all_arguments (argindx_inout).object_name;

А теперь - номер перегрузки: очень важно, что я положил это в константу, поскольку можно будет использовать сокращенную запись для преобразования NULL значения в 0.

c_overload CONSTANT PLS_INTEGER
   := NVL (g_all_arguments (argindx_inout).overload, 0);

Наконец, позиция параметра в списке параметров:

c_position CONSTANT PLS_INTEGER
   := g_all_arguments (argindx_inout).POSITION;

С помощью этих констант можно писать присваивания информации о параметрах верхнего уровня очень удобным для чтения образом внутри тела set_top_level_parameter. На рисунке 1 показаны эти присваивания, реформатированные, чтобы сделать различные уровни и шаги более прозрачными.

Второй шаг в set_top_level_parameter включает определение позиции последнего параметра, не имеющего значения по умолчанию, ключа для производства всех правильных вызовов перегрузки. Я вызываю программу cc_arguments, чтобы узнать имеет ли этот аргумент значение по умолчанию (конечно, не хочется жестко кодировать эту логику в cc_smartargs); и, если нет, то присваиваю позицию этого параметра полю с вычисляемым значением в записи перегрузки:

g_programs (c_program) (c_overload)
   .last_nondefault_parm :=  c_position;

Наконец, вот программа, которая действительно перемещает строку из коллекции g_all_arguments в соответствующую позицию фрагмента:

PROCEDURE add_breakout (argindx_in IN PLS_INTEGER)
IS
BEGIN
   g_programs 
     (c_program) 

        (c_overload)
           .PARAMETERS (c_position)
              .breakouts (l_breakout_pos) :=
                 g_all_arguments (argindx_in);
END add_breakout;

Будем надеяться, что вам совершенно понятен каждый из этих отдельных шагов, разбитых, как здесь, на простейшие, очевидные операции. Я уверяю вас, что будет гораздо труднее понять ту же самую логику, если все подробности будут открыты и свалены в один исполняемый раздел. Но к чему тратить слова? Я переписал add_new_parameter так, чтобы она не использовала никаких локальных, вложенных процедур, ни каких-либо предопределенных констант. Я просто написал то, что мне необходимо на каждом этапе пути. Результат можно увидеть в листинге 5.4.

Вот так так! Это какой-то ужасный код, не правда ли? Конечно, я могу сделать его несколько более понятным, добавив кучу комментариев, но это внесет избыточность в мой код. Если логика когда-нибудь изменится, мне придется менять и исполняемый раздел и комментарии.

Если, с другой стороны, я строго применяю поэтапную разработку во время фазы конструирования, мой код становится прозрачным. Он не требует комментариев, и программу гораздо легче отладить и улучшить. Разбивая сложную логику на отдельные элементы, я также уменьшаю вероятность того, что изменения одной части моей программы повлекут за собой изменения многих других частей.

Предоставление простого доступа к многоуровневой коллекции

Теперь у вас есть четкое понятие о том как:

Конечно, нет большого смысла заполнять такие коллекции, если вы не предоставите также способ получения информации из них. Поэтому, давайте посмотрим на некоторые программы в cc_smartargs, которые имеют отношение к предоставлению данных пользователю пакета, такого как Codecheck.

Но сначала напомню важные основные принципы работы:

Получив все это, давайте посмотрим, как можно создавать программы, чтобы ответить на следующие вопросы:

Является ли программа перегружаемой?

Должно существовать более одной программы с тем же именем, что означает, что в коллекции перегрузок g_programs существует более одной строки для заданного имени:

FUNCTION cc_smartargs.has_overloadings (
   program_in IN VARCHAR2
)
   RETURN BOOLEAN
IS

BEGIN
   RETURN g_programs (program_in).COUNT  >  1;
END;

Сколько параметров имеет программа?

Нужно посчитать количество элементов в коллекции параметров, которая является полем в записи перегрузки в N-ой позиции списка перегрузок программы:

FUNCTION parameter_count (
   program_in IN VARCHAR2, 
   overload_in IN PLS_INTEGER
)
   RETURN INTEGER
IS
BEGIN
   RETURN 
     g_programs 
       (program_in) 
         (overload_in).parameters.COUNT;
END;

Имеет ли перегрузка параметры?

Если количество строк в коллекции параметров для перегрузки равно 0, то нет.

FUNCTION cc_smartargs.has_no_parameters (
   program_in    IN   VARCHAR2,
   overload_in   IN   PLS_INTEGER
)
   RETURN BOOLEAN
IS
BEGIN
   RETURN 
     g_programs 
       (program_in) 
         (overload_in).parameters.COUNT = 0;
END;

Является ли перегрузка функцией или процедурой?

Иначе говоря, существуют ли данные в записи верхнего уровня поля return_clause для этой перегрузки? Если да, то должно существовать object_name.

FUNCTION is_function (
   program_in IN VARCHAR2, 
      overload_in IN PLS_INTEGER
)
   RETURN BOOLEAN
IS
BEGIN
   RETURN 
      g_programs 
         (program_in) 
            (overload_in)
               .return_clause.toplevel.object_name 
               IS NOT NULL;
END;

Как получить первую перегрузку заданной программы?

Надо просто использовать метод FIRST коллекции перегрузок этой программы:

FUNCTION first_overloading (program_in IN VARCHAR2)
   RETURN PLS_INTEGER
IS
BEGIN
   RETURN g_programs (program_in).FIRST;
END;

Как получить первый правильный вызов заданной перегрузки?

Первый правильный вызов определяется позицией первого параметра, не имеющего значения по умолчанию, следовательно:

FUNCTION first_invocation (
   program_in    IN   VARCHAR2,
   overload_in   IN   PLS_INTEGER
)
   RETURN PLS_INTEGER
IS
BEGIN
   RETURN 
     g_programs 
       (program_in) 
         (overload_in)
           .last_nondefault_parm;
END;

Маленькие простые программы - будете ли вы их писать, поддерживать или использовать. И вместе с тем, мы рассмотрели наиболее интересные и важные элементы пакета. Я призываю вас изучить полный исходный код пакета более подробно, и обратить внимание на дополнительные программы (такие как процедура show_args ).

В следующей статье я погружусь еще глубже в иерархию пакета и использую пакет cc_arguments для предстоящих уроков по конструированию кода.

Таблица 10

Компоненты cc_smartargs

Назначение

cc_smartargs.is_function (program_in, overload1_in)

Принимает имя программы и номер перегрузки и возвращает TRUE , если эта конкретная перегрузка является функцией.

cc_smartargs.load_arguments (package_in)

Для заданного пакета загружает всю информацию об аргументах в структуры данных пакета cc_smartargs, неопределенные в настоящее время.

cc_smartargs.ispackage

Возвращает TRUE,если загруженная в настоящее время программа является пакетом (можно загрузить информацию об аргументах для процедуры или функции).

cc_smartargs.parameter_list (program_in,overload1_in,1,perm1_in)
RETURN
cc_arguments.arguments_tt

Возвращает список информации об аргументах, реально появляющихся в списке параметров.

cc_smartargs.first_permutation (program_in,check_this_in)

Возвращает первую перестановку (набор параметров) для заданной перегрузки.

cc_smartargs.last_permutation (program_in,check_this_in)

Возвращает последнюю перестановку (набор параметров) для заданной перегрузки.

cc_smartargs.first_overloading (l_program_name)

Возвращает первую перегрузку для заданной программы.

cc_smartargs.last_overloading (program_in)

Возвращает последнюю перегрузку для заданной программы.

cc_smartargs.first_program

Возвращает первую программу (неповторяющееся имя программы) для заданного пакета.

cc_smartargs.has_overloadings (l_program_name)

Возвращает TRUE, если программа пакета является перегружаемой.

cc_smartargs.next_program (l_program_name)

Возвращает следующую программу (неповторяющееся имя программы) для заданного пакета.

Листинг 5.1

1  PROCEDURE load_arrays
2  IS
3    l_parm_position   PLS_INTEGER;
4    l_breakout_position   PLS_INTEGER;
5
6    PROCEDURE reset_for_new_parameter (argindx_in IN PLS_INTEGER)
7    IS
8    BEGIN
9       IF l_arguments (argindx_in).data_level = 0
10       THEN
11          l_parm_position := l_arguments (argindx_in).POSITION;
12          l_breakout_position := 0;
13       END IF;
14    END reset_for_new_parameter;
15  BEGIN
16    OPEN arguments_cur;
17    FETCH arguments_cur BULK COLLECT INTO l_arguments;
18
19    FOR argindx IN l_arguments.FIRST .. l_arguments.LAST
20    LOOP
21       reset_for_new_parameter (argindx);
22
23       l_breakout_position := l_breakout_position + 1;
24
25       l_programs
26         (l_arguments(argindx).object_name)
27            (NVL (l_arguments(argindx).overload, 0))
28               (l_parm_position)
29                  (l_breakout_position):= l_arguments(argindx);
30    END LOOP;
31  END load_arrays;

Листинг 5.2

1  PROCEDURE dump_multilevel_array
2  IS
3    l_program_index all_arguments.object_name%TYPE;
4
5    PROCEDURE show_overloadings (
6       programs_in      IN   programs_t,
7       object_name_in   IN   all_arguments.object_name%TYPE
8    )
9    IS
10       l_overloading_index       PLS_INTEGER;
11
12       PROCEDURE show_parameters (top_level_parms_in IN parameters_t)
13       IS
14          l_parm_sequence  PLS_INTEGER;
15
16          PROCEDURE show_breakouts (breakouts_in IN breakouts_t)
17          IS
18             l_breakout_index PLS_INTEGER  := breakouts_in.FIRST;
19          BEGIN -- main show_breakouts
20             LOOP
21                EXIT WHEN l_breakout_index IS NULL;
22                
23                l_breakout_index := breakouts_in.NEXT (l_breakout_index);
24             END LOOP;
25          END show_breakouts;
26       BEGIN -- main show_parameters
27          l_parm_sequence := top_level_parms_in.FIRST;
28          LOOP
29             EXIT WHEN parm_sequence IS NULL;
30             show_breakouts (
31                top_level_parms_in (l_parm_sequence));
32             l_parm_sequence := top_level_parms_in.NEXT (l_parm_sequence);
33          END LOOP;
34       END show_parameters;
35    BEGIN -- main show_overloadings
36       l_overloading_index := programs_in (object_name_in).FIRST;
37       LOOP
38          EXIT WHEN l_overloading_index IS NULL;
39          show_parameters (
40             programs_in (object_name_in) (l_overloading_index));
41          l_overloading_index :=
42             programs_in (object_name_in).NEXT (l_overloading_index);
43       END LOOP;
44    END show_overloadings;
45  BEGIN -- main dump_multilevel_array
46    l_program_index := l_programs.FIRST;
47    LOOP
48       EXIT WHEN l_program_index IS NULL;
49       show_overloadings (l_programs, l_program_index);
50       l_program_index := l_programs.NEXT (l_program_index);
51    END LOOP;
52	END dump_multilevel_array;

Листинг 5.3

SQL> exec show_all_arguments ('allargs_test.noparms2')

Dump of ALL_ARGUMENTS for "allargs_test.noparms2"

Object     OvLd Lev Pos Type            Name           Mode
---------- ---- --- --- --------------- ------------- ------
NOPARMS2     1   0    1 VARCHAR2        ARG1           IN
NOPARMS2     1   0    2 VARCHAR2        ARG2           IN
NOPARMS2     2   0    1 VARCHAR2        ARG1           IN
NOPARMS2     2   0    2 VARCHAR2        ARG2           IN
NOPARMS2     2   0    3 PL/SQL RECORD   ARG3           IN
NOPARMS2     2   1    1 NUMBER          PERSON_ID   	IN
NOPARMS2     2   1    2 VARCHAR2        PERSON_NM    	IN

Dump of Multi-level Collection of ALL_ARGUMENTS for "allargs_test.noparms2"
 
Package SCOTT.ALLARGS_TEST.NOPARMS2 - # of Distinct Programs = 1
  Name NOPARMS2 - # of Overloadings = 2
    Overloading 1 - # of Arguments = 2
      Parameter 1 - # of Breakouts = 1
          ARG1(VARCHAR2) Lvl-Pos: 0-1
      Parameter 2 - # of Breakouts = 1
          ARG2(VARCHAR2) Lvl-Pos: 0-2
    Overloading 2 - # of Arguments = 3
      Parameter 1 - # of Breakouts = 1
          ARG1(VARCHAR2) Lvl-Pos: 0-1
      Parameter 2 - # of Breakouts = 1
          ARG2(VARCHAR2) Lvl-Pos: 0-2
      Parameter 3 - # of Breakouts = 3
          ARG3(PL/SQL RECORD) Lvl-Pos: 0-3
          PERSON_ID(NUMBER) Lvl-Pos: 1-1
          PERSON_NM(VARCHAR2) Lvl-Pos: 1-2

Листинг 5.4

1  PROCEDURE add_new_parameter (argindx_inout IN OUT PLS_INTEGER)
2  IS
3     l_breakout_pos   PLS_INTEGER := 1;
4  BEGIN
5     g_programs
6        (g_all_arguments (argindx_inout).object_name)
7           (NVL (g_all_arguments (argindx_inout).overload,0))
8              .PARAMETERS
9                (g_all_arguments (argindx_inout).POSITION)
10                   .toplevel := g_all_arguments (argindx_in);
11
12     IF cc_arguments.not_defaulted (
13           g_all_arguments (argindx_in))
14     THEN
15        g_programs
16           (g_all_arguments (argindx_inout).object_name)
17              (NVL (g_all_arguments (argindx_inout).overload, 0))
18                .last_nondefault_parm :=
19                     g_all_arguments (argindx_inout).POSITION;
20     END IF;
21
22     LOOP
23        argindx_inout := g_all_arguments.NEXT (argindx_inout);
24
25        EXIT WHEN (   argindx_inout IS NULL
26                   OR cc_arguments.is_toplevel_parameter (
27                         g_all_arguments (argindx_inout))
28                  );
29
30        g_programs
31           (g_all_arguments (argindx_inout).object_name)
32              (NVL (g_all_arguments (argindx_inout).overload,0))
33                 .PARAMETERS (g_all_arguments (argindx_inout).POSITION)
34                    .breakouts (l_breakout_pos) :=
35                        g_all_arguments (argindx_in);
36        l_breakout_pos := l_breakout_pos + 1;
37     END LOOP;
38  END add_new_parameter;

От редакции:
Окончание публикации переводов статей (6-8) Стивена Фейерштейна
“Построение утилиты анализа кода” в следующем выпуске
интернет-журнала FORS Magazine