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
, поскольку за них отвечает вызывающий код; контейнеры используют абстракцию итераторов для предоставления доступа к содержащимся объектам, в то время как внутренняя структура контейнера остается скрытой.