ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.10.2023
Просмотров: 418
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
}
152 Глава 5
Этот метод — прямая адаптация функции из главы 3, поэтому здесь нет ничего нового, что требовалось бы объяснить. Однако адаптация в метод класса все же демонстрирует некоторые проект- ные решения. Первое, на что можно обратить внимание, состоит в том, что мы не создали нового поля класса для хранения буквенных оценок, но определяем соответствующую буквенную оценку на лету в ответ на каждый запрос. Альтернативный подход предполагает соз- дание поле класса _letterGrade и изменение метода setGrade для об- новления поля _letterGrade вместе с _grade. Тогда метод letterGrade стал бы простым методом get, возвращающим значение уже опреде- ленного поля класса.
Причины такого подхода лежат в избыточности данных . Этот тер- мин описывает ситуацию, когда хранимые данные либо являются в буквальном смысле дубликатом других данных, либо могут быть пря- мо получены из других данных. Эта проблема обычно возникает в базах данных, и их проектировщики стараются избежать создания избыточных данных в своих таблицах. Однако при нашей неосто- рожности избыточность данных может появиться в любой програм- ме. Рассмотрим программу, хранящую медицинские записи о воз- расте и дате рождения пациентов. Дата рождения пациента несет в себе информацию, которой нет в данных о возрасте. Эти два набо- ра данных не тождественны, но возраст не говорит нам ничего, что мы не могли бы получить из даты рождения. А что если эти два па- раметра не согласуются между собой (что может, в конечном счете, произойти, если возраст не рассчитывается автоматически)? Како- му значению мы верим? Мне вспоминается знаменитое (хотя веро- ятно апокрифическое) высказывание халифа Умара, произнесенное при приказе сжечь Александрийскую библиотеку. Он воскликнул, что если книги в библиотеке согласны с Кораном, то они избыточ- ны и их не стоит хранить, но если они не согласны с Кораном, они пагубные и их следует уничтожить. Избыточность данных – это ожи- даемая проблема. Единственным оправданием может быть произво- дительность, когда мы полагаем, что изменения поля _grade будут редки, а вызовы letterGrade часты, однако тяжело представить об- щее существенное увеличение производительности программы.
Однако этот метод можно улучшить. Тестируя его, я заметил проб лему. Хотя метод выдает верные результаты для корректных зна- чений поля _grade, метод перестает работать в случае отрицатель- ного значения переменной _grade. При выполнении цикла while от- рицательное значение поля _grade немедленно приводит к ошибке.
Следовательно, переменная category остается нулевой и выражение return пытается обратиться к GRADE_LETTER[ –1]. Мы могли бы из- бежать этой проблемы, инициализировав переменную category еди- ницей вместо нуля, но это означало бы, что отрицательная отметка связана с «F», при том что она не должна быть связана ни с какой строкой, поскольку некорректное значение оценки не подходит ни- какой категории.
Решение задач с классами 153
Вместо этого мы могли бы проверить поле _grade до преобра- зования его в буквенную оценку. Мы уже проверяем значение оцен- ки в методе setGrade, так что вместо добавления нового провероч- ного кода в метод letterGrade, нам следует «выявить» общую часть кода в этих методах и сделать третий метод. (Возможно, вы удиви- тесь, откуда может взяться некорректная оценка, если мы проверя- ем оценки при их установке. Однако мы могла решить устанавливать некорректную оценку в конструкторе как сигнал об отсутствии леги- тимной оценки.) Это еще один служебный метод, являющийся экви- валентом на уровне класса концепции общей вспомогательной функ- ции, предложенной в предыдущей главе. Давайте реализуем этот метод и исправим другие наши методы:
Xbool studentRecord::YisValidGrade(Zint grade) {
if ((grade >= 0) &&
(grade <= 100))
return true;
else return false;
}
void studentRecord::setGrade(int newGrade) {
if (
[isValidGrade(newGrade))
_grade = newGrade;
}
string studentRecord::letterGrade() {
if (
\!isValidGrade(_grade)) return "ERROR";
const int NUMBER_CATEGORIES = 11;
const string GRADE_LETTER[] = {"F", "D", "D+", "C–", "C", "C+", "B–",
"B", "B+", "A–", "A"}; const int LOWEST_GRADE_SCORE[] = {0, 60, 67, 70, 73, 77, 80, 83, 87,
90, 93};
int category = 0;
while (category < NUMBER_CATEGORIES &&
LOWEST_GRADE_SCORE[category]
<= _grade)
category++;
return GRADE_LETTER[category — 1];
}
Тип нового метода проверки оценок bool
X
и, поскольку его значе- ние да-или-нет, я выбрал название isValidGrade
Y
. Тем самым достига- ется наиболее англоязычное чтение вызовов этого метода, такое как в методах setGrade
[
и letterGrade
\
. Также обратите внимание, что ме- тод принимает оценку для проверки как параметр
Z
. Хотя letterGrade уже проводит проверку значения в поле класса _grade, setGrade прове- ряет значение, которое может быть установлено в поле класса. Таким образом, чтобы быть полезным обоим методам, метод isValidGrade должен принимать оценку в качестве параметра.
Хотя метод isValidGrade уже реализован, один вопрос по его пово- ду остался: какой у него должен быть уровень доступа? То есть должны ли мы поместить его в публичную или приватную секцию класса? В от- личие от методов set и get базового фреймворка класса, которые всег- да помещаются в публичной секции, служебные методы могут быть в зависимости от их использования, как публичными, так и приватными .
Чего мы добьемся, объявив метод isValidGrade публичным? Очевидно,
154 Глава 5
клиентский код получит доступ к этому методу. Поскольку кажется, что, чем больше в классе публичных методов, тем он полезнее, многие на- чинающие программисты объявляют публичными все методы, кото- рые могут использоваться клиентом. Однако такой подход игнорирует обратную сторону назначения публичного доступа. Помните, что пуб- личная секция определяет интерфейс нашего класса и мы не сможем изменить метод после того, как класс будет встроен в одну или несколь- ко программ, поскольку с большой вероятностью такие изменения по- требуют имений во всем клиентском коде. Таким образом, размещение метода в публичной секции фиксирует интерфейс метода и его резуль- таты. Предположим, в нашем случае, что некоторый клиентский код, основываясь на исходном коде в методе isValidGrade, ориентируется на диапазон оценок от 0 до 100, но впоследствии правила выставления оценок становятся более сложными. Клиентский код в этом случае мо- жет перестать работать. Чтобы избежать этого, нам, видимо, придется создать второй проверочный метод внутри класса, не трогая первый.
Предположим, мы планируем, что метод isValidGrade не будет широко использоваться клиентом, и решаем не делать его публич- ным. Мы могли бы объявить его приватным, но это не единственный вариант. Поскольку функция не ссылается непосредственно на поля класса или другие методы класса, мы могли бы объявить функцию во- обще вне класса. Однако в данном случае мы сталкиваемся не только с такими ограничениями, которые публичный доступ налагает на из- меняемость, но также снижаем инкапсуляцию, потому что теперь эта функция, требуемая классом, не является его частью. Также мы могли бы оставить метод в классе, но сделать его защищенным вместо при- ватного. Разница будет видна в подклассе. Если метод isValidGrade защищенный, его смогут вызывать методы подклассов; если метод isValidGrade приватный, его могут использовать только другие мето- ды класса studentRecord. Это та же дилемма выбора между публичным и приватным доступом, но в меньшем масштабе. Ожидаем ли мы, что наш метод будет очень полезен методам в подклассе, и ожидаем ли мы, что его результаты или интерфейс могут измениться в будущем?
В большинстве случаев наиболее безопасный вариант — объявить все служебные методы приватными и оставить публичным только те слу- жебные методы, которые написаны для клиента.
Классы с динамическими данными
Одна из важнейших причин создания класса лежит в инкапсуляции структур с динамическими данными . Как мы обсудили в главе 4, про- граммисты сталкиваются с реальной проблемой отслеживания ди- намического выделения памяти , назначения указателей и освобож- дения памяти для того, чтобы мы могли избежать утечек памяти, висячих ссылок и ссылок на неверную память. Упаковка всех ссылок на указатели в класс не устраняет кропотливую работу, но сделав ее один раз хорошо, мы можем безопасно вставлять этот код в другие
Решение задач с классами 155
проекты. Также это означает, что все проблемы, связанные со струк- турами с динамическими данными, изолированы внутри класса, тем самым облегчая отладку.
Давайте создадим класс с динамическими данными и посмотрим, как это работает. Для нашей задачи мы используем модифицирован- ную версию основной задачи главы 4.
:
$
В этой задаче вы создадите класс с методами для хранения и манипулирования кол- лекцией записей студентов. Запись студента содержит номер студента, оценку, оба параметра целочисленные, и фамилию студента (строковая переменная). Необходи- мо реализовать следующие функции:
addReord
Этот метод принимает номер, фамилию и оценку студента и добавля- ет новую запись с этими данными в коллекцию.
reordWithNumber
Эта функция принимает номер студента и возвращает из коллекции запись для студента с этим номером.
removeReord
Эта функция принимает номер студента и удаляет из коллекции запись студента с этим номером.
Коллекция может быть любого размера. Ожидается, что операция addReord будет выполняться часто, потому она должна быть реализована эффективно.
Основное отличие между этим описанием и исходной версией в том, что мы добавили новую операцию, recordWithNumber, и таким образом ни одна из операций не требует в качестве параметра ука- затель. Это главный выигрыш от использования класса для инкап- суляции связного списка. Клиент может знать, что класс реализует коллекцию записей о студентах в виде связного списка и может даже учитывать это (помните нашу дискуссию об ограничениях сокры- тия). Однако клиентский код непосредственно не взаимодействует ни со связным списком, ни с каким-либо указателем в классе.
Поскольку в этой задаче необходимо хранение той же информа- ции, как и в предыдущей, у нас есть возможность использовать класс еще раз. В нашем связном списке узлового типа вместо отдельных полей для каждой из частей информации о студенте, мы используем один объект studentRecord. Использование объекта одного класса в качестве типа данных во втором классе называется композиция .
У нас достаточно информации для предварительного объявле- ния класса:
class studentCollection {
private:
X struct studentNode {
Y studentRecord studentData;
studentNode * next;
};
Z public:
studentCollection();
156 Глава 5
void addRecord(studentRecord newStudent);
studentRecord recordWithNumber(int idNum);
void removeRecord(int idNum);
private:
[ typedef studentNode * studentList;
\ studentList _listHead;
};
Ранее я говорил о тенденции программистов начинать классы с публичных объявлений, однако здесь мы сделаем исключение. Нач- нем с приватного объявления узла struct studentNode
X
, который будем использовать для создания связного списка. Это объявление должно быть раньше публичной секции, поскольку некоторые пуб- личные функции класса ссылаются на этот тип. В отличие от узла в главе 4, у этого узла нет индивидуальных полей для полезных данных, но он включает в себя члена класса studentRecord
Y
. Публичные функ- ции класса следуют напрямую из условия задачи. Кроме того, у нас, как всегда, есть конструктор . Во второй приватной секции мы для яс- ности объявили псевдоним typedef
[
для указателя на наш узел, так как мы делали в главе 4. Затем мы объявили указатель на начало спи- ска , разумно названный _listHead
\
Этот класс объявляет два приватных типа. Классы могут объяв- лять типы данных также как и функции класса и поля класса. Как и любые другие члены класса, типы, появляющиеся в классе, могут быть объявлены с любым спецификатором доступа. Хотя, что касает- ся полей класса, вы должны по умолчанию объявлять их приватными и только в случае явных причин делать их менее закрытыми. Объявле- ние типов обычно лежит в основе того, как класс действует за сценой, и, как следствие, они имеют жизненно важное значение для сокры- тия. Кроме того, в большинстве случаев клиентскому коду не нужны типы, которые вы объявили в своем классе. Исключения бывают тог- да, когда тип, объявленный в классе, используется как тип возвращае- мого значения в публичном методе или как тип параметра публичного метода. В этом случае тип должен быть публичным или клиентский код не сможет воспользоваться публичным методом. Класс student-
Collection предполагает, что структура типа studentRecord будет от- дельно объявлена, но мы могли бы сделать ее частью класса. Если бы мы это сделали, нам надо было бы объявить ее в секции public.
Теперь мы можем реализовать методы нашего класса, начав с конструктора. В отличие от предыдущего примера, у нас будет толь- ко конструктор по умолчанию. Конструктора, принимающего пара- метры для инициализации полей, в классе не будет. Основная цель нашего класса спрятать детали связного списка, поэтому мы не хо- тим, чтобы клиент даже думал о _listHead, не говоря уж о манипуля- ции с ним. Все что нам надо сделать в конструкторе по умолчанию, это установить NULL как значение указателя на начало списка:
studentCollection::studentCollection() {
listHead = NULL;
}
Решение задач с классами 157
Добавление узла
Перейдем к функции addRecord. Поскольку условия задачи не требу- ют хранения записей о студентах в каком-то особом порядке, мы мо- жем непосредственно адаптировать функцию addRecord из главы 4. void studentCollection::addRecord(
XstudentRecord newStudent) {
YstudentNode * newNode = new studentNode;
ZnewNode-> studentData = newStudent;
[newNode->next = _listHead;
\_listHead = newNode;
}
Между этим кодом и исходной функцией всего два отличия. Здесь нам достаточно только одного параметра в списке
X
. Этим параме- тром является объект studentRecord, который мы собираемся добавить в нашу коллекцию. Этот объект инкапсулирует все данные о студен- те, тем самым уменьшая количество необходимых параметров. Кроме того, нам не надо передавать указатель на начало списка , поскольку он уже хранится в нашем классе как _listHead и мы обращаемся к нему по необходимости. Как и в функции addRecord из главы 4, мы создаем но- вый узел
Y
, копируем данные по новому студенту в новый узел
Z
, ука- зываем в новом узле в поле «следующий» на предыдущий первый узел списка
[
, и наконец, устанавливаем указатель _listHead на новый узел
\
. Обычно я рекомендую составлять схемы для всех манипуляций с ука- зателями, однако, поскольку мы повторяем уже проделанные нами ма- нипуляции, мы можем воспользоваться старыми схемами.
Теперь обратимся ко второй функции класса в списке, record-
WithNumber
. Это название слегка труднопроизносимо, и, возможно, некоторые программисты выбрали retrieveRecord или что-то по- добное. Однако, следуя своим изложенным выше принципам наи- менования, я решил использовать существительное, поскольку этот метод возвращает значение. Этот метод будет похож на метод aver- ageRecord
, поскольку ему также потребуется пробежаться по списку; однако отличие будет в том, что как только мы обнаружим соответ- ствующую студенческую запись, мы сможем остановиться.
studentRecord studentCollection::recordWithNumber(int idNum) {
X studentNode * loopPtr = _listHead;
Y while (loopPtr->studentData.studentID() != idNum) {
loopPtr = loopPtr->next;
}
Z return loopPtr->studentData;
}
В этой функции мы инициализируем указатель нашего цикла на начале списка
X
и пробегаем по списку, пока не обнаружим желаемый идентификатор
Y
. Наконец, достигнув желаемого узла, мы возвраща- ем всю соответствующую запись как значение функции
Z
. Этот код выглядит отлично, однако, как всегда, нужно учесть специальные слу- чаи . Случай, который всегда необходимо рассматривать, имея дело со