
Ноябрь 2004
Профессионалу разработчику
Стивен Фернстайн
Член OTN с 2001
Делаем все правильно со второго раза
(Getting It Right the Second Time,
by Steven Feuerstein)
Статья 8 из серии Фернстайна “Построение утилиты анализа кода”
Источник:
http://otn.oracle.com/pub/articles/feuerstein_codech8.html
[От редакции OM/RE: Переводы предыдущих семи статей серии
опубликованы в следующих выпусках OM/RE:
В каждой статье автор дает содержание всей серии:
|
Шаги по созданию утилиты
Ниже приведено краткое содержание статей, которые будут появляться на Oracle Technology Network (OTN) в течении следующих нескольких месяцев:
Статья 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 и подготовленные мной тестовые данные, чтобы выполнить тестирование. Когда я обнаружу проблемы в коде, я буду не только исправлять их, но и открывать возможности улучшения внутренней работы моих программ, этот процесс называется рефакторинг.
|
Потратьте немного времени на рефакторинг своего кода после того как он написан – это позволит вам избежать ошибок и проблем при эксплуатации.
Даже если с самого начала вы уделяли достаточно внимания дизайну программы, он только выиграет от некоторого систематического обзора и прочистки, после того как код уже написан – этот процесс называется refactoring (рефакторинг). Если вы читали эту серию, то знаете, что мы следуем строгому нисходящему процессу проектирования при разработке утилиты Codecheck, которая анализирует пакеты на предмет неоднозначной перегрузки (ситуации, в которой две или более программы с одним и тем же именем имеют параметры настолько похожие, что компилятор PL/SQL не может их различить). В этой последней статье серии, посвященной Codecheck, мы применим концепцию рефакторинга к некоторым разделам Codecheck, которые, в результате, значительно улучшатся – станут более понятными, простыми и легкими в обслуживании. Затем мы обсудим основные уроки, извлеченные из процесса разработки Codecheck.
Что такое “рефакторинг”?
В 1999 Мартин Фоулер опубликовал книгу под названием “Refactoring: Improving the Design of Existing Code” (“Рефакторинг:Улучшение дизайна существующего кода”) (изд. Addison-Wesley). В этой книге он определил рефакторинг как:
Процесс изменения программной системы таким образом, чтобы это не изменяло внешнего поведения кода, а только улучшало его внутреннюю структуру… Это дисциплинированный способ почистить код, который минимизирует вероятность внесения ошибки. По существу, когда вы выполняете рефакторинг, вы улучшаете дизайн кода, после того как он написан.
Рефакторинг – очень глубокая и полезная книга. В ней рассматривается дисциплинированный поэтапный подход для идентификации обычно встречающихся слабых мест в дизайне кода. Затем предлагается систематический, последовательный подход для исправления дизайна, все без воздействия на пользователя вашего кода.
Книга Фоулера объясняет технику рефакторинга, используя в качестве языка программирования Java. (Концепция рефакторинга возникла внутри програмистского сообщества Smalltalk, но Java похож на Smalltalk, поскольку он тоже является объектно-ориентированным языком.) Одного этого факта должно быть достаточно, чтобы убедить вас провести день или два, изучая основы Java программирования. Я рекомендую книгу Брюса Экелса “Думать на Java” (Thinking in Java, Bruce Eckels, Prentice-Hall, 2002) как лучший способ быстро понять язык. Чтобы научиться читать Java код не потребуется много времени – и тогда вы сможете узнать из “Рефакторинга” все виды прекрасных способов улучшения своего PL/SQL кода!
Объем статьи не позволяет нам обсудить особенности различных способов улучшения кода, которые предлагает Фоулер в своей книге, но мы можем воспользоваться основными концепциями рефакторинга в некоторых конкретных примерах для улучшения уже написанного кода. Наша задача – найти места в Codecheck, в которых можно сделать следующее:
- Упростить, где это делается (реорганизовать структуру кода и переместить код из одного места в другое, более подходящее)
- Упростить, как это делается (сделать код короче, более элегантным и понятным)
По ходу дела мы увидим, что эти цели часто переплетаются. Реорганизуя структуру кода, мы часто заканчиваем улучшением реализации ее логики – и наоборот.
Все на своих местах
Когда вы будете просматривать структуру уже написанного кода, поищите такие разделы, которые находятся не в самых подходящих местах. Например, можно обнаружить сходную логику, появляющуюся более одного раза, - это указывает на то, что можно объединить эту логику в одном месте. Или, вы можете заметить большую концентрацию вызовов программ другого пакета – в таком случае естественно возникает вопрос: возможно, весь код принадлежит этому пакету? В пакете Codecheck один такой раздел весьма очевиден, взгляните на частную функцию same_program_types (см. Листинг 1).
Функция принимает имя программы и указатели на две различных перегрузки для этого имени программы. Она возвращает TRUE, если обе эти перегрузки имеют один тип (где типом может быть либо PROCEDURE, либо FUNCTION); иначе она возвращает FALSE. Вот пример того, как функция вызывается из пакета Codecheck:
IF same_program_types (
program_in
,check_this_ovld_in
,against_this_ovld)
THEN
compare_all_invocations (...);
END IF;
Теперь давайте посмотрим, что происходит внутри самой функции (Листинг 1). В разделе декларации (строки с 7 по 12), вызывается программа cc_smartargs.is_function, которая определяет, является ли каждая из перегрузок функцией. Затем в теле функции (оператор RETURN, показанный в строках 14-18), проверяется, являются ли эти перегрузки обе функциями или обе процедурами. Это единственная возможность, доступная нам, по крайней мере в таком написании!
Как можно видеть, главная работа этой функции выполняется функцией другого пакета cc_smartargs.is_function: В таком случае, почему бы не перенести функцию same_program_types тоже в этот пакет? Более общая формулировка этого вопроса звучит следующим образом: принадлежит ли функциональность same_program_types пакету высокого уровня, или она принадлежит другому, более узко специализированному пакету Codecheck, такому как cc_smartargs?
Пакет cc_smartargs создан для того, чтобы спрятать как можно больше деталей о содержании ALL_ARGUMENTS — и сделать данные более “разумными”, чем те, что хранятся в ALL_ARGUMENTS. Для того, чтобы добиться этой цели, я определил четырехуровневую коллекцию, верхний уровень которой использует строковый индекс, основанный на имени программы. Второй уровень вложенности индексируется перегрузкой каждой программы. Как можно увидеть, взглянув на заголовок same_program_types (Листинг 1, строки 1 - 5), список параметров этой функции отражает эти два уровня вложенности. Если пакет cc_smartargs предназначен для того, чтобы спрятать эту информацию, имеет ли смысл раскрывать детали cc_smartargs, помещая функцию same_program_types на верхний уровень пакета Codecheck?
Я так не думаю. Глядя на завершенный пакет Codecheck, я вижу теперь, что функцию same_program_types необходимо переместить в пакет cc_smartargs. Шаги, из которых состоит рефакторинг в этом случае, ясны:
- Взять реализацию функции same_program_types из Codecheck и перенести ее в тело пакета cc_smartargs (желательно непосредственно под функцию is_function, поскольку эти две функции тесно связаны).
- Добавить описание этой функции в спецификацию пакета cc_smartargs.
- Изменить все вызовы same_program_types в Codecheck так, чтобы использовать версию cc_smartargs.
Эти шаги обычно включают вырезание и вставку (осторожно!), поэтому я не буду надоедать вам подробностями. Единственным изменением в Codecheck (кроме уменьшения общего размера пакета) является добавление префикса – имени нового пакета-владельца функции к вызову same_program_types, как показано ниже:
IF cc_smartargs.same_program_types (
program_in
,check_this_ovld_in
,against_this_ovld)
THEN
compare_all_invocations (...);
END IF;
Итак я закончил? Не совсем. Теперь, когда я обратил более пристальное внимание на эту функцию, я поражен объемом и сложностью кода, который написан для того, чтобы выполнить то, что кажется обычной простой операцией. Это привело меня к другой цели в рефакторинге программы…
Делай проще
В общем случае, чем проще ваш код, тем легче его отлаживать, поддерживать и улучшать. Поэтому, если у вас когда-нибудь появляется чувство, что этот раздел вашей программы чрезвычайно сложен, следуйте своей интуиции и ищите способы упростить его. При рефакторинге ищите разделы, которые кажутся слишком длинными и более сложными, чем они должны быть, судя по тому, для чего они предназначены.
В случае функции same_program_types второй взгляд привел меня к мысли о том, как проще реализовать эту же логику. Новая реализация использует булевское выражение более прямым и интуитивно понятным способом:
FUNCTION same_program_types (
program_in IN VARCHAR2
,overload1_in IN PLS_INTEGER
,overload2_in IN PLS_INTEGER)
RETURN BOOLEAN
IS
BEGIN
RETURN (
is_function (program_in, overload1_in) =
is_function (program_in, overload2_in));
END same_program_types;
Обратите внимание, что эта новая версия same_program_types имеет едва ли половину тех строк, что были в старой версии (показанной в Листинге 1), и, кроме того, более элегантную логику.
И значит, рефакторинг same_program_types (и пакета, в котором она более подходяще описана) завершен. В процессе рефакторинга я удалил внешние ссылки (вызовы других пакетов) внутри функции same_program_types, сделав функцию модульной. Я также чрезвычайно упростил реализацию, облегчив поддержку кода в будущем.
Теперь, давайте посмотрим на другой рефакторинг программы Codecheck, иллюстрирующий полезную стратегию реорганизации и оптимизации кода.
Спрячем детали
При рефакторинге функции мы начали с вопроса: где код расположен, и закончили, упростив то, как была реализована логика работы кода. Другим полезным подходом при рефакторинге является поиск слишком длинных разделов кода, которые можно упростить, спрятав детали (переместив их в низкоуровневый пакет или программу) – снова изменяя, где и как располагается код, чтобы получить более рациональную и понятную программу.
При построении Codecheck я пытался дисциплинировать себя, чтобы иметь дело только с небольшим количеством сложности за раз. Я надеялся на нисходящий дизайн (также известный как пошаговая детализация), чтобы спрятать детали до тех пор, пока у меня не возникало необходимости обратиться к ним, прикладывал все усилия, чтобы все исполняемые секции были маленькими и удобными для чтения. Однако, есть и некоторые недоработки, которые быстро обнаружились в процессе рефакторинга, когда я просматривал свой код в поисках слишком длинных исполняемых разделов. (Все, что длинее, чем 20 – 30 строк довольно легко обнаружить, когда большинство исполняемых разделов являются короткими!) Давайте посмотрим на одну такую ошибку и исправим ее, чтобы улучшить как удобство сопровождения, так и удобство чтения.
Программа check_for_similarity в Codecheck является центральным элементом пакета – но действительно ли она должна быть длиной 37 строк (см. Листинг 2)? Она перебирает параметры двух перегрузок и сравнивает семейства типов данных для каждого параметра. Если программа обнаруживает, что список параметров “слишком похож” (например, если список параметров не содержит параметров, или если в нем одинаковое количество параметров одинакового типа), то она сообщает о перегрузке, вызывая cc_report.ambig_ovld.
Посмотрев на эти 37 строк кода в беспристрастном свете процесса рефакторинга, я был сразу поражен двумя аспектами кода:
- Условная логика, которая определяет, являются ли параметры слишком похожими, показалась мне слишком сложной. Я проверяю различные случаи, устанавливая флаг too_similar. Затем, в зависимости от значения этого флага, выдается сообщение. Действительно ли необходимы эти две фазы, чтобы сделать эту работу? Мне кажется, что это можно сделать гораздо проще.
- Вызов cc_report.ambig_ovld слишком длинный и непонятный! В частности, что должна означать следующая строка для любого человека (даже для автора, после того как он не работал с кодом месяц или два)?
cc_util.ifelse (l_numargs1 = 0, 0, 1)
Сейчас я могу объяснить, что я делаю в этой строке; я могу даже использовать именованную нотацию в моем вызове ambig_ovld, чтобы сделать его более понятным. Однако, код в нижней строке далек от того, чтобы быть самоочевидным. Кроме того, меня вовсе не радует необходимость дополнительных комментариев или присутствие такой сложной логики в списке параметров программы. Эти красные флаги показывают мне, что механизм сообщений спроектирован не достаточно удобным для использования способом.
Пора делать рефакторинг!
Прежде всего, конечно, необходимо понять, чего я пытаюсь достичь логикой cc_util.ifelse. Это сводится к следующему: в особом случае, когда одна из анализируемых программ не имеет никаких параметров, мне необходимо передать 0 в качестве местоположения первого параметра; иначе – начальная позиция – 1 . Итак, это кажется достаточно понятным, не так ли? Не совсем. После близкого знакомства с использованием и реализацией ambig_ovld я вскоре обнаружил, что пытаюсь заставить одну эту программу делать “двойную работу” - сообщать как об общем случае неоднозначной перегрузки, так и о специальном случае, в котором ни одна из перегрузок не имеет аргументов.
В этой ситуации проще всего создать вторую программу сообщений для обработки специального случая. Поэтому, я создал новую программу в cc_report, которая обрабатывает случай с отсутствием аргументов, и является ничем иным как прямой инкапсуляцией исходной ambig_ovld:
PROCEDURE cc_report.ambig_ovld_noargs (
owner_in IN VARCHAR2,
package_name_in IN VARCHAR2,
object_name_in IN VARCHAR2,
overload1_in IN PLS_INTEGER,
overload2_in IN PLS_INTEGER,
)
IS
BEGIN
cc_report.ambig_ovld (
owner_in => owner_in,
package_name_in => package_name_in,
object_name_in => object_name_in,
overload1_in => overload1_in,
startarg1_in => 0,
endarg1_in => 0,
overload2_in => overload2_in,
startarg2_in => 0,
endarg2_in => 0
);
END;
Здесь я всего лишь спрятал необходимость передачи нуля в качестве значений для позиций первого и последнего аргументов. В результате, я могу предложить значительно упрощенный список параметров в программе сообщений.
Имея в запасе эту новую программу сообщений для специального случая, можно вернуться к исходной программе check_for_similarity и реструктурировать исполняемый раздел, сделав его более простым и понятным. Кроме сокрытия подробностей сообщения о специальном случае, я решил спрятать некотрую сложную логику сравнения параметров, переместив ее в новый локальный модуль compare_each_parameter (Листинг 3). В результате этих изменений тело программы check_for_similarity сократилось с 37 строк (Листинг 2) до легко понятных 17 строк, представленных ниже:
1 BEGIN
2 IF l_numargs1 != l_numargs2
3 THEN
4 NULL;
5 ELSIF l_numargs1 = 0 AND l_numargs2 = 0
6 THEN
7 cc_report.ambig_ovld_noargs (
8 cc_smartargs.owner_name,
9 cc_smartargs.package_name,
10 program_in,
11 overload1_in,
12 overload2_in
13 );
14 ELSE
15 compare_each_parameter;
16 END IF;
17* END;
Обратите внимание, насколько этот блок кода более ясно отражает смысл нижележащей логики, по сравнению со старой версией в Листинге 2. Я избавился от громоздкого флага too_similar. Теперь условная логика ведет прямо к решению: либо нет необходимости в сообщении (потому что две перегрузки имеют различное количество аргументов, следовательно, они различны), либо вызывается соответствующая логика сообщений. В специальном случае “отсутствия аргументов” я немедленно вызываю свою новую программу сообщений. Иначе говоря, я переместил всю более сложную логику сравнения каждого параметра в отдельный локальный модуль с весьма понятным названием: compare_each_parameter.
Взглянув ближе на процедуру compare_each_parameter (Листинг 3), мы видим, что она содержит всю логику, прежде выставленную в исполняемой секции check_for_similarity. Обратите внимание, что в процессе перемещения кода я также перемещал из внешней программы все объявления переменных, которые использовались только в новой локальной программе. Этот маленький шаг важен, чтобы избежать возможной путаницы и ошибок в будущем.
Обратите также внимание, что вызов cc_report.ambig_ovld стал проще. Больше нет смущающих вызовов в середине списка параметров (встроенный условный оператор обсуждался в Статье 7, “Реализация “Поставщика сервисов”: создание универсального пакета отчетности”). Процедура ambig_ovld теперь непосредственно обрабатывает случай, для которого она была создана (сообщение о перегрузках с нетривиальным списком параметров), и программист вовсе не обязан знать о логике обработки специального случая.
Окончательная версия вызова Codecheck для сообщения о неоднозначной перегрузке выглядит следующим образом:
1 cc_report.ambig_ovld (
2 cc_smartargs.owner_name
3 ,cc_smartargs.package_name
4 ,program_in
5 ,overload1_in
6 ,1
7 ,lastarg1_in
8 ,overload2_in
9 ,1
10 ,lastarg2_in
11 );
Извлеченные уроки
Несмотря на все внимание, проявленное при разработке Codecheck, код, тем не менее, предоставляет массу возможностей для улучшения с использованием техник рефакторинга, таких как оптимизация структуры, упрощение реализации логики, и сокрытие подробностей для того, чтобы сделать код более удобным для чтения. Можно пройти через десятки рефакторингов и еще останется повод для десятков последующих.
Должен ли я расстраиваться из-за этого кажущегося бесконечным цикла анализа и улучшения? Вовсе нет! Что я понял за многие годы моего близкого знакомства с PL/SQL, так это следующее: всегда есть способ улучшить то, что я делал раньше. Это не обязательно замечания или критика ранее выполненной работы, которая была сделана (надо полагать) настолько хорошо насколько я мог в то время. Главное заключается в том, что теперь я знаю больше, чем я знал раньше – и я могу действовать, используя это новое знание, чтобы улучшить свое искусство программирования.
Другой важный урок, который я извлек при написании Codecheck, заключается в том, что дисциплина играет решающую роль при создании программного обеспечения. Хотя я верю, что в нашей работе есть, и должна быть, большая доля творчества, нам также необходимо внести как можно больше логичности и структурированности в это творчество. Одним из способов обеспечения такой логичности является соглашение о наименованиях, однако руководящие принципы, которые глубже определяют структурные аспекты, являются даже более важными.
В Codecheck основной целью применения структурной дисциплины было создание маленьких исполняемых разделов. В тысячах строк кода, составляющих Codecheck, едва ли можно найти исполняемый раздел, состоящий более чем из 20 или 30 строк кода. Результат этих усилий – как во время написания, так и во время рефакторинга – это код, чрезвычайно удобный для чтения, документирующий сам себя и почти прозрачно легкий для исправления и улучшения.
Еще один заключительный урок состоит в том, что я всегда должен быть в курсе новых возможностей Oracle, которые могут улучшить способ создания кода. Например, в базе данных Oracle9i новое представление словаря данных ALL_PROCEDURES содержит полезную информацию о программах, как отдельных, так и пакетных. До версии Oracle9i, была доступна только часть этой информации программного уровня – и только косвенно – через ALL_ARGUMENTS (которую я использую в качестве источника информации для Codecheck, поскольку я не знал об ALL_PROCEDURES). Теперь эту информацию можно получить напрямую и гораздо более разумную из ALL_PROCEDURES. Я приглашаю своих читателей разобраться, как интегрировать ALL_PROCEDURES в Codecheck, поскольку это предоставляет прекрасную возможность поупражняться в рефакторинге!
Таковы уроки, которым я научился в этой серии, когда я проводил вас шаг за шагом через создание новой сложной утилиты. Я надеюсь, что это путешествие было полезным также и для вас – и что это благотворно скажется на вашем будущем коде, также как и на моем.
|