Но и при этом возникают определенные неприятности, связанные с перегрузкой функций.
void Display(int);
void Display(std::string);
Display(NULL); // вызов Display(int)
Этот результат для некоторых может оказаться сюрпризом. (Кстати, если бы выполнялся вызов Display(std::string)
, код бы обладал неопределенным поведением, поскольку создание std::string
из нулевого указателя некорректно, но конструктор этого класса не обязан проверять передаваемое ему значение на равенство нулю.)
Ссылки
[Dewhurst03] §36-37 • [Lakos96] §9.3.1 • [Meyers96] §5 • [Murray93] §2.4 • [Sutter00] §6, §20, §39
41. Делайте данные-члены закрытыми (кроме случая агрегатов в стиле структур С)
Резюме
Данные-члены должны быть закрыты. Только в случае простейших типов в стиле структур языка С, объединяющих в единое целое набор значений, не претендующих на инкапсуляцию и не обеспечивающих поведение, делайте все данные-члены открытыми. Избегайте смешивания открытых и закрытых данных, что практически всегда говорит о бестолковом дизайне.
Обсуждение
Сокрытие информации является ключом к качественной разработке программного обеспечения (см. рекомендацию 11). Желательно делать все данные-члены закрытыми; закрытые данные — лучшее средство для сохранения инварианта класса, в том числе при возможных вносимых изменениях.
Открытые данные — плохая идея, если класс моделирует некоторую абстракцию и, следовательно, должен поддерживать инварианты. Наличие открытых данных означает, что часть состояния вашего класса может изменяться неконтролируемо, непредсказуемо и асинхронно с остальной частью состояния. Это означает, что абстракция разделяет ответственность за поддержание одного или нескольких инвариантов с неограниченным множеством кода, который использует эту абстракцию, и совершенно очевидно, что такое положение дел просто недопустимо с точки зрения корректного проектирования.
Защищенные данные обладают всеми недостатками открытых данных, поскольку наличие защищенных данных означает, что абстракция разделяет ответственность за поддержание одного или нескольких инвариантов с неограниченным множеством кода — теперь это код существующих и будущих производных классов. Более того, любой код может читать и модифицировать защищенные данные так же легко, как и открытые — просто создав производный класс и используя его для доступа к данным.
Смешивание открытых и закрытых данных-членов в одном и том же классе является непоследовательным и попросту запутывает пользователей. Закрытые данные демонстрируют, что у вас есть некоторые инварианты и нечто, предназначенное для их поддержания. Смешивание их с открытыми данными-членами означает, что при проектировании так окончательно и не решено, должен ли класс представлять некоторую абстракцию или нет.
Не закрытые данные-члены почти всегда хуже даже простейших функций для получения и установки значений, поскольку последние обеспечивают устойчивость кода к возможным внесениям изменений.
Подумайте о сокрытии закрытых членов класса с использованием идиомы Pimpl (см. рекомендацию 43).
Примеры
Пример 1. Корректная инкапсуляция. Большинство классов (например, Matrix
, File
, Date
, BankAccount
, Security
) должны закрывать все данные-члены и открывать соответствующие интерфейсы. Позволение вызывающему коду непосредственно работать с внутренними данными класса работает против представленной им абстракции и поддерживаемых им инвариантов.
Агрегат Node
, широко используемый в реализации класса List
, обычно содержит некоторые данные и два указателя на Node
: next_
и prev_
. Данные-члены Node
не должны быть скрыты от List
. Однако не забудьте рассмотреть еще пример 3.
Пример 2. TreeNode. Рассмотрим контейнер Tree
, реализованный с использованием TreeNode
, агрегата, используемого в Tree
, который хранит указатели на предыдущий, следующий и родительский узлы и сам объект T
. Все члены TreeNode
могут быть открытыми, поскольку их не надо скрывать от класса Tree
, который непосредственно манипулирует ими. Однако класс Tree
должен полностью скрывать класс TreeNode
(например, как вложенный закрытый класс или как определенный только в файле реализации класса Tree
), поскольку это — детали внутренне реализации класса Tree
, от которых не должен зависеть и с которыми не должен иметь дела вызывающий код. И наконец, Tree
не скрывает содержащиеся в контейнере объекты T
, поскольку за них отвечает вызывающий код; контейнеры используют абстракцию итераторов для предоставления доступа к содержащимся объектам, в то время как внутренняя структура контейнера остается скрытой.
Читать дальше