|
Программирование >> Обобщенные обратные вызовы
непосредственно (например, хирург), но даже в этом случае а) это бывает редко, б) я сам решаю, нужна ли мне помощь хирурга и в) я сам решаю, какому хирургу я могу доверить такие драгоценные для меня внутренности. Аналогично, в большинстве случаев вызываюший код не должен работать со внутренней организацией класса непосредственно (например, просматривая или изменяя члены-данные), поскольку при этом очень легко непреднамеренно совершить неверные действия; в лучшем случае внешний код должен работать с внутренними данными косвенно, при помощи открытого интерфейса класса, когда класс может сам решить, что следует делать с переданными ему параметрами (рассмотренный пример БутылкаСвыпей меня )) на основании знаний и суждений автора данного класса. Конечно, определенный код может быть специально предназначен для непосредственной работы с внутренней организацией класса (обычно такой код должен быть функцией-членом класса, но, например, operator делать членом не рекомендуется), но даже в таком случае: а) это бывает редко, б) класс сам решает, объявлять ли кого-либо другом класса или нет, и в) класс сам решает, какой именно код заслуживает достаточного доверия, чтобы его можно было объявить другом и дать ему доступ к внутренней организации класса. Словом, открытые данные есть зло (кроме данных в структурах в стиле С). Аналогично, защищенные данные - такое же зло, но на этот раз без всяких исключений. Подождите минутку, - может возразить кто-то из читателей, - я согласен с вами, когда вы говорите об открытых данных, но почему вы считаете таким же злом защищенные данные? Погому что тс же аргументы, которые были перечислены для открытых данных, примени.мы и к защищенным данным, которые тоже являются частью интерфейса: защищенный интерфейс также является интерфейсом ко внешнему коду, только к меньшему его подмножеству - а именно коду производных классов. Почему в этом случае нет исключений? Потому что защищенные данные не могут быть просто сгруппированными данными; если бы это было так, то ими могли бы воспользоваться только производные классы, а какой в этом смысл? Об истории этого вопроса и о том, почему человек, ратовавший за наличие защищенных данных в языке, теперь считает это плохой идеей, можно прочесть в книге [Stroustrup94]. > Рекомендация Всегда делайте все члены-данные закрытыми. Единственное исключение - структура в стиле С, которая не предназначена для инкапсуляции чего бы то ни было, и все члены которой открыты. Преобразование в общем случае Давайте теперь докажем, правило все члены-данные всегда должны быть закрытыми от противного - предположим, что справедливо обратное утверждение (что имеются ситуации, когда public/protected члены-данные могут быть подходящим решением) и покажем, что в каждом таком случае данные не должны быть ни открытыми, ни защищенными. пример 17-2(а): не закрытые данные (плохо) class X { ... public: Tl tl ; protected: t2 t2 ; Для начала заметим, что этот код всегда можно преобразовать без потери общности и эффективности в следующий. Пример 17-2(6): инкапсулированные данные (хорошо) class X { ... public: Tl& useTlO { return tl ; } protected: T2& useT2() { return t2 ; } private: Tl tl ; T2 t2 ; Таким образом, даже если есть причины для непосредственного доступа к tl или t2 , возможно простое преобразование, в результате которого он предоставляется посредством (встраиваемых) функций. Примеры 17-2(а) и 17-2(6) эквивалентны. Однако нет ли каких-то особых преимуществ для использования класса в том виде, как он приведен в примере 17-2(а)? Для того чтобы обосновать, что метод из примера 17-2(а) никогда не должен использоваться, мы должны показать, что: 1. пример 17-2(а) не имеет никаких преимуществ, которых нет в примере 17-2(6); 2. пример 17-2(6) обладает конкретными преимуществами; и 3. пример 17-2(6) не приводит к дополнительным затратам. Рассмотрим эти пункты в обратном порядке. Выполнение пункта 3 показывается тривиально: встраиваемая функция, возвращающая ссылку и, следовательно, не выполняющая копирование, должна оптимизироваться компилятором вплоть до полного устранения ее кода. Пункт 2 также прост: давайте проанализируем зависимость исходного текста. В примере 17-2(а) весь вызывающий код, который использует tl и/или t2 , должен обращаться к ним с явны.м указанием имени; в примере 17-2(6) вызывающий код использует имена useTl и useT2. Пример 17-2(а) недостаточно гибкий, поскольку любые изменения tl или t2 (например, удаление их и замена чем-то другим) требует изменения всего использующего класс вызывающего кода, В примере 17-2(6) можно, например, полностью удалить tl и/или t2 , никак не изменяя вызывающий код, поскольку функции-члены, образующие интерфейс класса, скрывают его внутреннюю организацию. И наконец, в пункте 1 заметим, что все, что пользователь мог сделать с tl или t2 непосредственно в примере 17-2(а), он может точно так же сделать и при наличии функции для доступа к данным в примере 17-2(6). В вызывающем коде придется дописать пару скобок, и это все отличия в использовании двух примеров. Давайте рассмотрим конкретный пример. Пусть, например, мы хотим добавить определенную функциональность, скажем, простой счетчик количества обращений к tl или t2 . Если это члены данных, как в примере 17-2(а), то вот что мы должны для этого сделать. 1. Следует создать функцию для доступа к тому члену, который нас интересует, и сделать данные закрытыми (другими словами, выполнить преобразование к коду примера 17-2(6)). 2. Все пользователи вашего класса испытывают сомнительное удовольствие от поиска и замены всех обращений к tl и t2 в своем коде на их функциональные эквиваленты. Особенно большую радость это вызывает у тех, кто уже давно занят со- всем другой работой... Если после этого вам придут подарки от ваших пользователей -- прислушайтесь, не тикает ли что-то в полученном вами пакете... 3. Весь код ваших пользователей перекомпилируется. 4. Если что-то оказалось пропущено, код не будет корректно скомпилирован, и надо будет повторить шаги 2 и 3 до тех пор, пока не будут устранены все обращения к членам tl и t2 . Если же у нас уже имеются функции доступа, как в примере 17-2(6), то вот что нам остается сделать. 1. Вносим изменения в существующие функции доступа. 2. Все ваши пользователи перекомпонуют (если функции находятся в отдельном .срр-файле и не являются встраиваемыми) свои программы или, в худшем случае (если функции определены в заголовочных файлах), перекомпилируют их. Самое неприятное в реальной ситуации то, что если вы начали с примера 17-2(а), то можете уже никогда не перейти к примеру 17-2(6). Чем больше пользователей зависят от вашего интерфейса, тем труднее оказывается его изменить. Все это приводит к следующему правилу, которое можно назвать Законом Второго Шанса. > Рекомендация Самая важная задача - разработка правильного интерфейса. Все остальное можно исправить позже. У вас может не оказаться возможности исправить неверный интерфейс. Как только интерфейс начинает широко использоваться, от него может зависеть такое большое количество людей, что изменить его будет просто нереально. Да, интерфейс может быть расширен (путем добавления новых функций вместо изменения старых) без неприятных последствий для пользователей, но это не поможет исправить существующие части класса, если позже выяснится, что они были спроектированы не верно. В лучшем случае добавление функций дает возможность выполнить задачу другим способом, что обычно только запутывает ваших пользователей, которые вполне обоснованно начинают выяснять, почему имеется два (три, N) способа достижения некоторой цели, и какой именно из них следует использовать. Коротко говоря, плохой интерфейс может быть очень трудно, а то и просто невозможно исправить впоследствии. Старайтесь разработать правильный интерфейс с первого раза и спрячьте за ним все детали внутренней организации класса. Актуальный момент 3. Шаблон класса std:: pai г использует открытые члены-данные, поскольку он не инкапсулирует никаких данных, а просто позволяет их группировать. Заметим, что здесь мы имеем дело с рассматривавшимся исключением, когда допустимо использование открытых данных. Но даже в этом случае использование функций доступа ни в чем не хуже применения открытых членов-данных. Представим шаблон класса наподобие std::pai г, но с тем отличием, что у него дополнительно имеется флаг deleted, который может бьпъ установлен и опрошен (но не сброшен). Ясно, что такой флаг должен быть закрытым для защиты от непосредственного изменения пользователем. Если остальные члены-данные будут открытыми, как в std: :pai г, в конечном итоге мы получим примерно следуюншй код. Пример 17-3(а): одновременное использование открытых и закрытых данных?
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |