Файл: Руководство по стилю программирования и конструированию по.pdf
ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.11.2023
Просмотров: 806
Скачиваний: 2
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
ГЛАВА 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 методов:
Пока не все поля напечатаны,
получить тип поля из описания сообщения.
Выбор ( типа поля )
вариант: ( число с плавающей запятой )
прочитать значение с плавающей запятой,
напечатать метку поля,
напечатать значение с плавающей запятой.
вариант: ( целое число )
прочитать целое значение,
напечатать метку поля,
напечатать целое значение.
вариант: ( символьная строка )
прочитать символьную строку,
напечатать метку поля,
напечатать символьную строку.
вариант: ( время суток )
прочитать время суток,
напечатать метку поля,
напечатать время суток.
вариант: ( логическое значение )
прочитать значение флажка,
ГЛАВА 18 Табличные методы
415
напечатать метку поля,
напечатать значение флажка.
вариант: ( битовое поле )
прочитать битовое поле,
напечатать метку поля,
напечатать битовое поле.
Конец Выбора
Конец цикла Пока
Нужно признать, что этот метод с шестью вариантами выбора длиннее, чем от- дельный метод для печати температуры. Но это единственный метод, который вам необходим. Вам не нужны остальные 19 функций для остальных 19 типов сооб- щений. Данный метод обрабатывает шесть типов полей, и обслуживает все виды сообщений.
Этот метод также иллюстрирует наиболее сложный способ реализации таблич- ного поиска, так как использует оператор
case. Другой подход — создание абст- рактного класса
AbstractField и последующее наследование от него подклассов для каждого типа поля. Тогда вам не понадобится оператор
case, вы сможете вызы- вать метод-член соответствующего объектного типа.
Вот как можно создать такие объекты на C++:
Пример создания объектных типов (C++)
class AbstractField {
public:
virtual void ReadAndPrint( string, FileStatus & ) = 0;
}
class FloatingPointField : public AbstractField {
public:
virtual void ReadAndPrint( string, FileStatus & ) {
}
}
class IntegerField ...
class StringField ...
Этот фрагмент объявляет во всех классах метод, принимающий строковый пара- метр и параметр типа
FileStatus.
Следующий шаг — объявление массива для хранения набора объектов. Этот мас- сив и есть таблица для поиска. Вот как она выглядит:
Пример создания таблицы для хранения объектов каждого типа (C++)
AbstractField* field[ Field_Last ];
Последний шаг в настройке таблицы объектов — заполнение массива
Field кон- кретными объектами:
416
ЧАСТЬ IV Операторы
Пример заполнения списка объектов (C++)
field[ Field_FloatingPoint ] = new FloatingPointField();
field[ Field_Integer ] = new IntegerField();
field[ Field_String ] = new StringField();
field[ Field_TimeOfDay ] = new TimeOfDayField();
field[ Field_Boolean ] = new BooleanField();
field[ Field_BitField ] = new BitFieldField();
В этом коде предполагается, что
FloatingPointField и другие идентификаторы с правой стороны выражений присваивания — это имена объектов, унаследован- ных от
AbstractField. Присваивание объектов элементам массива означает, что вы сможете вызвать правильную версию метода
ReadAndPrint(), обращаясь к элементу массива, а не используя конкретный тип объекта напрямую.
Подготовив таблицу методов, можно обрабатывать поле сообщения с помощью простого обращения к таблице объектов и вызова одного из методов-членов этих объектов. Код может выглядеть так:
Пример выбора объектов и их методов из таблицы (C++)
Это строки — служебный код, необходимый для обработки каждого поля в сообщении.
fieldIdx = 1;
while ( ( fieldIdx <= numFieldsInMessage ) and ( fileStatus == OK ) ) {
fieldType = fieldDescription[ fieldIdx ].FieldType;
fieldName = fieldDescription[ fieldIdx ].FieldName;
Это — обращение к таблице, в результате которого будет вызван метод, зависящий от типа поля:
он просто выбирается в таблице объектов.
field[ fieldType ].ReadAndPrint( fieldName, fileStatus );
}
Помните первоначальные 34 строки псевдокода табличного поиска, содержаще- го оператор
case? Если вы замените оператор case таблицей объектов, то это весь код, который вам нужен для обеспечения той же функциональности. Невероят- но, но это также весь код, необходимый для замены всех 20 отдельных методов,
применяемых при логическом подходе. Более того, если описания сообщений читаются из файла, то новые типы сообщений не потребуют изменений кода, если только не будут содержать новых типов полей.
Вы можете использовать такой подход в любом объектно-ориентированном язы- ке. Он менее подвержен ошибкам, легче в сопровождении и эффективнее длин- ных выражений
if, операторов case или огромного количества подклассов.
Сам факт, что проект использует наследование и полиморфизм, не делает его хорошим проектом. Механический объектно-ориентированный дизайн, описан- ный в разделе «Объектно-ориентированный подход», потребовал бы такого же большого объема кода, как и механический функциональный дизайн, а может, и больше. Такой подход скорее усложнил бы решение, чем упростил. В данном слу- чае основная суть проектного решения не в объектной и не в функциональной ориентации, а в использовании хорошо продуманной таблицы поиска.
>
>
1 ... 47 48 49 50 51 52 53 54 ... 104
ГЛАВА 18 Табличные методы
417
Подгонка значений ключа
Во всех трех предыдущих примерах вы могли использовать данные в качестве ключа для прямого обращения к таблице. То есть можно было указать перемен- ную
messageID как ключ без всяких изменений, переменную month в примере количества дней в месяцах, а также
gender, maritalStatus и smokingStatus в приме- ре ставок страхования.
Было бы хорошо всегда обращаться к таблице напрямую, потому что это просто и быстро. Однако не всегда данные для этого годятся. В примере со ставками стра- хования переменная
age не очень удобна в качестве ключа. Первоначальная ло- гика определяла одну ставку для лиц моложе 18 лет, индивидуальные ставки для возрастов от 18 до 65 и одну ставку для людей старше 65. Это означает, что для возрастов от 0 до 17 и от 66 и выше нельзя использовать возраст как ключ напря- мую, если таблица хранит только один набор ставок для нескольких лет.
Это приводит к обсуждению вопроса подгонки значений ключа в таблице поис- ка. Подогнать ключ можно несколькими способами.
Продублировать информацию, чтобы использовать ключ напрямую
Один прямолинейный способ заставить
age работать ключом в таблице ставок —
продублировать все ставки для лиц, моложе 18, для каждого возраста от 0 до 17, а затем использовать возраст для прямого обращения к таблице. То же самое мож- но сделать и для возрастов от 66 лет и старше. Преимущество этого подхода в том,
что структура таблицы остается простой и доступ к данным так же прост. Если нужно добавить специальное значение ставки для некоторого возраста, меньше- го 17, вы можете просто изменить табличное значение. Недостаток этого метода в том, что дублирование приведет к напрасным затратам на хранение избыточ- ной информации, а также увеличит вероятность появления ошибок в таблице хотя бы потому, что таблица будет содержать избыточные данные.
Преобразовать ключ, чтобы использовать его напрямую Второй способ задействовать
Age в качестве прямого ключа — применить к переменной Age неко- торую функцию, которая позволит это делать. В данном случае такая функция дол- жна преобразовывать все возрасты от 0 до 17 к какому-то одному значению, ска- жем, 17, а возрасты старше 66 — к другому, например, 66. В данном случае такое преобразование легко выполнить с помощью функций
min() и max(). Так, для со- здания табличного ключа в диапазоне от 17 до 66 можно использовать выражение:
max( min( 66, Age ), 17 )
Реализация функции трансформации требует хорошего понимания структуры данных, которые вы хотите применить как ключ, и это не всегда так просто, как использование функций
min() и max(). Допустим, в этом примере ставки меня- ются через интервалы не в 5 лет, а в 1 год. Если только вы не хотите дублировать все данные по пять раз, вам придется написать функцию, которая делит
Age на 5
и использует методы
min() и max().
Изолируйте преобразование ключа в собственном методе Если вам нуж- но подгонять данные для использования в качестве табличного ключа, помести- те операции, трансформирующие данные в ключ, в отдельный метод. Его исполь- зование исключает возможность применения разных преобразований в разных
418
ЧАСТЬ IV Операторы местах. Это упростит модификацию при изменении функции преобразования.
Хорошее имя процедуры, такое как
KeyFromAge(), также прояснит и задокументи- рует назначение математических махинаций.
Если ваша среда предоставляет готовые варианты преобразования ключа, исполь- зуйте их. Например, класс
HashMap в языке Java позволяет создавать пары «ключ- значение».
18.3. Таблицы с индексированным доступом
Иногда простого математического преобразования недостаточно для перехода от таких данных, как
Age к значению ключа. Некоторые из таких случаев подходят для схем с индексным доступом.
Применяя индексы, вы используете исходные данные для поиска ключа в индексной таблице, а затем значение из этой таблицы служит для поиска интересующих вас данных.
Допустим, вы заведуете складом, и у вас около 100 наименований товара. Далее предположим, что каждый товар имеет четырехзначный номер в диапазоне от 0000
до 9999. В этом случае, если вы захотите задействовать номер товара в качестве ключа для прямого доступа к таблице, описывающей какой-то признак каждого товара, вам придется создать индексный массив с 10 000 записей (от 0 до 9999).
Этот массив в основном будет пустым за исключением 100 элементов, соответ- ствующих номерам товаров на вашем складе. Как показано на рис. 18-4, эти эле- менты указывают на таблицу с описанием товаров, содержащую гораздо менее
10 000 записей.
Рис. 18-4. В отличие от таблиц с прямым доступом для обращения к таблице
с индексным доступом используется промежуточный индекс