Пример 3. Функции получения и установки значений. Если не имеется лучшей предметной абстракции, открытые и защищенные данные-члены (например,
color
) могут, как минимум, быть сделаны закрытыми и скрыты за функциями получения и установки значений (например,
GetColor
,
SetColor
). Тем самым обеспечивается минимальный уровень абстракции и устойчивость к изменениям.
Использование функций повышает уровень общения по поводу "цвета" от конкретного состояния до абстрактного, которое мы можем реализовать тем способом, который сочтем наиболее приемлемым. Мы можем изменить внутреннее представление цвета, добавить код для обновления дисплея при изменении цвета, добавить какие-то инструментальные
средства или внести еще какие-то изменения — и все это без каких-либо изменений в вызывающем коде. В худшем случае вызывающий код потребуется перекомпилировать (т.е. мы сохраняем совместимость на уровне исходных текстов); в лучшем — не потребуется ни перекомпиляция, ни даже перекомпоновка (если изменения сохраняют бинарную совместимость). Ни совместимость на уровне исходных текстов, ни бинарная совместимость при внесении таких изменений невозможны, если исходный дизайн содержит открытый член
color
, с которым тесно связан вызывающий код.
Исключения
Функции получения и установки значений полезны, но дизайн класса, состоящего практически из одних таких функций, оставляет желать лучшего. Подумайте над тем, требуется ли в таком случае обеспечение абстракции или достаточно ограничиться простой структурой.
Агрегаты значений (известные как структуры в стиле С) просто хранят вместе набор различных данных, но при этом не обеспечивают ни их поведение, ни делают попыток моделирования абстракций или поддержания инвариантов. Такие агрегаты не предназначены для того, чтобы быть абстракциями. Все их данные-члены должны быть открытыми, поскольку эти данные-члены и представляют собой интерфейс. Например, шаблон класса
std::pair<T, U>
используется стандартными контейнерами для объединения двух несвязанных элементов типов
T
и
U
, и при этом
pair
сам по себе не привносит ни поведения, ни каких-либо инвариантов.
42. Не допускайте вмешательства во внутренние дела
Резюме
Избегайте возврата дескрипторов внутренних данных, управляемых вашим классом, чтобы клиенты не могли неконтролируемо изменять состояние вашего объекта, как своего собственного.
Обсуждение
Рассмотрим следующий код:
class Socket {
public:
// ... Конструктор, который открывает handle_,
// деструктор, который закрывает handle_, и т.д. ...
int GetHandle const { return handle_; } // Плохо!
private:
int handle_; // дескриптор операционной системы
};
Сокрытие данных — мощный инструмент абстракции и модульности (см. рекомендации 11 и 41). Однако сокрытие данных при одновременном обеспечении доступа к их дескрипторам обречено на провал, потому что это то же, что и закрыть свою квартиру на замок и положить ключ под коврик у входа или просто оставить его в замке. Вот почему это так.
• В этом случае клиент имеет две возможности реализации функциональности. Он может воспользоваться абстракцией вашего класса (
Socket
) либо непосредственно работать с реализацией, на которой основан ваш класс (дескриптор сокета в стиле С). В последнем случае объект оказывается не осведомлен об изменениях, происходящих с ресурсом, которым он, как ему кажется, владеет. Теперь класс не в состоянии надежно обогатить или усовершенствовать функциональность (например, обеспечить прокси, журнализацию, сбор статистики и т.п.), поскольку клиенты могут просто обойти эти возможности реализации, как и любые
другие инварианты, которые вы, как вы полагаете, добавили в ваш класс. Это делает невозможной, в частности, корректную обработку возникающих ошибок (см. рекомендацию 70).
• Класс не может изменять внутреннюю реализацию своей абстракции, поскольку от нее зависят клиенты. Если в будущем класс
Socket
будет обновлен для поддержки другого протокола с использованием других низкоуровневых примитивов, вызывающий код, который будет по-прежнему получать доступ к дескриптору
handle_
и работать с ним, окажется некорректным.
• Класс не в состоянии обеспечить выполнение его инвариантов, поскольку вызывающий код может изменить состояние без ведома класса. Например, кто-то может закрыть дескриптор, используемый объектом
Socket
, минуя вызов функции-члена Socket, а это приведет к тому, что объект станет недействительным.
• Код клиента может хранить дескрипторы, возвращаемые вашим классом, и пытаться использовать их после того, как код вашего класса сделает их недействительными.
Распространенная ошибка заключается в том, что действие const на самом деле неглубокое и не распространяется посредством указателей (см. рекомендацию 15). Например,
Socket::GetHandle
— константный член; пока мы рассматриваем ситуацию с точки зрения компилятора, возврат
handlе_
сохраняет константность объекта. Однако непосредственный вызов функций операционной системы с использованием значения
handlе_
вполне может изменять данные, к которым косвенно обращается
handlе_
.
Приведенный далее пример очень прост, хотя в данном случае ситуация несколько лучше — мы можем снизить вероятность случайного неверного употребления возвращаемого значения, описав его тип как
const
:
class String {
char* buffer_;
public:
char* GetBuffer const { return buffer_; }
// Плохо: следует возвращать const char*
// ...
};
Хотя функция
GetBuffer
константная, технически этот код вполне корректен. Понятно, что клиент может использовать эту функцию
GetBuffer
для того, чтобы изменить объект
String
множеством разных способов, не прибегая к явному преобразованию типов. Например,
strcpy(s.GetBuffer, "Very Long String...")
— вполне законный код; любой компилятор пропустит его без каких бы то ни было замечаний. Если бы мы объявили возвращаемый тип как
const char*
, то представленный код вызвал бы, по крайней мере, ошибку времени компиляции, так что случайно поступить столь опасно было бы просто невозможно — вызывающий код должен был бы использовать явное преобразование типов (см. рекомендации 92 и 95).
Но даже возврат указателей на
const
не устраняет возможности случайного некорректного использования, поскольку имеется еще одна проблема, связанная с корректностью внутренних данных класса. В приведенном выше примере с классом
String
, вызывающий код может сохранить значение, возвращаемое функцией
GetBuffer
, а затем выполнить операции, которые приведут к росту (и перемещению) буфера
String
, что в результате может привести к использованию сохраненного, но более недействительного указателя на несуществующий в данный момент буфер. Таким образом, если вы считаете, что у вас есть причины для обеспечения такого доступа ко внутреннему состоянию, вы должны детально документировать, как долго возвращаемое значение остается корректным и какие операции делают его недействительным (сравните с гарантиями корректности явных итераторов стандартной библиотеки; см. [C++03]).