41. Делайте данные-члены закрытыми (кроме случая агрегатов в стиле структур С)

41. Делайте данные-члены закрытыми (кроме случая агрегатов в стиле структур С)

Резюме

Данные-члены должны быть закрыты. Только в случае простейших типов в стиле структур языка С, объединяющих в единое целое набор значений, не претендующих на инкапсуляцию и не обеспечивающих поведение, делайте все данные-члены открытыми. Избегайте смешивания открытых и закрытых данных, что практически всегда говорит о бестолковом дизайне.

Обсуждение

Сокрытие информации является ключом к качественной разработке программного обеспечения (см. рекомендацию 11). Желательно делать все данные-члены закрытыми; закрытые данные — лучшее средство для сохранения инварианта класса, в том числе при возможных вносимых изменениях.

Открытые данные — плохая идея, если класс моделирует некоторую абстракцию и, следовательно, должен поддерживать инварианты. Наличие открытых данных означает, что часть состояния вашего класса может изменяться неконтролируемо, непредсказуемо и асинхронно с остальной частью состояния. Это означает, что абстракция разделяет ответственность за поддержание одного или нескольких инвариантов с неограниченным множеством кода, который использует эту абстракцию, и совершенно очевидно, что такое положение дел просто недопустимо с точки зрения корректного проектирования.

Защищенные данные обладают всеми недостатками открытых данных, поскольку наличие защищенных данных означает, что абстракция разделяет ответственность за поддержание одного или нескольких инвариантов с неограниченным множеством кода — теперь это код существующих и будущих производных классов. Более того, любой код может читать и модифицировать защищенные данные так же легко, как и открытые — просто создав производный класс и используя его для доступа к данным.

Смешивание открытых и закрытых данных-членов в одном и том же классе является непоследовательным и попросту запутывает пользователей. Закрытые данные демонстрируют, что у вас есть некоторые инварианты и нечто, предназначенное для их поддержания. Смешивание их с открытыми данными-членами означает, что при проектировании так окончательно и не решено, должен ли класс представлять некоторую абстракцию или нет.

Не закрытые данные-члены почти всегда хуже даже простейших функций для получения и установки значений, поскольку последние обеспечивают устойчивость кода к возможным внесениям изменений.

Подумайте о сокрытии закрытых членов класса с использованием идиомы Pimpl (см. рекомендацию 43).

Примеры

Пример 1. Корректная инкапсуляция. Большинство классов (например, Matrix, File, Date, BankAccount, Security) должны закрывать все данные-члены и открывать соответствующие интерфейсы. Позволение вызывающему коду непосредственно работать с внутренними данными класса работает против представленной им абстракции и поддерживаемых им инвариантов.

Агрегат Node, широко используемый в реализации класса List, обычно содержит некоторые данные и два указателя на Node: next_ и prev_. Данные-члены Node не должны быть скрыты от List. Однако не забудьте рассмотреть еще пример 3.

Пример 2. TreeNode. Рассмотрим контейнер Tree<T>, реализованный с использованием TreeNode<T>, агрегата, используемого в Tree, который хранит указатели на предыдущий, следующий и родительский узлы и сам объект T. Все члены TreeNode могут быть открытыми, поскольку их не надо скрывать от класса Tree, который непосредственно манипулирует ими. Однако класс Tree должен полностью скрывать класс TreeNode (например, как вложенный закрытый класс или как определенный только в файле реализации класса Tree), поскольку это — детали внутренне реализации класса Tree, от которых не должен зависеть и с которыми не должен иметь дела вызывающий код. И наконец, Tree не скрывает содержащиеся в контейнере объекты T, поскольку за них отвечает вызывающий код; контейнеры используют абстракцию итераторов для предоставления доступа к содержащимся объектам, в то время как внутренняя структура контейнера остается скрытой.

Пример 3. Функции получения и установки значений. Если не имеется лучшей предметной абстракции, открытые и защищенные данные-члены (например, color) могут, как минимум, быть сделаны закрытыми и скрыты за функциями получения и установки значений (например, GetColor, SetColor). Тем самым обеспечивается минимальный уровень абстракции и устойчивость к изменениям.

Использование функций повышает уровень общения по поводу "цвета" от конкретного состояния до абстрактного, которое мы можем реализовать тем способом, который сочтем наиболее приемлемым. Мы можем изменить внутреннее представление цвета, добавить код для обновления дисплея при изменении цвета, добавить какие-то инструментальные средства или внести еще какие-то изменения — и все это без каких-либо изменений в вызывающем коде. В худшем случае вызывающий код потребуется перекомпилировать (т.е. мы сохраняем совместимость на уровне исходных текстов); в лучшем — не потребуется ни перекомпиляция, ни даже перекомпоновка (если изменения сохраняют бинарную совместимость). Ни совместимость на уровне исходных текстов, ни бинарная совместимость при внесении таких изменений невозможны, если исходный дизайн содержит открытый член color, с которым тесно связан вызывающий код.

Исключения

Функции получения и установки значений полезны, но дизайн класса, состоящего практически из одних таких функций, оставляет желать лучшего. Подумайте над тем, требуется ли в таком случае обеспечение абстракции или достаточно ограничиться простой структурой.

Агрегаты значений (известные как структуры в стиле С) просто хранят вместе набор различных данных, но при этом не обеспечивают ни их поведение, ни делают попыток моделирования абстракций или поддержания инвариантов. Такие агрегаты не предназначены для того, чтобы быть абстракциями. Все их данные-члены должны быть открытыми, поскольку эти данные-члены и представляют собой интерфейс. Например, шаблон класса std::pair<T, U> используется стандартными контейнерами для объединения двух несвязанных элементов типов T и U, и при этом pair сам по себе не привносит ни поведения, ни каких-либо инвариантов.

Ссылки

[Dewhurst03] §80 • [Henricson97] pg. 105 • [Koenig97] §4 • [Lakos96] §2.2 • [Meyers97] §20 • [Murray93] §2.3 • [Stroustrup00] §10.2.8, §15.3.1.1, §24.4.2-3 • [SuttHysl04a]