UML2 и унифицированный процесс. Джим арлоуайла нейштадтпрактический объектно ориентированныйанализ и проектированиеu
![]()
|
10.3. Наследование классов В иерархии обобщения (рис. 10.2) кроется наследование между клас сами, посредством которого подклассы наследуют все возможности Shape Square (квадрат) Circle (круг) Triangle (треугольник) более общий элемент более специальные элементы родитель суперкласс базовый класс предок дочерний класс подкласс потомок наследник обобщение специализация Иерархия обобщения «является разновидностью» Рис. 10.2. Иерархия обобщения 232 Глава 10. Наследование и полиморфизм своих надклассов. Чтобы быть более специальными, подклассы насле дуют: • атрибуты; • операции; • отношения; • ограничения. Подклассы наследуют характеристики своего суперкласса. Подклассы также могут вводить новые возможности и переопределять операции суперкласса. Все эти аспекты наследования подробно рас сматриваются в следующих разделах. 10.3.1. Переопределение В примере на рис. 10.3 подклассы Square и Circle класса Shape наследуют все его атрибуты, операции и ограничения. Это означает, что хотя мы и не видим этих элементов в подклассах, они присутствуют в них неяв но. Говорят, что Square и Circle типа Shape. Обратите внимание, что операции draw() (отрисовать) и getArea() (найти площадь), определенные в Shape, не подходят для подклассов. Посы лая сообщение draw(), мы ожидаем, что объект Square отрисует квадрат, а объект Circle – круг. Очевидно, что стандартная операция draw(), уна следованная обоими подклассами от их родителя, не годится. Факти чески данная операция может вообще ничего не отрисовывать. В кон це концов, откуда ей знать, как должен выглядеть Shape? То же самое можно сказать в отношении операции getArea(). Как вычислить пло щадь Shape? Shape origin : Point = (0,0) width : int {>0} height : int {>0} draw( g : Graphics ) getArea() : int getBoundingArea() : int Square Circle radius: int = width/2 {width = height} Рис. 10.3. Наследование характеристик суперкласса 10.3. Наследование классов 233 Эти проблемы явно указывают на необходимость возможности измене ния поведения суперкласса в подклассах. Классам Square и Circle надо реализовать собственные операции draw() и getArea(), которые переоп ределяют стандартные операции, предоставляемые родителем, и обес печивают более подходящее поведение. На рис. 10.4 все это показано в действии: подклассы Square и Circle пре доставили собственные операции draw() и getArea(), имеющие соответст вующее поведение. • Square::draw( g : Graphics ) – отрисовывает квадрат. • Square::getArea() : int – вычисляет и возвращает площадь квадрата. • Circle::draw( g : Graphics ) – отрисовывает круг. • Circle::getArea() : int – вычисляет и возвращает площадь круга. Подклассы переопределяют унаследованные операции, предоставляя новую операцию с такой же сигнатурой. Чтобы переопределить операцию надкласса, подкласс должен предо ставить операцию с точно такой же сигнатурой, что и у переопределяе мой операции надкласса. UML определяет сигнатуру операции как имя операции, ее возвращаемый тип и типы всех параметров в порядке пе речисления. Имена параметров не учитываются, поскольку они явля ются просто удобным способом обращения к определенному параметру в теле операции и поэтому не считаются частью сигнатуры. Все это замечательно, но важно знать, что разные языки программиро вания могут по разному определять «сигнатуру операции». Напри мер, в C++ и Java возвращаемый тип операции не является частью сигнатуры операции. Таким образом, если операции подкласса и над класса будут отличаться только возвращаемым типом, в этих языках будет сформирована ошибка компилятора или интерпретатора. Shape draw( g : Graphics ) getArea() : int getBoundingArea() : int Square Circle draw( g : Graphics ) getArea() : int draw( g : Graphics ) getArea() : int ширина высота радиус 2 Рис. 10.4. Переопределение унаследованных операций 234 Глава 10. Наследование и полиморфизм 10.3.2. Абстрактные операции и классы Иногда требуется перенести реализацию операции в подклассы. В при мере с классом Shape операция Shape::draw( g : Graphics ) – как раз такой случай. В самом классе Shape обеспечить какую либо разумную реали зацию этой операции невозможно, поскольку неизвестно, как должны отрисоваться «фигуры». Понятие «отрисовка фигуры» слишком абст рактное, чтобы иметь конкретную реализацию. У абстрактных операций нет реализации. Отсутствие реализации операции можно обозначить, сделав ее абст рактной операцией. В UML для этого имя операции просто записыва ется курсивом. Класс с одной или более абстрактными операциями является непол ным, поскольку в нем есть операции, не имеющие реализации. Это означает невозможность создания экземпляров подобных классов. По этому такие классы называют абстрактными. Чтобы показать, что класс является абстрактным, его имя записывается курсивом. Абстрактные классы имеют одну или более абстрактных операций. Соз дать экземпляр абстрактного класса невозможно. В примере на рис. 10.5 абстрактный класс Shape имеет две абстрактные операции: Shape::draw( g : Graphics ) и Shape::getArea() : int. Эти операции реализуются подклассами Square и Circle. Хотя Shape является непол ным, и его экземпляр не может быть создан, оба его подкласса предо ставляют недостающие реализации, являются полными и могут иметь экземпляры. Любой класс, экземпляр которого может быть создан, на зывается конкретным классом. Операция getBoundingArea() является конкретной операцией класса Sha pe, потому что контактная площадь (bounding area) любой фигуры вы числяется одинаково: ширина фигуры умножается на высоту. Shape draw( g : Graphics ) getArea() : int getBoundingArea() : int Square Circle draw( g : Graphics ) getArea() : int draw( g : Graphics ) getArea() : int абстрактные операции абстрактный класс конкретные классы конкретные операции Рис. 10.5. Абстрактный класс Shape и конкретные подклассы Square и Circle 10.3. Наследование классов 235 Использование абстрактных классов и операций обеспечивает два серьезных преимущества: • В абстрактном суперклассе можно определять ряд абстрактных операций, которые должны быть реализованы всеми подклассами Shape. Это можно рассматривать как определение «контракта», ко торый должны реализовать все конкретные подклассы Shape. • Можно написать код управления фигурами и затем подставить Circ le, Square и другие подклассы Shape соответственно. Согласно принци пу замещаемости код, написанный для управления Shape, должен работать для всех подклассов Shape. Более подробно эти преимущества рассматриваются при обсуждении полиморфизма в разделе 10.4. 10.3.3. Уровень абстракции Перед тем как перейти к полиморфизму, неплохо было бы разобраться в уровнях абстракции. Что не так в модели на рис. 10.6? Сущности, располагающиеся на одном уровне иерархии обобщения, должны находиться на одном уровне абстракции. Ответ: «в уровне абстракции». Иерархия обобщения определяет ряд уровней абстракции, начиная от самого общего, находящегося на са мом верху, вплоть до самого конкретного, находящегося в самом низу иерархии. Всегда необходимо стараться придерживаться одного уров ня абстракции на одном уровне иерархии обобщения. В приведенном выше примере этого нет. JaguarXJS – марка автомобиля. Очевидно, что это более низкий уровень абстракции, чем Truck (грузовик). Исправить данную модель очень просто. Необходимо ввести между JaguarXJS и Ve hicle (транспортное средство) суперкласс Car (автомобиль). 10.3.4. Множественное наследование Множественное наследование – у класса может быть более одного не посредственного суперкласса. Vehicle Truck JaguarXJS Рис. 10.6. Пример неверной иерархии 236 Глава 10. Наследование и полиморфизм UML позволяет классу иметь несколько непосредственных надклас сов. Это называется множественным наследованием (multiple inherit ance ). Подкласс наследуется от всех его непосредственных надклассов. Обычно множественное наследование считают вопросом проектирова ния, поэтому отложим его обсуждение до раздела 17.6.2. 10.4. Полиморфизм Полиморфизм означает «много форм». Полиморфная операция – это операция, имеющая много реализаций. Мы уже видели две полиморф ные операции в примере с классом Shape. Абстрактные операции draw() и getArea() класса Shape имеют две разные реализации: реализацию в классе Square и другую – в классе Circle. У этих операций «много форм», следовательно, они полиморфны. Рисунок 10.7 прекрасно иллюстрирует полиморфизм. Определен класс Shape, имеющий абстрактные операции draw() и getArea(). Полиморфизм означает «много форм». Полиморфные операции имеют много реализаций. Классы Square и Circle наследуются от Shape и предоставляют реализации полиморфных операций Shape::draw() и Shape::getArea(). Все конкретные подклассы Shape должны предоставлять конкретные операции draw() и getArea(), потому что в надклассе они являются абстрактными. Это значит, что в draw() и getArea() все подклассы Shape можно интерпрети ровать (treat) одинаково. Таким образом, набор абстрактных операций является средством определения набора операций, которые должны быть реализованы всеми конкретными подклассами. Это называют контрактом. Конкретный подкласс должен реализовывать абстрактные операции, которые он наследует. Shape draw( g : Graphics ) getArea() : int getBoundingArea() : int Square Circle draw( g : Graphics ) getArea() : int draw( g : Graphics ) getArea() : int полиморфные операции абстрактный суперкласс конкретные подклассы Рис. 10.7. Иллюстрация полиморфизма 10.4. Полиморфизм 237 Очевидно, что реализация операций draw() и getArea() для классов Square и Circle будет разной. Операция draw() для объектов класса Square будет отрисовывать квадраты, а для объектов класса Circle – круги. И реали зации операции getArea() тоже будут разными. Для квадрата она будет возвращать width*height, а для круга – π*r 2 . В этом суть полиморфизма: объекты разных классов имеют операции с одинаковой сигнатурой, но разными реализациями. Инкапсуляция, наследование и полиморфизм – это «три столпа» ОО. Полиморфизм позволяет разрабатывать более простые системы, кото рые проще изменять, потому что разные объекты в них интерпретиру ются одинаково. По сути, полиморфизм является важнейшим аспектом ОО, поскольку предоставляет возможность отправлять объектам разных классов оди наковое сообщение и получать от них соответствующий ответ. Иными словами, если послать объектам класса Square сообщение draw(), они от рисуют квадрат, а если послать такое же сообщение объектам класса Circle, они отрисуют круг. Объекты кажутся разумными. 10.4.1. Пример полиморфизма Здесь представлен пример полиморфизма в действии. Предположим, есть класс Canvas (холст), обслуживающий коллекцию классов Shape. Хотя это и несколько упрощенная картина, работа многих графиче ских систем действительно подобна этому. Модель такой простой гра фической системы приведена на рис. 10.8. Нам уже известно, что создать экземпляр Shape (поскольку это абст рактный класс) нельзя. Но согласно принципу замещаемости можно создать экземпляры его конкретных подклассов и использовать их везде, где требуется класс Shape. Итак, хотя на рис. 10.8 показано, что объекты типа Canvas содержат коллекцию объектов Shape, на самом деле в коллекцию могут входить только экземпляры конкретных подклассов Shape, потому что сам Shape является абстрактным и не может иметь экземпляров. В данном Shape draw( g : Graphics ) getArea() : int getBoundingArea() : int Square Circle draw( g : Graphics ) getArea() : int draw( g : Graphics ) getArea() : int Canvas 1 * Рис. 10.8. Диаграмма классов: полиморфизм в действии 238 Глава 10. Наследование и полиморфизм случае есть два конкретных подкласса, Circle и Square. Таким образом, коллекция может содержать объекты Circle и/или объекты Square. С полиморфизмом объекты разных классов по разному отвечают на одно и то же сообщение. На рис. 10.9 представлена модель объектов, соответствующая диа грамме классов, изображенной на рис. 10.8. Эта модель объектов по казывает, что у объекта :Canvas имеется коллекция из четырех объек тов Shape: s1, s2, s3 и s4, где s1, s3 и s4 – объекты класса Circle, а s2 – объ ект класса Square. Что происходит, когда объект :Canvas посылает каж дому объекту этой коллекции сообщение draw()? Каждый объект, что и не удивительно, реагирует правильно: объекты Square отрисовывают квадраты, а объекты Circle – круги. Именно класс объекта определяет, что отрисовывает объект. Иначе говоря, класс объекта определяет се мантику набора операций, предлагаемых объектом. Главное здесь то, что каждый объект отвечает на сообщение вызовом соответствующей операции, заданной его классом. Все объекты одного класса будут отвечать на одно и то же сообщение вызовом одной и той же операции. Это не означает, что все объекты одного класса отвечают на одно сообщение совершенно одинаково. Результаты вызова опера ции обычно зависят от состояния объекта: значений всех его атрибу тов и состояния всех отношений. Например, есть три объекта класса Square с разными значениями атрибутов width (ширина) и height (высо та). Получив сообщение draw(), каждый из этих классов отрисует квад рат (т. е. значение или семантика операции остается неизменной), но все квадраты будут разного размера в зависимости от значений атри бутов width и height. Вот еще один пример. Бизнес правила снятия денег и вычисления про центов отличаются в зависимости от типа банковского счета. Так, для 2.draw( ) 3.draw( ) 1.draw( ) 4.draw( ) :Canvas s2:Square s1:Circle s3:Circle s4:Circle Рис. 10.9. Объект Canvas имеет коллекцию из 4 х объектов Shape 10.4. Полиморфизм 239 текущих счетов обычно существуют ограничения по превышению кре дита, и поэтому они могут иметь отрицательный баланс, в то время как баланс депозитных счетов не может опускаться ниже нуля. Анало гично часто по разному вычисляются и начисляются на счет процен ты. Один из простых способов моделирования этих банковских опера ций показан на рис. 10.10. Описывается абстрактный класс Account (счет). Затем предоставляются конкретные подклассы CheckingAccount (текущий счет) и DepositAccount (депозитный счет). Абстрактный класс определяет абстрактные операции withdraw() (снять деньги) и calculateIn terest() (вычислить процент), которые реализуются по разному каж дым из конкретных подклассов. Конкретные операции тоже могут быть полиморфными, но это считается плохим стилем. Обратите внимание, что мы также переопределили конкретную опера цию deposit() (разместить), предоставив ей новую реализацию в классе ShareAccount (паевой счет). Запомните, что для переопределения опера ции базового класса необходимо только предоставить подкласс с опе рацией, имеющей абсолютно аналогичную сигнатуру. Мы сделали это для ShareAccount, потому что бизнес правила размещения денег на пае вом счете ( ShareAccount) отличаются от правил для других типов счетов ( Account). Например, могут быть правила, определяющие минималь ный размер вклада. Тогда будет две реализации deposit(): одна в Account, а другая в ShareAccount. Это значит, что теперь deposit() является поли морфной операцией. То есть конкретные операции, такие как deposit(), могут быть полиморфными! Необходимо быть очень осторожным при переопределении конкрет ных операций, потому что при этом происходит не просто предоставле ние реализации операции абстрактного суперкласса – в этом случае из меняется существующая реализация. Выяснить допустимость этого можно, проверив спецификацию операции суперкласса и убедившись, Account withdraw( amount ) calculateInterest() deposit( amount ) CheckingAccount withdraw( amount ) calculateInterest() Bank 1 * withdraw( amount ) calculateInterest() deposit( amount ) DepositAccount withdraw( amount ) calculateInterest() ShareAccount Рис. 10.10. Диаграмма классов для банковских операций 240 Глава 10. Наследование и полиморфизм что при этом ее контракт не будет нарушен. Абстрактные операции можно безопасно переопределять всегда, потому что они именно для этого и предназначены. Однако переопределение конкретных опера ций может иметь неожиданные последствия и представлять некото рую опасность. Часто операция подкласса просто выполняет какое то дополнительное действие и вызывает операцию суперкласса. Иначе говоря, она добавляет собственное поведение в операцию суперкласса. Это хороший способ повторного использования и расширения поведе ния конкретной операции суперкласса, поскольку, как правило, он безопасен. Некоторые языки программирования обеспечивают возможность пре дотвратить переопределение конкретной операции суперкласса в под классе. В Java добавление ключевого слова final (финальный) в сигна туру операции явно запрещает переопределение операции. На практи ке в Java считается хорошим стилем определять как final все операции, кроме тех, которые вы хотите явно определить как полиморфные. 10.5. Дополнительные аспекты обобщения В данном разделе мы рассмотрим два углубленных аспекта обобще ния: множества обобщения и множества всех типов. Понятие мно жеств обобщения может быть весьма полезным. А вот множества всех типов используются крайне редко. Они включены в эту книгу глав ным образом для обеспечения полноты изложения. |