Бьерн Страуструп. Язык программирования С++. Специальное издание (2011) (1004033), страница 189
Текст из файла (страница 189)
На первый взгляд, сделать Све1И производным от Р1еЫ вполне разумно; с(авв ОгеЫ: риЫЫ Р(е(й (1* ... *) ); гоЫ )г1 (Р1* рвг) ( В* рЬ =рог рЬ= ард- Ьг РЬ->В() г рд->В () г РД- Ь.В(); рь->) (); рг1->Г( ): гоЫ Ь2 (Р2* рг() ( В*рЬ =рог РЬ->В()г р'1 >В () ' рь->) (); рг(->1'() г ) Глава 24. Проектирование и программирование 24.3. Классы 871 Таким образом, с помощью наследования выражается тот факт, что С/1е1а является разновидностью ЛеЫ, повышается удобство записи при использовании функциями СИе!а'общих с Ле1а членов, и — самое важное — позволяет классу СНЫ замешать виртуальные функции класса Ле1а.
Подвох, однако, заключается в том, что неявное преобразование от С)геЫ* к Ле!а", вытекающее из определения типа СИе!й, сводит на нет все попытки контролировать доступ к ЛеЫ: гоЫд(СНЫ* р) ! р = "авв(/» ! У доступ к ЯвЫ яри контроле со стороны операции ариев-ния из С/)в!гй // р-» С/) е!г!карего)ог= !"авар! Ле!й* о = р; ~У неявное приведение от С/)в!д» к ЕвЫ* *о = "ава/" ! // ООРЬУ нвт контроля ) Другое решение — сделать так, чтобы С/геЫ агрегировал (содержал) член типа Ле1а', но это не позволит Сне!а' замещать виртуальные функции класса ЛеЫ. Наилучшим решением в таком случае будет закрытое наследование (рг!ваге с!ег!чаг!оп); с!авв С7)в!а: рпыаге ЛеЫ (/* ... * /); С проектной точки зрения закрытое наследование эквивалентно агрегации (включению) за существенным исключением касательно замещения.
Важный пример применения такого подхода — открытое наследование от абстрактного базового класса, определяющего интерфейс, и одновременно закрытое или защищенное наследование от конкретного класса с целью предоставления реализации (82.5.4, 8! 2.3, 825.3). Поскольку закрытое или защищенное наследование является деталью реализации, не оказывающей влияния на тип производного класса, его иногда называют наследованием реализации (!тр1етеп)айоп!пйепгапсе), что контрастирует с открытым наследованием, подразумевающим наследование интерфейса и неявное преобразование в базовый тип.
Это часто называют подтипизацией (виЬ)ур!пя) или наследованием интерфейса (!пге~асе !пйепгапсе). Другим способом выразить то же самое является утверждение, что «объект производного класса можно использовать там, где допустим объект базового класса». Это часто называют «принципом подстановки Бя!соч» ($23.6[(.!з)гоч,!987]) (Е!в/соч видя!!гиг!оп ргтс!р!е). Различие между рцы)с/ргогесгед/рпчаге формами наследованиями ярко наблюдается для полиморфных типов, объекты которых адресуются через указатели или ссылки.
24.3.4.1. Альтернатива «включение/наследование» Дальнейшее изучение альтернативы «включение (агрегация)/наследование» будем проводить на примере графического интерфейса пользователя: зададимся вопросом, как представить полосу прокрутки (зсго!йзаг) в интерактивной графической системе и как прикрепить ее к окну. Нам нужны два вида полос прокрутки — горизонтальная и вертикальная.
Мы можем представить их двумя типами Ног!ганга! всгоИЬаг и Уегг(са1 всгоИЬаг — или одним типом $сгоИЬаг, принимающим аргумент, определяющий пространственное расположение (горизонтальное или вертикальное). В первом случае нужен еще и третий тип — ЯсгоИЬаг (полоса прокрутки вообще), выступающий в качестве базового класса для двух частных ти- вуг Глава 24. Проектирование и программирование пов полос прокрутки. Во втором случае нужен дополнительный аргумент для задания типа полосы прокрутки и два его значения для идентификации этих вариантов полос. Например: епигп Опепгапоп ( Ьог(холегг, гегггсаг) г Выбрав один из двух вариантов, мы сталкиваемся с необходимостью дальнейших расширений системы.
Например, может обнаружиться необходимость ввести третий тип полосы прокрутки. Первоначально мы могли думать, что существует лишь две возможности для полос прокрутки («в конце концов, окно имеет лишь два измерения»), И, тем не менее, есть и другие возможности, требующие изменений в первоначальном проекте. Например, кому-то может прийти в голову использовать одну «навигационную кнопку» вместо двух полос прокрутки.
Такая кнопка может осуществлять прокрутку в разных направлениях в зависимости от того, где пользователь нажал эту кнопку: если где-то вверху посередине — то прокрутка выполняется вертикально вверх; если слева — то прокрутка выполняется горизонтально влево; если в районе верхнего левого угла — то одновременная прокрутка горизонтально влево и вертикально вверх и т.д. Такие кнопки встречаются в реальной практике.
Их можно рассматривать как развитие идей полос прокрутки. Добавление навигационной кнопки в программу с иерархией трех классов полос прокрутки означает добавление нового класса, но это не влечет за собой никаких изменений в коде старых полос прокрутки: ггеМсаг гсгоггьаг Ноггхопга! гсгоИЬаг )Уаг(даМоп Ьииоп Это очень приятный аспект иерархического решения. Задание ориентации полосы прокрупси в качестве аргумента означает наличие полей типа в объектах полос прокрутки и наличие операторов злегсй в коде функций-членов. Таким образом, проектный выбор между двумя вариантами системы полос прокрутки означает выбор между объявлениями и лишним кодом. Первый вариант увеличивает степень статической проверки н увеличивает объем информации, доступной автоматизированным инструментам. Второй вариант откладывает выбор до момента исполнения программы и позволяет вносить изменения посредством модификации кода индивидуальных функций, не касаясь общей структуры системы, как она видится с точки зрения проверки типов н иных инструментальных средств.
Лично я предпочитаю классовую иерархию, непосредственно моделирующую иерархические отношения между концепциями системы. Решение с одним типом полосы прокрутки облегчает задачу хранения и передачи информации о типе полосы: гоЫ Ье!рег (Омепгапоп оо) ( // р = леп Ясгопьаг (оо); 24.3. Классы 873 гоЫ ее() ( йе(рег (Ьог/яопяа1) я / ... Такое решение может облегчить переориентацию полос прокрутки прямо во время выполнения программы. Вряд ли здесь это имеет большое значение, но во многих иных случаях это важно. В обшем, альтернатива есть, но выбор не прост. 24.3.4.2. Альтернатива «агрегация/наследование» Теперь рассмотрим вопрос о том, как привязать полосу прокрутки к окну.
Если мы будем считать класс )Ипйов вйй ясгоИЬаг чем-то таким, что одновременноявляется и окном, и полосой прокрутки, то получим нечто вроде с(ояя )г/плов вИИ ясгоИЬог: риЬИс )Иолов, риЬИс $сгоййа ( У... ); Это позволяет любому Вгяайов нйгй ясго11йаг действовать и как $сюИЬаг, и как ЬИпИов, но ограничивает нас одним типом полосы прокрутки.
С другой стороны, если мы будем рассматривать ЬИпя(ов н Ий ясю11Ьаг как окно, содержащее полосу прокрутки, то получим следуюшее; с(аяя И'1»лов в1(Ь ясгоИЬ«г: риЫ(с И'1»йов // .. $сгоИЬаг* яЬ; риЫ(с: И1»Иов в/гй ясгоИЬаг($сгоИЬаг* р, /* ... */) )г/плов(/* ...*/], яЬ(р) (/*...*/) //... )' Это позволяет применить решение с иерархией полос прокрутки. Передача типа полосы в качестве аргумента позволяет окну не знать точного типа своей полосы прокрутки. Мы даже можем передать $сго11йаг так же, как мы передавали ориентацию Ог(епгапоп (524.3.4.1). Если понадобится, чтобы ИйЫов вйй ясюИЬаг действовал как полоса прокрутки, достаточно добавить операцию преобразования: И1»Иов в(уй ясгоИЬаг:: орегаяог $сгоИЬагя () гелии *яЬ; На мой взгляд, предпочтительнее, чтобы окно содержало полосу прокрупсн.
Мне легче представить окно, имеющее полосу прокрупси, чем окно, которое по совместительству еше и полоса прокрупси. Моя излюбленная стратегия проектирования со- Глава 24. Проектирование и программирование 874 стоит в том, что полоса прокрутки есть специальный вид окон, который содержится в окнах, нуждающихся в полосах прокрутки. Такая стратегия отдает предпочтение агрегации перед наследованием. Другой аргумент в пользу этого решения проистекает из эмпирического правила, заключающегося в вопросе «может ли окно содержать несколько полос прокрутки» (З24,3.4). Поскольку это действительно так (почему окно не может содержать, например, горизонтальную и вертикальную полосы прокрутки), класс В'йи1оп яйй всгоИЬаг не следует делать производным от ИсгоИЬаг.
Заметьте, что невозможно создать производный класс от неизвестного класса. Точный тип базового класса должен быть известен на этапе компиляции (з12.2). С другой стороны, если атрибут класса передается конструктору в качестве параметра, в классе где-нибудь должно быть представляющее его поле данных, Если тип этого поля (члена класса) — указатель или ссылка, мы можем передать объект класса, производного от типа, ассоциированного с указателем или ссылкой. В частности, поле данных зЬ типа ЮсгоИЬаг* из предыдущего примера может реально указывать на объекты полос прокрутки типа Маг(дапоп Ьппоп, неизвестные пользователям ЮсгоИЬаг*. 24.3.5.
Отношение использования Для проекта важно знать и понимать, как класс использует другие классы. Такие зависимости проявляются в языке С++ неявно. Класс может использовать только имена, которые были где-то объявлены. Для извлечения всех используемых имен требуется либо специальный инструмент, либо внимательное чтение файлов проекта. Способы, которыми класс Х использует класс К можно классифицировать по-разному, например следующим образом; ° Х использует имя К ° Х использует К ° Х вызывает функцию-член К ° Х читает поле данных класса К ° Х записывает поле данных класса У. ° Х создает объект типа К ° Х выделяет память под автоматические илн статические объекты У.
° Хсоздает объект класса Упри помощи пеп. ° Х принимает размеры К Получение в качестве аргумента размеров объекта классифицируется отдельно, поскольку в этом случае требуется знакомство с объявлением класса, но нет зависимости от конструкторов. Пользование именем Утакже классифицируется отдельно, поскольку используя У" или упоминая Ув объявлении внешней функции, мы не нуждается в знании объявления класса У(З5.7): с!алл У; й У вЂ” имя класса У* р. ехгегп Уг'(сопле Уь); Часто важно различать зависимости от интерфейса класса (объявления класса) и зависимости от реализации класса (определение классовых функций-членов).
24.3. Классы 875 Последние имеют еще много других зависимостей, менее интересных пользователю, чем зависимость от интерфейса класса 524.4.2). Как правило, целью проектирования является минимизация зависимостей от интерфейса, ибо они становятся зависимостями пользователей класса 58.2.4.1, 59.3.2, 512.4.1.1, 524.4). Язык С++ не требует от программиста специфицировать, какие другие классы используются и каким образом. Отчасти это связано с тем, что число таких зависимостей может быть весьма велико, что при полном их описании чрезвычайно затруднит читабельность кода.
И, вообще, это не вопрос языка программирования. Скорее, это вопрос проектирования системы, который может поддерживаться специальными инструментами. 24.3.6. Программируемые отношения Язык программирования не может и не должен напрямую поддерживать любую и каждую концепцию проектирования. Подобным образом и методы, н технологии проектирования не должны поддерживать все особенности любого языка программирования. Язык проектирования обычно богаче и менее озабочен деталями, чем язык программирования. Язык программирования должен быть способен поддержать множество идей (идиом) проектирования, или он потерпит неудачу в приспособлении к проекту.
Когда язык программирования не может напрямую поддержать некоторую идею проектирования, нужно косвенно отобразить проектную конструкцию в конструкции языка программирования. Пусть, например, в проекте введено понятие делегироеаиия (с(е)еяаг)ол) посредством требования, чтобы все операции, не определенные в классе А, передавались (делегировались) для выполнения объекту класса В, адресуемому указателем р.