ВУЗ: Не указан
Категория: Не указан
Дисциплина: Не указана
Добавлен: 30.10.2023
Просмотров: 423
Скачиваний: 1
ВНИМАНИЕ! Если данный файл нарушает Ваши авторские права, то обязательно сообщите нам.
СОДЕРЖАНИЕ
146 Глава 5
Позднее в этой главе мы увидим специфические примеры это- го, однако основной принцип максимизации читабельности — это при написании любой части интерфейса класса постоянно думать о клиент ском коде.
Выразительность
Конечная цель хорошо спроектированного класса состоит в выра- зительности или в том, что в широком смысле может быть названо
«писабельностью» — легкостью, с которой код пишется. Однажды на- писанный хороший класс облегчает написание остального кода так же, как делает это хорошая функция. Классы эффективно расширяют язык, становясь высокоуровневыми партнерами для основных низко- уровневых инструментов, таких как циклы, инструкции if и так да- лее. В языке C++ даже основной функционал, такой как ввод и вывод, не является неотъемлемыми частями языка, а реализован как набор классов, которые необходимо явно включать в программу для их ис- пользования. При помощи классов программные действия, которые раньше требовали большого количества этапов, могут быть реализо- ваны за несколько или вообще за один шаг. Решая задачи, мы должны присвоить этой цели особый приоритет. Мы всегда должны думать:
«Как этот класс позволит облегчить написание остальной программы или будущих программ, которые смогут его использо вать?»
Создание простого класса
Теперь, когда мы знаем, какие цели наш класс должен преследовать, наступило время применить теорию на практике и создать некото- рый класс. Прежде всего, мы разработаем наш класс для решения следующей задачи.
:
Спроектируем класс или набор классов для использования в программе, которая поддерживает список класса. Для каждого студента будем хранить его фамилию, идентификатор и результат выпускного экзамена в числовом диапазоне от 0 до 100.
Программа должна позволять добавлять и удалять записи, отображать записи по кон- кретному студенту, идентифицируемому по ID с оценкой, представленной в числовом или буквенном формате, и отображать среднюю оценку класса. Соответствующая буквенная оценка для каждого результата представлена в табл. 5.1.
Табл. 5.1. Буквенные оценки
,=C=ƒ%… !ƒ3 2=2%"
3"……= % …=
93–100
A
90–92
A–
87–89
B+
83–86
B
80–82
B–
Решение задач с классами 147
77–79
C+
73–76
C
70–72
C–
67–69
D+
60–66
D
0–59
F
Мы начнем с рассмотрения базового фреймворка класса, кото- рый составляет основу для большинства классов. Затем мы рассмо- трим пути расширения базового фреймворка.
Базовый фреймворк класса
Лучше всего изучать базовый фреймворк класса с помощью класса- примера. В нашем случае мы начнем со структуры студентов из гла- вы 3 и разовьем ее в полноценный класс. Для удобства повторим ис- ходную структуру:
struct student {
int grade;
int studentID;
string name;
};
Даже в такой простой структуре у нас есть инкапсуляция. Вспом- ним, что в главе 3 мы создавали массив данных о студентах в виде структуры struct и без использования struct нам пришлось создать три параллельных массива, один для оценок, второй для ID и третий для имен — ужас! Однако с использованием struct мы не достигаем со- крытия. Базовый фреймворк класса дает нам сокрытие, объявляя все данные приватными и затем добавляя публичные методы, с помощью которых клиентский код может опосредованно получать или менять эти данные.
class studentRecord {
X public:
Y studentRecord();
studentRecord(int newGrade, int newID, string newName);
Z int grade();
[ void setGrade(int newGrade);
int studentID();
void setStudentID(int newID);
string name();
void setName(string newName);
\private:
]int _grade;
int _studentID;
string _name;
};
Как договаривались, это объявление класса разделено на публич- ную секцию с функциями класса
X
и приватную секцию
\
, которая содержит те же данные, что и исходная struct
]
. В классе восемь функций: два конструктора
Y
и пара функций класса для каждого поля класса. Например, у поля _grade есть две связанных функции
148 Глава 5
класса, grade
Z
и setGrade
[
. Первый из этих методов будет исполь- зоваться клиентским кодом для получения оценки для конкретной studentRecord
, а второй — для установки новой оценки для конкрет- ной studentRecord.
Методы получения и установки, связанные с полем класса, на- столько распространены, что их обычно называют краткими терми- нами get и set (геттер и сеттер) . Как вы видите, я включил слово set в названия методов, которые устанавливают новое значение для поля. Многие программисты также включили бы слово get в другие названия, например, getGrade вместо grade. Почему я так не сделал?
Потому что в этом случае я бы использовал бы глагол для функции, использующейся как существительное. Однако некоторые могли бы возразить, что термин get настолько общепринят и, следовательно, его значение настолько очевидно, что его использование перевеши- вает прочие доводы. В конечном счете, это дело личного стиля.
Хотя я и указывал в этой книге на преимущества C++ по сравне- нию с другими языками, я должен признать, что более современные языки, такие как C#, превосходят C++ в том, что касается методов get и set. В языке C# есть встроенный механизм, называемый свой- ством , который действует и как метод get, и как set. После опреде- ления клиентский код получает доступ к свойствам, как если бы они были полями класса, а не через вызов функции. Это большой шаг в увеличении читабельности и выразительности . Поскольку в языке
C++ такой механизм отсутствует, важно определить некоторую кон- венцию наименований и постоянно ее придерживаться.
Обратите внимание, что моя конвенция наименований распро- страняется и на поля класса, которые в отличие от исходной struct все начинаются с подчеркивания. Это позволяет мне называть функ- ции get (почти) также как и поля класса, значения которых они по- лучают. Кроме того, это позволяет легко распознавать обращения к полям класса в коде, повышая тем самым его читабельность. Некото- рые программисты используют ключевое слово this для обращения ко всем полям класса вместо подчеркивания. Так вместо инструкции:
return _grade;
у них будет:
return this.grade;
Возможно, вы не встречались с ключевым словом this ранее. Это ссылка на объект, в котором оно появляется. То есть если вышепри- веденное строка присутствует в методе класса, а в этом методе объ- явлена локальная переменная с именем grade, выражение this.grade будет относиться к полю класса grade, а не к локальной переменной с тем же именем. Использование этого ключевого слова таким образом дает особое преимущество в средах программирования с автомати- ческим дополнение синтаксиса: программист может набрать только
Решение задач с классами 149
this
, нажать точку, и выбрать поле класса из списка, избежав допол- нительного набора и возможных опечаток. Однако в обоих вариантах выделяются поля класса, это то, что действительно важно.
Теперь, когда мы рассмотрели объявление класса, давайте по- смотрим реализацию методов. Начнем с первой пары get/set.
int studentRecord::grade() {
X return _grade;
}
void studentRecord::setGrade(int newGrade) {
Y _grade = newGrade;
}
Это самая простая форма пары get/set. Первый метод, grade, воз- вращает текущее значение соответствующего поля класса, _grade
X
Второй метод, setGrade, присваивает полю класса _grade значение параметра newGrade
Y
. Однако, если бы это было все, что делает наш класс, у нас бы ничего путного не вышло. Хотя этот код предусма- тривает сокрытие, поскольку передает данные в обоих направлени- ях без рассмотрения и модификации, он лучше, чем объявление пе- ременной _grade публичной, поскольку оставляет нам возможность изменить имя или тип полю класса. Метод setGrade должен как ми- нимум выполнить несколько примитивных проверок; необходимо удостовериться, что значение, получаемое полем класса _grade из переменной newGrade, имеет смысл как оценка. Однако надо быть осторожными, решая задачи спецификации, и не делать предполо- жений о данных, базируясь только на своем опыте, без обсуждения с пользователем. Возможно, оценка варьируется от 0 до 100, а может и нет, если, например, в школе предусмотрены дополнительные бал- лы или используется оценка –1 для отчисленных. Поскольку в дан- ном случае у нас есть описание ситуации, мы можем применить эти знания для проверки.
void studentRecord::setGrade(int newGrade) {
if ((newGrade >= 0) &&
(newGrade <= 100))
_grade = newGrade;
}
В данном случае проверка является только привратником. Одна- ко, в зависимости от описания задачи, возможно, следует добавить в метод вывод сообщения об ошибке, запись в файл сообщения об ошибке или еще какой-то механизм управления ошибками.
Остальные пары get/set работают точно так же. Несомненно, существуют правила формирования номеров ID студентов в конкрет- ной школе. Эти правила надо использовать при проверке. Однако при вводе имени студента лучше всего отклонять строки со странны- ми символами, как % или @, но сейчас это вряд ли возможно.
Заключительный этап создания нашего класса — это написание конструкторов. В базовый фреймворк мы включаем два конструкто- ра: конструктор по умолчанию, без параметров, который назначает
150 Глава 5
полям класса разумные значения по умолчанию, и конструктор с па- раметрами для каждого поля класса. Второй конструктор важен для нашей цели выразительности , поскольку позволяет одномоментно создать объект нашего класса и инициализировать его. После того как вы написали код для остальных методов, этот второй конструк- тор почти написался сам по себе. studentRecord::studentRecord(int newGrade, int newID, string newName) {
setGrade(newGrade);
setStudentID(newID);
setName(newName);
}
Как видите, конструктор просто вызывает соответствующие ме- тоды set для каждого параметра. В большинстве случаев это пра- вильный подход, поскольку позволяет избежать дублирования кода и гарантирует, что конструктор использует преимущества проверок, сделанных в методах set.
Иногда конструктор по умолчанию бывает немного сложнее, но не из-за сложности кода, а потому что не всегда очевидно, какие именно должны быть значения по умолчанию. Выбирая значения по умолчанию для полей класса, помните о ситуациях, при которых бу- дет использоваться конструктор по умолчанию и, особенно, будет ли объект по умолчанию легитимным для данного класса. Это под- скажет вам, заполнять ли поля класса полезными значениями по умолчанию или значениями, сигнализирующими о том, что объект неверно инициирован. Для примера рассмотрим класс, представ- ляющий коллекцию значений и инкапсулирующий связный список.
В этом случае существует связный список по умолчанию, а именно пу- стой связный список. В этом случае мы устанавливаем в полях класса легитимный, но пустой, связный список. Однако в нашем примере с базовым классом, не существует правильного определения сту- дента по умолчанию; мы не хотим давать верный номер ID объекту studentRecord по умолчанию, так его теоретически можно спутать с легитимным объектом studentRecord. Следовательно, мы должны выбрать очевидно нелегитимные значения по умолчанию для поля
_studentID
, например –1:
studentRecord::studentRecord() {
setGrade(0);
setStudentID(-1);
setName("");
}
Мы задаем оценку с помощью метода setGrade, который уста- навливает свой параметр. Это значит, что мы должны передать корректную оценку, в данном случае 0. Поскольку в поле номера ID установлено неверное значение, вся запись может быть легко иден- тифицирована как нелегитимная. Следовательно, корректное значе- ние оценки не имеет значения. Если бы нам было бы важно, мы мог- ли бы установить некорректное значение прямо в поле _grade.
Решение задач с классами
1 ... 14 15 16 17 18 19 20 21 ... 34
151
Тем самым мы завершаем базовый фреймворк класса. Мы созда- ли группу приватных полей класса, соответствующих разным атри- бутам одного и того же логического объекта, в данном случае записи класса студентов. У нас есть функции класса для получения или из- менения с соответствующими проверками данных объекта. И у нас есть набор полезных конструкторов. Мы создали хорошую основу класса. Однако возникает вопрос: нужно ли делать что-то еще?
Служебные методы
Служебный метод – это метод класса, который не получает и не устанав- ливает данные. Некоторые программисты называют их вспомогатель- ными методами, методами-помощниками или как-то еще. Их можно на- звать как угодно, однако именно они делают класс чем-то большим, чем базовый фреймворк класса. Часто именно хорошо спроектированный набор служебных методов делает класс поистине полезным.
Для определения возможных служебных методов, подумайте, как будет использоваться класс. Можем ли мы ожидать от клиентско- го кода совершения каких-то обычных действий с полями нашего класса? В нашем примере программа, для которой мы проектируем класс, выдает оценки студентов не только в цифровом, но и в бук- венном формате. Тогда давайте создадим служебный метод, который возвращает оценку студента в виде буквы. Сначала добавим объявле- ние метода в публичную секцию объявления класса. string letterGrade();
Теперь нам надо реализовать метод. Функция будет преобразовы- вать цифровые значения, хранящиеся в поле _grade, в соответствующую переменную формата string, основываясь на таблице оценок, представ- ленной в задаче. Мы могли бы справиться с этим серией условных ин- струкций if, но нет ли более чистого, более элегантного способа? Если вы сейчас подумали: «Эй, это ж похоже на то, как мы конвертировали доход в категории бизнес-лицензии в главе 3», то поздравляю — вы об- наружили подходящую программистскую аналогию. Мы можем адапти- ровать тот код с параллельными константными (const) массивами для хранения буквенных оценок и нижней границы цифровой оценки соот- ветствующей этим буквам для конвертации цифровых оценок в цикле.
string studentRecord::letterGrade() {
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];