Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 193
Текст из файла (страница 193)
Эти функции-члены предоставляют слишком низкоуровневый интерфейс. Обычно, классы со столь низкоуровневыми интерфейсами уместны в качестве деталей реализации частей более крупных компонентов (если вообще где-либо уместны). В идеале, функции интерфейса должны принимать самоописываемые аргументы. Язык С++ разрешает программисту экспонировать часть реализации класса в качестве части интерфейса. Зта часть реализации может бьггь закрытой (рпча(е или рго(ес(еб), но она доступна компилятору, чтобы он мог вьшелять память под автоматические переменные, встраивать функции и т.д. Отрицательным эффектом такого решения является возможность порождения нежелательных зависимостей в случае, когда пользовательские типы (классы) присутствуют в реализации класса. На самом деле, порождают ли члены класса с типами Уи Хпроблемы или нет, зависит от того, какими на самом деле типами являются У и Х Если это простые типы, вроде 11»т, сотр!ех и зИая, их применение чаше всего вполне оправдано.
Зто весьма стабильные типы, а необходимость включения их объявлений не является чрезмерной нагрузкой для компилятора. Однако если Уи Усами являются интерфейсными классами больших и значимых компонентов, таких как графическая система или система управления банковскими счетами, неразумно зависеть от них столь непосредственно.
В подобных случаях лучше использовать указатели или ссылки: с1азз У; с1азл л; с(азз Х //Х имеет доступ к У и 2 только через указатели и ссылки ( У* а; га Ьз /.. Это разъединяет определение Хот определений Уи Х(остается зависимость Хот имен Уи Я). Реализация Хбудет, конечно же, по-прежнему зависеть от определений Уи Х, но это напрямую не относится к пользователям класса Х Глава 24. Проектирование и программирование 890 Это иллюстрирует важное положение: интерфейс, скрывающий значительную часть информации — как и должен делать полезный интерфейс — имеет намного меньше зависимостей, чем скрываемая им реализация.
Например, определение класса Хможно откомпилировать без доступа к определениям Уи Е Однако определения функций-членов класса Х, манипулирующие обьектами Уи Х, нуждаются в определениях Уи Е При анализе зависимостей зависимости интерфейса и реализации должны рассматриваться отдельно. В обоих случаях для упрощения системы желательно иметь ациклические графы зависимостей. Этот идеал гораздо более важен и достижим для интерфейсов, чем для реализации.
Отметим, что класс может определить три интерфейса: с(азв Х ( рг(газе: У доступен только для членов и друзей ргогесгед: У доступен членам и друзьям, а также УУ членом и друзьям производных классов рибйс: У доступен широкой публике )' Кроме того, друзья тоже относятся к открытому интерфейсу (в11.5). Поля данных должны попадать в интерфейс в самом редком случае. Они должны быть закрытыми, если нет веских причин делать их более доступными.
В последнем случае их желательно делать защищенными, если нет конкретных причин, по которым они должны стать открытыми. Функции и типы, входящие в открытый интерфейс класса, должны соответствовать концепции, которую класс представляет. Отметим еше, что для большей степени сокрытия представления можно использовать абстрактные классы (В2.5.4, В12.3, В25.3).
24.4.3. «Жирные» интерфейсы В идеале интерфейс должен предоставлять лишь те операции, которые имеют смысл и хорошо представимы любым производным классом, реализующим этот интерфейс. Это, однако, не всегда легко сделать. Рассмотрим списки, массивы, ассоциативные массивы, деревья и т.д. Как показано в В16.2.2, соблазнительно и иногда полезно обобщить эти типы до типа контейнер (сопгагпег), который можно было бы использовать в качестве интерфейса ко всем перечисленным структурам данных.
Как кажется, это могло бы освободить пользователя от необходимости работать с деталями многочисленных специализированных контейнеров. Однако определить интерфейс универсального контейнера не так-то легко. Допустим, мы хотим определить Санга(аег как абстрактный тип. Какими операциями его нужно снабдить? Мы можем ввести лишь операции, которые допустимы для любого из перечисленных специализированных контейнеров, то есть фактически пересечение множеств их операций (до смешного узкий интерфейс). Более того, во многих практических случаях такое пересечение представляет собой пустое множество.
Вй1 24.4 Компоненты с1аев Сопаипег ( риЫ<с: в(гис( ВаИ оре< ( сопв< с«аг* р( Ва«орег(сопл(с«аг* рр): р(рр) () <<' класс исключений г!г<иа! гоЫрш(сопи Т') (<йго<г Ва«орег<"Соп(атег:: риг'); ) Мг<иа1 Т* ае(() (йго<г Вай орег("Сон<а!пег::ае(") < ) Ыг<иа1 т'ь орега(о<[] (тн (<йго<г Ва«орег("Сота<пег: < (] (<п<) ") ( ) Ыг<иа! Т*а орега(ог [] (сопи с«аг*) (йтт Вай орег ( Сон<а(пег:: [] (с«аг*) "); ) !<...
)< Тогда конкретные контейнеры можно объявлять следующим образом: с1авв Е!и соп(а(пе(п риЫк Сон<атее, ргина<е Вв< ( риЫк: гоЫ ри( (сопи Т' ); Т* ее<() < <)' .. отсутствует орска(от(] ... ); с!а<в Уес<ог сон<а(пег: риЫк Соп(а<пег, рпга<е гес(ог ( риЫ<с < Т* ь орега<ог [ ] (1п() < Т*ь орега<ог [] (сопл< с«аг*); ,(< ... отсутствуют ркр и ее<!] ... )( Пока соблюдаются предосторожности, все в порядке: гоЫ]() ( Ыи сон<атее вс; Уес<ог сон<а<пег гс( <... ивег(вс, гс) < ) гоЫ ивег (Соп(а!пега с1, Соп(а!пега с2) Т* р1 = с1 .
ее< ( ); В качестве иной крайности мы можем ввести объединение множество всех операций и выдавать сообщение об ошибке, если во время выполнения программы через такой интерфейс произошло обращение к операции, не поддерживаемой объектом. Интерфейс, реализующий объединение интерфейсов ко множеству концепций, называют жирным интерфейсол< []а( !п(енсе). Рассмотрим универсальный контейнер объектов типа Г 892 Глава 24. Проектирование и программирование т* рг = сг(з); /не используйте сг.ее(() или с!(3/ // ... ) Однако немногие структуры данных одновременно поддерживают и индексацию, и операции, характерные для списков.
Поэтому идея объединить эти операции в одном интерфейсе вряд ли хороша. Это влечет либо интенсивный опрос типов во время выполнения (915.4), либо приводит к генерации исключений (глава 14) ради устранения ошибок времени исполнения. Например: ио!й ивег2 (Санга!пега с1, Солта!пега с2) ( пу ( Т* р1 = с! . «е! ( ); т*рг=сг(з); // ... //обнаружение просто, восстановление - нет свеса (Сопгатег:: Вай прего Ьай) / Оорл! //И что теперь делать? или го(й ивегЗ (Санга!пега с1, Сопев!пега с2) //раннее обнаружение не просто; // восстановление также не просто ( (Г(йупат!с сав«глвг сопев!пег*> (ас1) ьа йупат!с сал«Уесгог сопев!пег*> ( ьсг) ) ( Т* р1 = с1.ее!() г т* рг = сг (з); // ...
) е!ле ( // Оорл! //Ичто теперь делотьг ) В обоих случаях может пострадать быстродействие и код может быть излишне большим. В результате, программисты могут понадеяться на лучшее, то есть что пользователи будут работать с программой надлежащим образом и ошибок возникать не будет.
К тому же исчерпывающее тестирование в данном случае труднодостижимо и слишком дорого. Следовательно, жирных интерфейсов лучше избегать, когда производительность является важнейшим условием, когда требуется высокая надежность исполнения 893 24.5 Советы и вообще, когде есть другая альтернатива. Применение жирных интерфейсов ослабляет степень соответствия между концепциями проекта и классами, и открывает простор для использования наследования исключительно ради удобства реализации. 24.5. Советы 1.
Эволюционируйте в направлении абстракции данных и объектно-ориентированного программирования; 924.2. 2. Применяйте средства языка С++ и технологии программирования сообразно необходимости; 924.2. 3. Стремитесь к соответствию стиля программирования стилю проектирования; 924.2.1. 4. В проектировании фокусируйтесь на концепциях/классах, а не на функциях/вычислениях; 824.2.1. 5. Применяйте классы для представления концепций; 924.2.1, 924.3.
6. Применяйте наследование исключительно для отражения иерархических отношений между концепциями; 924.2.2, 924.2.5, 924.3.2. 7. Базируйте строгие гарантии касательно интерфейсов в терминах статических типов уровня приложения; 924.2.3. 8. Для решения четко определенных задач применяйте генераторы кода и другие средства визуального программирования; 924.2.4. 9. Избегайте тех генераторов кода и средств визуального программирования, что плохо стыкуются с языками программирования общего назначения; 924.2.4.
10. Не смешивайте между собой разные уровни абстракции; 924.3.!. 11. Фокусируйтесь на проектировании компонентов; 924.4. 12. Убедитесь, что виртуальные функции имеют четко определенный смысл, и что все замещающие функции реализуют надлежащее поведение; 924.3.4, 824.3.2.!. 13. Используйте открытое наследование для выражения отношения "1з-а"; 924.3.4. 14.