![]() |
![]() |
![]() |
Анимация
JavaScript
|
Главная Библионтека Для начала заметим, что этот код всегда можно преобразовать без потери общности и эффективности в следующий. Пример 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(а): одновременное использование открытых и закрытых данных? temp!ate<с1ass T, class U> class Couple { public: Основные члены-данные открыты... т first; и second; ...но есть еще нечто, делающее его более "классоподобным", а также закрытая реализация couple О : deleted (false ) { void MarkDeleted() { deleted = true; ] bool isDeleted () { return deleted ; } private: bool deleted.; Должны ли в этом случае остальные члены-данные быть, как показано в коде, открытыми? Почему? Если да, то является ли этот код хорошим примером того, почему иногда одновременное наличие открытых и закрытых данных в одном классе может быть удачным решением? Рассматриваемый класс Couple предложен в качестве контрпримера к обычной рекомендации по кодированию. Здесь приведен класс, который является "почти структурой", но имеет некоторые закрытые служебные данные. Эти данные (в приведенном примере -- простой атрибут) имеют инвариант. Утверждается, что обновление атрибута происходит абсолютно независимо от обновления открытых членов класса coupl е. Начнем с утверждения об абсолютной независимости. Я не согласен! Обновление может быть независимым, но понятно, что сам атрибут не является независимым от этих значений; иначе он бы не был сгруппирован с ними в один объект. Атрибут de-leted - приложение к сопутствующим объектам. Вот как мы можем решить поставленную задачу с использованием функций доступа. пример 17-3(6): корректная инкапсуляция, изначально использующая встраиваемые функции доступа, позже при необходимости эти функции могут превратиться в нетривиальный код (или остаться такими, как есть). tempiate<class т, class u> class couple { couple() : deleted (false) { } return first ; } return second ; } deleted. = true; } return deleted.; } T& Fi rst() U& SecondO void MarkDeleted() bool IsDeletedO private: T first.; и second.; bool deleted.; "Hy и зачем беспокоиться о ничего не делающих функциях доступа?" - мог бы спросить, не подумав, кто-то из читателей. Отвечаю. Как уже упоминалось в обсуждении примера 17-2(6), если сегодня вызывающему коду потребовалось изменение некоторого аспекта данного объекта (в данном примере атрибут deleted.), то завтра может потребоваться добавление в него новых возможностей - даже если это будут всего лишь средства для отладки. Пример 17-3(6) обеспечивает вас необходимой гибкостью, которая не вызывает лишних затрат из-за использования встраиваемых функций. Пусть, например, месяц спустя вам потребуется фиксировать все обращения к объектам, помеченным как удаленные. 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 [ 33 ] 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
![]() |