
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfI 610 I |
Часть III |
|
double vol = cyl2.getVolume(); |
// отсутствует в Circle |
|
cout « |
" Circumference of first cylinder: " « length « endl; |
|
cout « |
" Volume of the second cylinder: " « |
vol « endl; |
cout « |
" Diameter of the first cylinder: " « |
diam « endl; |
cout « |
"Area of first cylinder: " « cyl1.getArea()« endl; |
return 0;
}
Когда производный класс переопределяет метод базового класса, он исполь зует то же имя метода. В данном примере как у базового, так и у производного методов интерфейс одинаковый. Это часто происходит потому, что обе функции выполняют подобные операции. Объекты операции в некоторой степени отлича ются, но их семантика одна и та же. Их интерфейсы также будут одинаковыми.
В правилах C+-I- об этом ничего не говорится. Вы можете переписать базовую функцию с тем же интерфейсом, но делать это не обязательно. По какой-то причине многие программисты С+Н- верят, что интерфейс должен быть тот же. Это не так. Изменяет ли проектировщик производного класса интерфейс метода или сохраняет тот, что и в базовом классе, наименование базового метода скры вается от клиентской программы производного класса. Это метод производного класса, который вызывается из клиентской программы. Клиентская программа может использовать базовый метод, но в этом случае потребуется явное задание от объекта базового класса выдать команду компилятору и визуальную подсказку для читателя.
Cylinder |
су11(2.5,6.0), |
су12(5.0, |
|
7.5); |
|
|
|
/ / |
Инициализация данных |
||||
double |
length = cyl1.getLength() |
; |
|
|
|
/ / |
подобно Circle |
||||||
cyl1.set(3.0) ; |
|
|
|
|
|
|
|
|
|
|
|||
double diam = 2 * cyl1 .getRadiusO; |
|
|
|
|
|
|
|||||||
double vol = cyl2.getVolume(); |
|
|
|
|
|
/ / |
отсутствует |
в Circle |
|||||
cout |
« |
" |
Circumference of f i r s t |
|
cylinder |
: " |
« length |
« endl; |
|
||||
cout |
« |
" Volume of the second cylinder |
: |
" |
« |
vol « endl; |
|
||||||
cout |
« |
" |
Diameter |
of |
the f i r s t |
cylinder: |
" |
« |
diam « |
endl; |
|
||
cout |
« |
" |
Side area |
of |
f i r s t cylinder: |
" |
|
|
|
|
|
||
|
« |
cyl1.Circle::getArea() |
« |
endl; |
|
|
/ / |
визуальная |
подсказка |
Достоинства и недостатки наследования и композиции
Наследование является хорошим абстрактным средством. Оно явно подчер кивает концептуальные связи между классами, если они существуют. Например, общность классов Circle и Cylinder лучше всего отражается в структуре про граммы получением производного класса Cylinder из класса Circle. Эти связи' наследования показаны в программе Cylinder. Программисты клиентской части и лица, осуществляющие сопровождение, не должны отдельно изучать классы, сравнивать тексты программы.
За счет использования наследования сокращается объем работ по разработке программы. Существуют и другие способы. Многие программисты все еще верят, что легче разработать сложный класс, чем единый модуль. Например, разработка в первую очередь класса Circle позволяет проектировщику сконцентрироваться на относительно простых вещах (вычисление длины окружности) и попытаться позже найти решение более сложных задач (вычисление объема или площади поверхности цилиндра), когда класс Circle уже разработан.
Однако использование наследования вводит дополнительные неявные зависи мости между классами. Они не являются очевидными для проектировщиков или лиц, осуществляющих сопровождение клиентской программы. При изучении
Глава 14 • Выбор между наследованием и ко1н^позицией |
611 |
описания производного класса читатель не видит список сервисов, доступных клиентской программе. Читатель должен также изучить базовый класс.
Использование композиции класса составляет хорошую конкуренцию насле дованию. Составной класс предоставляет читателю полный список сервисов, которые поддерживаются классом. Композиция также вводит зависимости между классами. Однако эти явные зависимости имеют вид однострочных методов, кото рые передают работу от объемлюш.его класса к методам составного класса.
Выбор наследования или композиции зависит от того, насколько похожи свя занные классы. Если обш,их методов немного, а число дополнительных сервисов, которые должны поддерживаться, очень велико, то композиция — подходящий способ. Составной класс будет содержать только несколько однострочных функ ций, а затраты на их написание будут возмещены за счет использования явного списка поддерживаемых сервисов.
Если количество общих методов относительно велико, а число дополнитель ных сервисов небольшое, то более подходящим способом является наследование. Многие программисты раздражены необходимостью записывать "бессмысленные" однострочные методы — методы базового класса будут непосредственно наследо ваться производным классом. Переопределение базовых методов в производном классе привлекает эстетически и открывает способ для использования полимор физма (см. следующую главу).
При выборе наследования используйте его наиболее простым способом, на пример общедоступным выводом. Избегайте защищенного и закрытого наследо вания.
Часто наследование используется для ускорения работы, без ясной концеп туальной связи "is а" между классами. Если есть сомнения, что между классами существует естественная связь "является", следует использовать не наследование, а композицию.
Унифицированный язык моделирования
Традиционные программы пишутся как системы взаимодействующих функций. Объектно-ориентированные программы создаются как системы взаимодействую щих классов, которые включают и данные, и функции. В первой части книги основное внимание уделялось методам написания функций. Навыки написания программ обработки данных и реализации связей между функциями являются критическими для создания высококачественных программ на С+Н-.
Во второй части книги рассматривается написание классов. Навыки связыва ния в одно целое данных и операций и реализация классов как серверов и клиен тов являются решающими для создания высококачественных программ на C+ + .
Вэтой части книги внимание сосредоточено на написании связанных между собой классов. Здесь рассказывается о том, что навыки реализации связей между классами, например наследованием и композицией, весьма существенны при со здании высококачественных программ на C++.
Впрограмме на C++ трудно визуально представить связи между классами, реализованными программой. Более того, когда программа достаточно усложня ется, становится трудно визуально представить связи между классами, которые предполагается реализовать.
Цели использования UML
Обычный подход к решению проблемы заключается в использовании графиче ского представления для описания связей объектов реальной жизни — кругов и цилиндров, клиентов и счетов, товарно-материальных ценностей и поставщиков, поведение которых должно имитировать приложение. В этом состоит задача объектно-ориентированного анализа, при котором действия системы описываются в виде взаимодействия классов, а не операций (функций).
I 612 |
Чость ill # Прогром1^л11ро8ан1^е с агрвтроваытет и насдедование1У1 |
Вам надо знать, какие объекты реального мира должны представляться в про грамме C + + как классы и какие связи между ними должны быть реализованы
впрограмме C + + как связи между классами. В этом состоит задача объектноориентированного проектирования, в котором для описания классов и их связей
впрограмме также используется графическое представление.
Вотличие от объектно-ориентированного анализа систем, который сосредото чен на описании внешних интерфейсов программы (с пользователями и другими системами), в объектно-ориентированном проектировании основное внимание уделяется описанию структурных компонентов системы: системной архитектуры, размещению подсистем по различным аппаратным компонентам и связям между различными классами. Большинство связей — это связи между объектами, уста новленными на этапе объектно-ориентированного анализа. Однако в программе могут быть добавлены некоторые дополнительные связи между классами для по вышения производительности системы и улучшения интерфейса пользователя.
Вдвух первых частях книги обсуждались методы создания системных компо нентов, которые в меньшей степени зависят друг от друга. Это логичный подход, поскольку зависимости между компонентами системы требуют взаимодействия разработчиков, порождающего лишние ошибки. Но реально не стоит ожидать, что компоненты системы будут полностью независимы друг от друга. Будучи частью одной системы, они должны взаимодействовать с другими компонентами. Важно точно описать такое взаимодействие.
Рассмотрим нотацию UML. В традиционном процессе разработки систем на каждом этапе анализа, проектирования и реализации используются различные методики графического представления для поддержки разнообразных интересов аналитика, проектировщика и программиста. В процессе разработки с исполь зованием объектно-ориентированного подхода аналитик, проектировщик и про граммист используют одну нотацию. Во-первых, все совместно работающие разработчики — аналитики, проектировщики и программисты — используют одинаковую систему графического представления, следовательно, остается мень ше возможностей для неверного понимания или различной интерпретации предпо ложений, не выраженных явно. Во-вторых, отсутствуют радикальные изменения представления на разных этапах процесса разработки, следовательно, возникает меньше ошибок во время преобразования.
UML является мощным языком моделирования. Он позволяет разработчику использовать графическую нотацию для представления связей между взаимодей ствующими компонентами системы. "Взаимодействие" означает, что компоненты системы знают друг о друге и зависят друг от друга. Графическая нотация основы вается на концепции объекта как компонента системы с его элементами данных, функциями-членами и связями. Диаграммы объектов можно обсудить с пользова телями и проектировщиками системы, чтобы проверить, правильно ли отражены связи. Позднее эти диаграммы преобразуются в объектно-ориентированные при ложения.
Суть объектно-ориентированного программирования заключается в связыва нии данных и операций. Именно комбинация конкретных элементов данных и операций над ними характеризует класс в C+ + . В этом определении ничего не говорится о связях. При этом создается неверное впечатление о том, что объеди нение данных и поведения достаточно хорошо описывает объект.
Объектно-ориентированный анализ и объектно-ориентированное проектиро вание используют другой подход. Объекты описываются в них как комбинация данных, поведения и связей с другими программными компонентами. Как можно видеть, описание данных и поведения в UML играют скорее базовую роль. Нота ция UML в большей степени концентрируется на связях классов и объектов.
В объектно-ориентированном программировании преуменьшается значение связей или ассоциаций, поскольку программисты пытаются добиться как можно большей независимости программных компонентов друг от друга. В результате
Глава 14 • Выбор между наследованием и композицией |
| |
613 |
| |
все объектно-ориентированные языки предоставляют программисту специальные |
|
||
средства для описания элементов данных и функций-членов. Языки не дают про |
|
||
граммистам собственные средства для описания взаимосвязей программных ком |
|
||
понентов. |
|
|
|
В реальной жизни программные компоненты всегда связаны друг с другом. |
|
||
Следовательно, программисты описывают эти связи. Они используют следуюндие |
|
||
методики: элементы данных с типами, определенными пользователем, или элемен |
|
||
ты данных, которые являются указателями или ссылками на другие объекты. |
|
||
Основная задача любой нотации при проектировании, включая UML, состоит |
|
||
в том, чтобы помочь разработчикам описать связи объектов. Когда программа |
|
||
пишется на С4- + , именно программист принимает решение о связи объектов |
|
||
друг с другом. Если возможно реализовать программу таким образом, чтобы она |
|
||
могла выполнять то, что она должна делать, замечательно. Используя нотацию |
|
||
UML, разработчики могут сравнить различные проектные решения и выбрать |
|
||
такие взаимосвязи, которые (1) достаточны для выполнения задания, и (2) упро- |
|
||
идают связи между программными объектами. |
|
|
|
В UML объединены три различные нотации для построения графических моде |
|
||
лей компьютерных систем. Эти модели помогают разработчикам проанализиро |
|
||
вать требования к системе. Они обычно описывают функциональные возможности |
|
||
системы, интерфейс пользователя, интерфейсы с другими системами, производи |
|
||
тельность и надежность. Графические методы пытаются представить эти требова |
|
||
ния в виде связей между системными компонентами. |
|
|
|
Одна система нотации была разработана Гради Бучем (Grady Booch) и его |
|
||
компанией Rational Software Corporation. Она включала несколько представлений |
|
||
системы с отдельными диаграммами для каждого представления. Обозначения |
|
||
объектов на диаграммах Буча имели неправильную форму "облака" и их трудно |
|
||
было нарисовать от руки. Буч был одним из первых, кто понял, что графическое |
|
||
моделирование требует поддержки компьютерных инструментальных средств. Ин |
|
||
струментальное средство Rational Rose, разработанное его компанией, является |
|
||
одним из наиболее успешных средств объектно-ориентированного моделирования. |
|
||
С созданием UML Rational Rose была модифицирована для поддержки нотации |
|
||
UML. |
|
|
|
Другая нотация была разработана Джеймсом Рамбо (James Rumbaugh) и его |
|
||
коллегами в General Electric. Она получила название Object Modeling Technique |
|
||
(ОМТ). В дополнение к объектной модели, которая описывает связи между объек |
|
||
тами в системе, нотация ОМТ также включала две другие модели: динамическую |
|
||
и функциональную. Хотя эти две модели были не совсем объектно-ориентирован |
|
||
ными, они представляли адаптацию двух, хорошо известных методик |
анализа |
|
|
и проектирования: диаграмм переходов состояний и диаграмм потоков данных. |
|
||
Этот синтез сгладил переход к объектно-ориентированному подходу для разработ |
|
||
чиков, которые имели опыт использования этих двух графических нотаций. Веро |
|
||
ятно, это основная причина того, что ОМТ стала новым стандартным подходом. |
|
||
Третья нотация разработана в Швеции Иваром Якобсоном (Ivar Jacobson) |
|
||
и его компанией Objective Systems. Он представил на рынке свою нотацию под |
|
||
названиями Object-Oriented Software Engineering (OOSE) и Objectory. Эта нота |
|
||
ция включает так называемые варианты использования ("use cases"), описываю- |
|
||
и|ие взаимодействия между системой и такими внешними действуюндими лицами, |
|
||
как системный оператор или другие системы. |
|
|
|
Каждая нотация содержала рекомендации по ее исгюльзованию, во-первых, для объектно-ориентированного анализа, во-вторых, для объектно-ориентирован ного проектирования и, в-третьих, для объектно-ориентированного программиро вания. В каждой книге мы пытаемся объяснить, почему объектно-ориентированный подход лучше, чем традиционный. Будем откровенны, эти пояснения не совсем понятны. Они хороши для людей, которые верят в преимуш,ества объектноориентированного подхода.
I 616 Часть III • Програмтирошаиив с агрегирование.
не определен. Обратите внимание на то, что в прямоугольнике указано только имя класса, а атрибуты и операции отсутствуют. Их можно было бы указать здесь, но это только усложнило бы диаграмму классов. Это стоит делать только в том случае, если список атрибутов и операций в некоторой степени проясняет характер связей между объектами.
Ассоциации между объектами обычно являются двунаправленными: если объект Circle связан с объектом Cylinder, то объект Cylinder связан с объектом Circle. Нотация UML позволяет выразить дополнительную информацию о связях между объектами, указывая имена ассоциаций и назначая роли объектам, ассо циированным друг с другом.
На рис. 14.4(b) показано, что объект Person может быть связан с объектом Саг, а объект Саг с объектом Person и объектом Registration. У каждой ассоциа ции есть две метки, одна для прохождения ассоциации в одном направлении, другая для прохождения ее в противоположном направлении. Чтобы избежать путаницы в отношении направления, в котором текст метки соединяет объекты, можно поместить рядом с ним небольшие стрелочки.
На рис. 14.4(a) не удалось найти подходяш,ее название для ассоциации межлу объектами Circle и Cylinder. Они связаны между собой. На рис. 14.4(b) показа но, что объект Person владеет объектом Саг, а объект Саг принадлежит объекту Person. К тому же демонстрируется, что объект Саг регистрируется объектом Registration, а объект Registration регистрирует объект Саг. Определяется роль каждого объекта в связи. Объект Person играет роль владельца, объект Саг — транспортного средства, а объект Registration — документа.
Однако знание только связей между объектами и ролей, которые они играют, не слишком серьезно помогает понять взаимодействие объектов в реальной жизни и объектов программы во время ее выполнения. Правда, когда разработчик знает совсем немного об области приложения, имена ассоциаций и роли объектов могут быть полезны для проведения анализа в соответствуюш.ем направлении.
Часто сравнение различных имен связей в модели классов представляется подобным описанию теории относительности простыми терминами. Основная проб лема описания ассоциаций состоит в том, что любое решение является относитель ным. На рис. 14.4(c) ассоциации между классами Person, Car и Registration описываются с помош,ью различных связей. Другая возможность состоит в ассо циации каждого класса с двумя другими классами. Какой вариант лучше и почему? На этот вопрос нет точного ответа.
В C + + ассоциации можно реализовать ссылками, которые указывают из одно го объекта на другой ассоциированный объект. Другой популярной методикой реализации ассоциации в языке C + + является использование идентификатора объекта как атрибута другого класса. Например, класс Person может иметь атрибут, который идентифицирует объект Саг, ассоциированный с экземпляром Person.
Основы UML:
нотация для агрегации и обобщения
Агрегация — это особый случаем ассоциации. Она указывает, что два класса соединяются через ассоциацию, но ассоциация является специальной. Эта ассо циация обозначает, что связь имеет смысл как "целое-часть". (Один объект яв ляется частью другого объекта либо другой объект содержит первый объект или состоит из нескольких объектов.)
Нотация, используемая в UML для агрегации, такая же, как и для ассоциа ции — это связь между классами. Для указания агрегированного объекта на конце линии между связью и агрегированным объектом изображается незакра шенный ромб. Ромб можно присоединять только с одной стороны, а не с обеих.
|
|
Глава 14 • Выбор между наследованием и композицией |
617 |
|||||
А) |
Круг |
< ^ |
Цилиндр |
С) |
Круг |
D) |
Счет |
|
|
|
|
|
|
7\ |
|
5 |
|
В) |
Цилиндр |
|
Круг |
|
Цилиндр |
Сберегательный |
Текущий |
|
|
|
|
счет |
счет |
||||
|
|
|
|
|||||
Рис. 14 . 5 . Примеры |
совместно |
используемой агрегации, |
композиции и наследования |
На рис. 14.5(a) показано, что объект Circle является частью объекта Cylinder. Фактически не закрашенный ромб указывает, что агрегация используется со вместно, и часть может присутствовать одновременно более чем в одной агрега ции. В композиции совместное использование не допускается. Нотация UML для композиции такая же, как для совместно используемой агрегации. Однако ромб, присоединенный к агрегации, является сплошной фигурой, а не пустой. На рис. 14,5(b) представлена нотация для композиции, в которой объект Circle явля ется частью объекта Cylinde г, но не может быть частью любого другого объекта.
Поскольку агрегация является особым случаем ассоциации, всегда можно представить связь между объектами как ассоциацию, а не агрегацию. Обратитесь к рис. 14.4(a), где ассоциация используется для моделирования связи объектов Circle и Cylinder. Однако эта модель менее точная. Задача проектировш^ика со стоит в представлении агрегирования как агрегации, а не как ассоциации. Правда, если агрегация не представляет связь объектов достаточно хорошо, следует ис пользовать ассоциацию. Борьба между аргументами в пользу обш^ей ассоциации и специальной агрегации часто становится для проектировш,ика источником му чений.
Совместно используемую агрегацию можно реализовать на языке C + + по добно ассоциации, с указателями (или ссылками) на компонентные объекты. Композицию можно реализовать, используя объекты-части как элементы данных агрегированных объектов.
Обобш,ение представляет собой связь более обш,его и более конкретного клас са. Более конкретный класс содержит те же атрибуты и операции, что и более общ.ий класс, и может включать дополнительную информацию: атрибуты или операции. Обобш.ение реализуется в объектно-ориентированных языках програм мирования как наследование. Обобш,ение — это связь "является" ("is а") между классами. Обратите внимание, что это связь классов, а не экземпляров объектов. Класс может быть наследником другого класса в отличие от экземпляра объекта.
Конкретный класс (подкласс) связи обобш^ения наследует все атрибуты, опера ции и ассоциации от обш,его класса (суперкласса) связи. Нотация UML для этой связи использует сплошную линию для указания связи между классами на диа грамме классов. Отличить в связи подкласс от суперкласса на диаграмме можно по небольшому незакрашенному треугольнику на конце линии связи, обозначаю- ш.ей суперкласс.
На рис. 14.5(c) показаны два класса Circle и Cylinder, связанные обобш,ением. В этом случае Circle интерпретируется как обобидение (суперкласс), а Cylinder — как специализация (подкласс).
Если класс используется как суперкласс для нескольких специализаций, то каждый класс представляется на диаграмме классов отдельно, а каждый класс специализации связывается с суперклассом отдельной связью с отдельным тре угольником, обозначаюш^им суперкласс. Обычно применяется только один тре угольник, указывающий на суперкласс, и с ним связывается ка>едый подкласс. На рис. 14.5(d) представлен класс Account, который используется как обобш^ение, и два других класса, SavingsAccount и CheckingAccount, которые представляют различные специализации класса Account.
Глава 14 • Выбор между наследованием и композицией |
Гб19'1 |
Советуем вам указать ассоциации и опустить атрибуты в классе. Логическое обоснование такого подхода заключается в том, что ассоциации представляют точку зрения анализа и проектирования, а атрибуты — точку зрения реализации. Поэтому на этапе анализа обозначаются связи, а при реализации они выражаются соответствующими указателями, элементами данных и т. д. Возможно, с практи ческой точки зрения этот подход не столь важен. Поскольку не стоит беспокоиться о компиляторе UML, следуйте собственной интуиции.
Если в конец ассоциации или агрегации ничего не добавлено, значит, для функ ционирования связи требуется ровно один объект этого класса. На рис. 14.6(a) и 14.6(b) показано, что присутствие одного объекта класса Rectangle является обязательным.
Иногда связь между объектами не фиксируется, а ее множественность изменя ется во время выполнения. Например, класс History в главе 12 ассоциирован с классом Sample. Фактически связь является композицией: объект класса History содержит массив объектов класса Sample. Как можно видеть из листинга 14.2, в массиве отсутствуют действительные объекты Sample. Во время выполнения программы появляются выборки измерений и информация сохраняется в массиве до тех пор, пока завершится программа либо количество объектов Sample будет равно восьми.
UML позволяет представить этот вид переменной множественности, указывая диапазон ассоциированных значений. На рис. 14.6(c) приведен пример, в котором количество ассоциированных объектов может изменяться от нуля до восьми. Если количество объектов не может быть меньше одного, то диапазон начинается с 1, а не с О (например, 1...8).
Часто диапазоны объектов в связи являются искусственными. Почему в Sample не может быть больше восьми объектов? Потому что язык C+-h не допускает определение объекта без указания его длины. Число 8 подходит, как и любое другое число. В предметной области нет никаких указаний на то, что 8 лучше, чем 10, 20, 100 или любое иное число.
С концептуальной точки зрения количество выборок в истории не должно ограничиваться. По этой же причине реализация не должна заставлять проектировш^ика фиксировать конкретное число. В листинге 12.7 показан класс-контей нер с динамически выделяемой памятью, который реализует эту концептуальную модель. На рис. 14.6(d) представлено обозначение неограниченной множествен ности.
Учебный пример: магазин проката
Рассмотрим приложение, в котором ведется учет взятия напрокат и возврата фильмов клиентами в магазине проката видеофильмов.
Для простоты (предполагается, что магазин проката небольшой, а память компьютера велика) будем считать, что для выполнения приложения сначала в память компьютера загружается база данных клиентов и продукции, выдавае мой напрокат. Данные о сдаваемых напрокат фильмах включают наименование фильма, число имеюш,ихся в магазине фильмов и идентификаторы фильмов. Данные о клиенте включают имя и номер телефона клиента (номер телефона используется в качестве идентификатора клиента), количество фильмов, взятых клиентом напрокат, и их идентификаторы.
Данный пример иллюстрирует основные вопросы проектирования классов, определения их связей и оптимизации проекта с точки зрения сведения до миниму ма зависимостей между классами.
Чтобы сделать пример более интересным с точки зрения использования насле дования, добавлена следуюш^ая деталь: данные о фильмах сохраняются в файле с добавлением буквы, указываюш,ей категорию фильма ("f" — для художествен ных фильмов, "с" — для комедий, "h" — для фильмов ужасов). Когда данные