straustrup2 (852740), страница 87
Текст из файла (страница 87)
Рассмотрим задание класса cfield (controled field - управляемое поле), который,помимо всего прочего, дает возможность контролировать на стадии выполнения доступ к другомуклассу field. На первый взгляд кажется совершенно правильным определить класс cfield какпроизводный от класса field:class cfield : public field {// ...};Это выражает тот факт, что cfield, действительно, есть сорта field, упрощает запись функции, котораяиспользует член части field класса cfield, и, что самое главное, позволяет в классе cfield переопределятьвиртуальные функции из field. Загвоздка здесь в том, что преобразование cfield* к field*, встречающеесяв определении класса cfield, позволяет обойти любой контроль доступа к field:void q(cfield* p){*p = "asdf";// обращение к field контролируется322Бьерн Страуструп.field* q = p;*q = "asdf";Язык программирования С++////////функцией присваивания cfield:p->cfield::operator=("asdf")неявное преобразование cfield* в field*приехали! контроль обойден}Можно было бы определить класс cfield так, чтобы field был его членом, но тогда cfield не можетпереопределять виртуальные функции field.
Лучшим решением здесь будет использованиенаследования со спецификацией private (частное наследование):class cfield : private field { /* ... */ }С позиции проектирования, если не учитывать (иногда важные) вопросы переопределения, частноенаследование эквивалентно принадлежности. В этом случае применяется метод, при котором классопределяется в общей части как производный от абстрактного базового класса заданием егоинтерфейса, а также определяется с помощью частного наследования от конкретного класса,задающего реализацию ($$13.3). Поскольку наследование, используемое как частное, являетсяспецификой реализации, и оно не отражается в типе производного класса, то его иногда называют"наследованием по реализации", и оно является контрастом для наследования в общей части, когданаследуется интерфейс базового класса и допустимы неявные преобразования к базовому типу.Последнее наследование иногда называют определением подтипа или "интерфейснымнаследованием".Для дальнейшего обсуждения возможности выбора наследования или принадлежности рассмотрим, какпредставить в диалоговой графической системе свиток (область для прокручивания в ней информации),и как привязать свиток к окну на экране.
Потребуются свитки двух видов: горизонтальные ивертикальные. Это можно представить с помощью двух типов horizontal_scrollbar и vertical_scrollbar илис помощью одного типа scrollbar, который имеет аргумент, определяющий, является расположениевертикальным или горизонтальным. Первое решение предполагает, что есть еще третий тип, задающийпросто свиток - scrollbar, и этот тип является базовым классом для двух определенных свитков. Второерешение предполагает дополнительный аргумент у типа scrollbar и наличие значений, задающих видсвитка.
Например, так:enum orientation { horizontal, vertical };Как только мы остановимся на одном из решений, определится объем изменений, которые придетсявнести в систему. Допустим, в этом примере нам потребуется ввести свитки третьего вида. Вначалепредполагалось, что могут быть свитки только двух видов (ведь всякое окно имеет только дваизмерения), но в этом примере, как и во многих других, возможны расширения, которые возникают каквопросы перепроектирования. Например, может появиться желание использовать "управляющуюкнопку" (типа мыши) вместо свитков двух видов.
Такая кнопка задавала бы прокрутку в различныхнаправлениях в зависимости от того, в какой части окна нажал ее пользователь. Нажатие в серединеверхней строчки должно вызывать "прокручивание вверх", нажатие в середине левого столбца "прокручивание влево", нажатие в левом верхнем углу - "прокручивание вверх и влево". Такая кнопка неявляется чем-то необычным, и ее можно рассматривать как уточнение понятия свитка, котороеособенно подходит для тех областей приложения, которые связаны не с обычными текстами, а с болеесложной информацией.Для добавления управляющей кнопки к программе, использующей иерархию из трех свитков, требуетсядобавить еще один класс, но не нужно менять программу, работающую со старыми свитками:свитокгоризонтальный_свитоквертикальный_свитокуправляющая_кнопкаЭто положительная сторона "иерархического решения".Задание ориентации свитка в качестве параметра приводит к заданию полей типа в объектах свитка ииспользованию переключателей в теле функций-членов свитка.
Иными словами, перед нами обычнаядилемма: выразить данный аспект структуры системы с помощью определений или реализовать его воператорной части программы. Первое решение увеличивает объем статических проверок и объеминформации, над которой могут работать разные вспомогательные средства. Второе решение323Бьерн Страуструп.Язык программирования С++откладывает проверки на стадию выполнения и разрешает менять тела отдельных функций, не изменяяобщую структуру системы, какой она представляется с точки зрения статического контроля иливспомогательных средств. В большинстве случаев, предпочтительнее первое решение.Положительной стороной решения с единым типом свитка является то, что легко передаватьинформацию о виде нужного нам свитка другой функции:void helper(orientation oo){//...p = new scrollbar(oo);//...}void me(){helper(horizontal);}Такой подход позволяет на стадии выполнения легко перенастроить свиток на другую ориентацию.Вряд ли это очень важно в примере со свитками, но это может оказаться существенным в похожихпримерах.
Суть в том, что всегда надо делать определенный выбор, а это часто непросто.Теперь рассмотрим как привязать свиток к окну. Если считать window_with_scrollbar (окно_со_свитком)как нечто, что является window и scrollbar, мы получим подобное:class window_with_scrollbar: public window, public scrollbar {// ...};Это позволяет любому объекту типа window_with_scrollbar выступать и как window, и как scrollbar, но отнас требуется решение использовать только единственный тип scrollbar.Если, с другой стороны, считать window_with_scrollbar объектом типа window, который имеет scrollbar,мы получим такое определение:class window_with_scrollbar : public window {// ...scrollbar* sb;public:window_with_scrollbar(scrollbar* p, /* ... */): window(/* ... */), sb(p){// ...}};Здесь мы можем использовать решение со свитками трех типов.
Передача самого свитка в качествепараметра позволяет окну (window) не запоминать тип его свитка. Если потребуется, чтобы объект типаwindow_with_scrollbar действовал как scrollbar, можно добавить операцию преобразования:window_with_scrollbar :: operator scrollbar&(){return *sb;}12.2.6 Отношения использованияДля составления и понимания проекта часто необходимо знать, какие классы и каким способомиспользует данный класс.
Такие отношения классов на С++ выражаются неявно. Класс можетиспользовать только те имена, которые где-то определены, но нет такой части в программе на С++,которая содержала бы список всех используемых имен. Для получения такого списка необходимы324Бьерн Страуструп.Язык программирования С++вспомогательные средства (или, при их отсутствии, внимательное чтение). Можно следующим образомклассифицировать те способы, с помощью которых класс X может использовать класс Y:•X использует имя Y•X использует Y•-X вызывает функцию-член Y-X читает член Y-X пишет в член YX создает Y-X размещает auto или static переменную из Y-X создает Y с помощью new-X использует размер YМы отнесли использование размера объекта к его созданию, поскольку для этого требуется знаниеполного определения класса. С другой стороны, мы выделили в отдельное отношение использованиеимени Y, поскольку, указывая его в описании Y* или в описании внешней функции, мы вовсе ненуждаемся в доступе к определению Y:class Y; // Y - имя классаY* p;extern Y f(const Y&);Мы отделили создание Y с помощью new от случая описания переменной, поскольку возможна такаяреализация С++, при которой для создания Y с помощью new необязательно знать размер Y.
Это можетбыть существенно для ограничения всех зависимостей в проекте и сведения к минимумуперетрансляции после внесения изменений.Язык С++ не требует, чтобы создатель классов точно определял, какие классы и как он будетиспользовать. Одна из причин этого заключена в том, что самые важные классы зависят от стольбольшого количества других классов, что для придания лучшего вида программе нужна сокращеннаяформа записи списка используемых классов, например, с помощью команды #include. Другая причина втом, что классификация этих зависимостей и, в частности, обЪединение некоторых зависимостей неявляется обязанностью языка программирования.