Tree
, реализованный с использованием TreeNode
, агрегата, используемого в Tree
, который хранит указатели на предыдущий, следующий и родительский узлы и сам объект T
. Все члены TreeNode
могут быть открытыми, поскольку их не надо скрывать от класса Tree
, который непосредственно манипулирует ими. Однако класс Tree
должен полностью скрывать класс TreeNode
(например, как вложенный закрытый класс или как определенный только в файле реализации класса Tree
), поскольку это — детали внутренне реализации класса Tree
, от которых не должен зависеть и с которыми не должен иметь дела вызывающий код. И наконец, Tree
не скрывает содержащиеся в контейнере объекты T
, поскольку за них отвечает вызывающий код; контейнеры используют абстракцию итераторов для предоставления доступа к содержащимся объектам, в то время как внутренняя структура контейнера остается скрытой.
color
) могут, как минимум, быть сделаны закрытыми и скрыты за функциями получения и установки значений (например, GetColor
, SetColor
). Тем самым обеспечивается минимальный уровень абстракции и устойчивость к изменениям.
Использование функций повышает уровень общения по поводу "цвета" от конкретного состояния до абстрактного, которое мы можем реализовать тем способом, который сочтем наиболее приемлемым. Мы можем изменить внутреннее представление цвета, добавить код для обновления дисплея при изменении цвета, добавить какие-то инструментальные средства или внести еще какие-то изменения — и все это без каких-либо изменений в вызывающем коде. В худшем случае вызывающий код потребуется перекомпилировать (т.е. мы сохраняем совместимость на уровне исходных текстов); в лучшем — не потребуется ни перекомпиляция, ни даже перекомпоновка (если изменения сохраняют бинарную совместимость). Ни совместимость на уровне исходных текстов, ни бинарная совместимость при внесении таких изменений невозможны, если исходный дизайн содержит открытый член color
, с которым тесно связан вызывающий код.
Функции получения и установки значений полезны, но дизайн класса, состоящего практически из одних таких функций, оставляет желать лучшего. Подумайте над тем, требуется ли в таком случае обеспечение абстракции или достаточно ограничиться простой структурой.
Агрегаты значений (известные как структуры в стиле С) просто хранят вместе набор различных данных, но при этом не обеспечивают ни их поведение, ни делают попыток моделирования абстракций или поддержания инвариантов. Такие агрегаты не предназначены для того, чтобы быть абстракциями. Все их данные-члены должны быть открытыми, поскольку эти данные-члены и представляют собой интерфейс. Например, шаблон класса std::pair
используется стандартными контейнерами для объединения двух несвязанных элементов типов T
и U
, и при этом pair
сам по себе не привносит ни поведения, ни каких-либо инвариантов.
42. Не допускайте вмешательства во внутренние дела
Избегайте возврата дескрипторов внутренних данных, управляемых вашим классом, чтобы клиенты не могли неконтролируемо изменять состояние вашего объекта, как своего собственного.
Рассмотрим следующий код:
class Socket {
public:
// ... Конструктор, который открывает handle_,
// деструктор, который закрывает handle_, и т.д. ...
int GetHandle() const { return handle_; } // Плохо!
private:
int handle_; // дескриптор операционной системы
};
Сокрытие данных — мощный инструмент абстракции и модульности (см. рекомендации 11 и 41). Однако сокрытие данных при одновременном обеспечении доступа к их дескрипторам обречено на провал, потому что это то же, что и закрыть свою квартиру на замок и положить ключ под коврик у входа или просто оставить его в замке. Вот почему это так.