Совершенный код. Совершенный код. Мастер-класс. Стив Макконнелл. Руководство по стилю программирования и конструированию по
Скачать 5.88 Mb.
|
ГЛАВА 18 Табличные методы 405 18.1. Основные вопросы применения табличных методов При определенных обстоятельствах табличный код проще, чем сложные логические выражения, легче поддается изменению и эффективнее. До# пустим, вы хотите классифицировать символы, выделив буквы, знаки пре# пинания и цифры. Вы можете использовать сложную логическую последователь# ность вроде этой: Пример использования сложной логики для классификации символов (Java) if ( ( ( ‘a’ <= inputChar ) && ( inputChar <= ‘z’ ) ) || ( ( ‘A’ <= inputChar ) && ( inputChar <= ‘Z’ ) ) ) { charType = CharacterType.Letter; } else if ( ( inputChar == ‘ ‘ ) || ( inputChar == ‘,’ ) || ( inputChar == ‘.’ ) || ( inputChar == ‘!’ ) || ( inputChar == ‘(‘ ) || ( inputChar == ‘)’ ) || ( inputChar == ‘:’ ) || ( inputChar == ‘;’ ) || ( inputChar == ‘?’ ) || ( inputChar == ‘’ ) ) { charType = CharacterType.Punctuation; } else if ( ( ‘0’ <= inputChar ) && ( inputChar <= ‘9’ ) ) { charType = CharacterType.Digit; } Если бы вместо этого фрагмента вы использовали таблицу подстановки, то помес# тили бы тип каждого элемента в массив и обращались бы к нему по коду символа. Сложный фрагмент кода, представленный выше, заменялся бы на такое выражение: Пример использования таблицы подстановки для классификации символов (Java) charType = charTypeTable[ inputChar ]; Этот фрагмент предполагает, что массив charTypeTable был заранее заполнен. Вы поместили знания, доступные программе, в данные, а не в логику: в таблицу, а не в условия if. Два вопроса применения табличных методов При применении табличных методов перед вами стоят два основных вопроса. Во# первых, вам надо решить, как будет выполняться поиск записей в таблице. Вы можете использовать какие#либо данные для прямого доступа к таблице. Так, если вам нужно систематизировать данные по месяцам, то выбор ключа для таблицы месяцев очевиден. Вы можете использовать массив с индексом от 1 до 12. Другие данные затруднительно использовать для прямого поиска таблич# ной записи. Так, для классификации информации по номеру социального страхования (SSN) вы не можете использовать этот номер в качестве ключа непосредственно, если, конечно, вы не собираетесь хранить в таблице 999#99#9999 записей. Вам понадобится более сложный подход. Вот какие спосо# бы, применяются для поиска записи в таблице: 406 ЧАСТЬ IV Операторы прямой доступ; индексированный доступ; ступенчатый доступ. Каждый из этих вариантов доступа подробно описан ниже. Второй вопрос, который нужно решить при использовании табличных методов: что хранить в таблице. Иногда результатом поиска в таблице являются данные — тогда можно хранить в таблице сами данные. Если же результатом поиска является действие, код, который описывает это действие, можно хранить, а в некоторых языках можно хранить ссылку на метод, выпол# няющий это действие. В каждом из этих случаев таблицы усложняются. 18.2. Таблицы с прямым доступом Как и все таблицы подстановки, таблицы с прямым доступом предназначены для замены более сложных логических структур. Они имеют «прямой доступ», пото# му что вам не нужно ходить по кругу, чтобы найти в таблице необходимую ин# формацию. Вы можете непосредственно выбрать нужную запись (рис. 18#1). Рис. 18'1. Как следует из ее имени, таблица с прямым доступом позволяет обращаться к требуемому элементу напрямую Пример определения количества дней в месяце Допустим, вы хотите получить число дней в месяце (для простоты забудем о ви# сокосных годах). Разумеется, создание большого условия if — неуклюжий способ решения этой проблемы: Пример неуклюжего способа определения количества дней в месяце (Visual Basic) If ( month = 1 ) Then days = 31 ElseIf ( month = 2 ) Then days = 28 ElseIf ( month = 3 ) Then days = 31 ElseIf ( month = 4 ) Then days = 30 ElseIf ( month = 5 ) Then days = 31 ElseIf ( month = 6 ) Then ГЛАВА 18 Табличные методы 407 days = 30 ElseIf ( month = 7 ) Then days = 31 ElseIf ( month = 8 ) Then days = 31 ElseIf ( month = 9 ) Then days = 30 ElseIf ( month = 10 ) Then days = 31 ElseIf ( month = 11 ) Then days = 30 ElseIf ( month = 12 ) Then days = 31 End If Более простой и удобный для модификации способ выполнения тех же самых действий — разместить данные в таблице. В Visual Basic первым делом нужно за# полнить таблицу: Пример элегантного способа определения количества дней в месяце (Visual Basic) ‘ Инициализируем таблицу данными о количестве дней в месяцах. Dim daysPerMonth() As Integer = _ { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 } А теперь вместо создания длинного выражения if для выяснения числа дней в месяце можно просто обратиться к элементу массива: Пример элегантного способа определения количества дней в месяце на Visual Basic (продолжение) days = daysPerMonth( month1 ) Если вы хотите учитывать високосные годы в этой версии табличного поиска, код все еще будет простым. Допустим, LeapYearIndex() возвращает 0 или 1: Пример элегантного способа определения количества дней в месяце на Visual Basic (продолжение) days = daysPerMonth( month1, LeapYearIndex() ) Если бы в версии с выражением if тоже учитывались високосные годы, то длин# ная строка условий if еще более усложнилась бы. Определение количества дней в месяце — удобный пример, так как переменную month можно использовать для поиска записи в таблице. Для прямого доступа к таблице зачастую можно использовать данные, которые могли бы управлять пос# ледовательностью if#выражений. Пример со ставками страхования Допустим, вы пишете программу для вычисления ставок медицинского страхова# ния, которые варьируются в зависимости от возраста, пола, семейного положе# 408 ЧАСТЬ IV Операторы ния и от того, курит ли страхователь. Если бы вы писали для этих ставок логиче# скую управляющую структуру, то получилось бы нечто вроде этого: Пример неуклюжего способа расчета ставки страхования (Java) if ( gender == Gender.Female ) { if ( maritalStatus == MaritalStatus.Single ) { if ( smokingStatus == SmokingStatus.NonSmoking ) { if ( age < 18 ) { rate = 200.00; } else if ( age == 18 ) { rate = 250.00; } else if ( age == 19 ) { rate = 300.00; } else if ( 65 < age ) { rate = 450.00; } else { if ( age < 18 ) { rate = 250.00; } else if ( age == 18 ) { rate = 300.00; } else if ( age == 19 ) { rate = 350.00; } else if ( 65 < age ) { rate = 575.00; } } else if ( maritalStatus == MaritalStatus.Married ) } Эта сокращенная версия логической структуры — хорошая иллюстрация того, насколько сложной может получиться программа. Она не учитывает замужних женщин, всех мужчин и большинства возрастов между 18 и 65 годами. Вы можете вообразить, насколько сложной станет эта структура, если запрограммировать всю таблицу ставок. Вы можете сказать: «Да, но почему вы проверяете каждый возраст? Почему бы не поместить ставку для каждого возраста в массив?» Хороший вопрос, и одним из очевидных усовершенствований будет размещение ставок для каждого возраста в отдельных массивах. ГЛАВА 18 Табличные методы 409 Однако лучшее решение — создать массив ставок не только для каждого возрас# та, но вообще для всех факторов. Вот как объявить такой массив на Visual Basic: Пример объявления данных для заполнения таблицы ставок страхования (Visual Basic) Public Enum SmokingStatus SmokingStatus_First = 0 SmokingStatus_Smoking = 0 SmokingStatus_NonSmoking = 1 SmokingStatus_Last = 1 End Enum Public Enum Gender Gender_First = 0 Gender_Male = 0 Gender_Female = 1 Gender_Last = 1 End Enum Public Enum MaritalStatus MaritalStatus_First = 0 MaritalStatus_Single = 0 MaritalStatus_Married = 1 MaritalStatus_Last = 1 End Enum Const MAX_AGE As Integer = 125 Dim rateTable ( SmokingStatus_Last, Gender_Last, MaritalStatus_Last, _ MAX_AGE ) As Double Определив массив, необходимо придумать способ его за# полнения. Вы можете использовать операторы присваива# ния, читать данные из дискового файла, вычислять данные или делать что#то еще. После подготовки данные могут при# меняться при расчете ставок. Сложная логическая струк# тура, показанная ранее, заменяется простым выражением, например: Пример элегантного способа определения ставки страхования (Visual Basic) rate = rateTable( smokingStatus, gender, maritalStatus, age ) Основное преимущество этого подхода — в замене сложной логики табличным поиском. Такой код удобней читать и проще изменять. Перекрестная ссылка Одно из преимуществ табличного подхо- да в том, что можно поместить данные из таблицы в файл и читать его во время выполне- ния. Это позволит вам изменять такие параметры, как ставки страхования, не изменяя саму программу (см. раздел 10.6). 410 ЧАСТЬ IV Операторы Пример гибкого формата сообщения Таблицу можно использовать для реализации такой логики, которая слишком динамична для представления в коде. В примерах по классификации символов, количеству дней в месяцах и страховым ставкам вы хотя бы знали, что можете в случае необходимости написать длинную строку условий if. Однако иногда дан# ные слишком сложны, чтобы жестко закодировать их с помощью операторов if. Если вам кажется, что вы поняли принцип работы таблиц с прямым доступом, можете пропустить следующий пример. Тем не менее он немного сложнее пре# дыдущих и продолжает демонстрацию мощности табличных подходов. Допустим, вы разрабатываете метод для печати сообщений, хранящихся в файле. Обычно файл содержит около 500 сообщений, которые бывают примерно 20 видов. Изначально сообщения поступают от бакенов и включают в себя информацию о температуре воды, расположении бакена и т. д. Каждое сообщение имеет несколько полей и начинается с заголовка, содержаще# го идентификатор, позволяющий узнать, с каким из примерно 20 видов сообще# ний вы имеете дело (рис. 18#2). Рис. 18'2. Сообщения хранятся в произвольном порядке, каждое определяется идентификатором Сообщения имеют переменный формат, определяемый заказчиком, и вы не мо# жете заставить его стабилизировать формат (рис. 18#3). ГЛАВА 18 Табличные методы 411 Рис. 18'3. За исключением идентификатора каждое сообщение имеет свой формат Логический подход Используя логический подход, вы, вероятно, прочитали бы каждое сообщение, проверили его идентификатор, а затем вызвали метод, разработанный для чтения, преобразования и печати каждого сообщения. Имей вы 20 типов сообщений, вы создали бы 20 методов. Для поддержки пришлось бы написать неизвестное коли# чество методов более низкого уровня. Так, вы могли бы создать метод PrintBuoy% TemperatureMessage() для печати сообщения о температуре. Объектно#ориентиро# ванный подход не дал бы никаких преимуществ: скорее всего вы задействовали бы абстрактный объект, представляющий сообщение, и породили от него подклас# сы для каждого типа сообщения. При каждом изменении формата какого#нибудь сообщения вам пришлось бы менять логику в соответствующем классе или методе. Если в приведенном выше содержимом сообщения поле со средней температурой поменяло бы тип с пла# вающей запятой на иной, вам пришлось бы изменить логику метода PrintBuoyTempe% ratureMessage(). (А если бы изменился тип самого бакена, вам бы пришлось раз# рабатывать класс для нового!) В логическом подходе метод для чтения сообщений состоит из цикла, читающе# го каждое сообщение, декодирующего его идентификатор, а затем вызывающего на основе этого идентификатора один из 20 методов. Вот пример псевдокода логического подхода: Пока есть сообщения для чтения Прочитать заголовок сообщения Декодировать идентификатор сообщения из заголовка Если заголовок сообщения соответствует типу 1, то напечатать сообщение 1го типа. Иначе, если заголовок сообщения соответствует типу 2, то напечатать сообщение 2го типа. Перекрестная ссылка Этот псев- докод низкого уровня исполь- зуется в иных целях, нежели псевдокод, предназначенный для проектирования метода. О разработке с помощью псевдо- кода см. главу 9. 412 ЧАСТЬ IV Операторы Иначе, если заголовок сообщения соответствует типу 19, то напечатать сообщение 19го типа. Иначе, если заголовок сообщения соответствует типу 20, то напечатать сообщение 20го типа. Текст этого псевдокода приводится не полностью — понять его смысл можно и без просмотра всех 20 вариантов. Объектно-ориентированный подход При использовании механического объектно#ориентированного подхода логи# ка была бы скрыта в структуре унаследованных объектов, но основная структура была бы столь же сложной: Пока есть сообщения для чтения, прочитать заголовок сообщения. Декодировать идентификатор сообщения из заголовка. Если заголовок сообщения соответствует типу 1, то создать объект сообщения 1го типа. Иначе, если заголовок сообщения соответствует типу 2, то создать объект сообщения 2го типа. Иначе, если заголовок сообщения соответствует типу 19, то создать объект сообщения 19го типа. Иначе, если заголовок сообщения соответствует типу 20, то создать объект сообщения 20го типа. Конец Если Конец цикла Пока Независимо от того, будет ли логика написана непосредственно или реализова# на в специальных классах, каждое из 20 видов сообщений будет иметь собствен# ный метод печати. Каждый такой метод тоже можно изобразить с помощью псев# докода. Вот его пример для метода, считывающего и печатающего сообщение о температуре бакена: Напечатать «Сообщение о температуре бакена». Прочитать значение с плавающей запятой. Напечатать «Средняя температура». Напечатать значение с плавающей запятой. Прочитать значение с плавающей запятой. Напечатать «Диапазон температур». Напечатать значение с плавающей запятой. Прочитать целое значение. Напечатать «Количество проб». Напечатать целое значение. Прочитать символьную строку. Напечатать «Местонахождение». Напечатать символьную строку. ГЛАВА 18 Табличные методы 413 Прочитать время суток. Напечатать «Время измерения». Напечатать время суток. Это код только для одного типа сообщений. Для каждого из оставшихся 19 типов нужно реализовать похожий код. И если будет добавлен 21#й тип сообщения, потребуется добавить 21#й метод или подкласс — в любом случае новый тип со# общения потребует изменения существующего кода. Табличный подход Табличный подход экономичнее предыдущего. Метод чтения сообщений состо# ит из цикла, который считывает заголовок каждого сообщения, декодирует его идентификатор, находит описание сообщения в массиве Message, а затем всегда вызывает один и тот же метод для декодирования сообщения. Этот подход позво# ляет описать формат каждого сообщения в форме таблицы, а не задавать его же# стко в логике программы. Это упрощает первоначальное программирование, со# здает меньше кода и облегчает сопровождение программы без изменения кода. Применение этого подхода начинается с перечисления типов сообщений и типов полей. В C++ вы можете определить типы всех возможных полей таким образом: Пример определения типов данных сообщения (C++) enum FieldType { FieldType_FloatingPoint, FieldType_Integer, FieldType_String, FieldType_TimeOfDay, FieldType_Boolean, FieldType_BitField, FieldType_Last = FieldType_BitField }; Вместо жестко закодированных методов печати каждого из 20 видов сообщений можно создать горстку функций для печати основных типов данных: чисел с пла# вающей точкой, целых чисел, символьных строк и т. д. Вы можете описать содер# жимое каждого типа сообщения в таблице (с указанием имени каждого поля), а затем декодировать все сообщения на основе этого описания. Элемент таблицы, содержащий сведения об одном типе сообщений, может выглядеть так: Пример определения элемента таблицы, описывающего сообщение Message Begin NumFields 5 MessageName “Buoy Temperature Message” Field 1, FloatingPoint, “Average Temperature” Field 2, FloatingPoint, “Temperature Range” Field 3, Integer, “Number of Samples” Field 4, String, “Location” Field 5, TimeOfDay, “Time of Measurement” Message End 414 ЧАСТЬ IV Операторы Эта таблица может быть жестко закодирована в программе (в этом случае значе# ния всех элементов будут присвоены переменным) или читаться из файла при запуске программы или позже. Поскольку определения сообщений поступают в программу извне, то вместо вне# дрения информации в логику программы мы внедрили ее в данные. Данные обычно гибче программной логики: их легко изменять, если меняется формат сообщения. Если нужно добавить новый вид сообщений, вы можете просто добавить еще один элемент в таблицу данных. Вот псевдокод цикла верхнего уровня для табличного подхода: Первые три строки такие же, как и при логическом подходе. Пока есть сообщения для чтения, прочитать заголовок сообщения, декодировать идентификатор сообщения из заголовка, найти описание сообщения в таблице описаний сообщений, прочитать поля сообщения и напечатать их, основываясь на описании сообщения. Конец цикла Пока В отличие от псевдокода при логическом подходе в этом случае псевдокод не сокращен, так как логика гораздо проще. Логика более низкого уровня содержит метод, который интерпретирует сообщение на основе таблицы описаний сооб# щений, считывает данные сообщения и печатает его. Этот метод более общего вида, чем методы печати сообщений при логическом подходе, но он не намного слож# нее и заменяет собой все 20 методов: Пока не все поля напечатаны, получить тип поля из описания сообщения. Выбор ( типа поля ) вариант: ( число с плавающей запятой ) прочитать значение с плавающей запятой, напечатать метку поля, напечатать значение с плавающей запятой. вариант: ( целое число ) прочитать целое значение, напечатать метку поля, напечатать целое значение. вариант: ( символьная строка ) прочитать символьную строку, напечатать метку поля, напечатать символьную строку. вариант: ( время суток ) прочитать время суток, напечатать метку поля, напечатать время суток. вариант: ( логическое значение ) прочитать значение флажка, |