►Разложение...249

Чтобы увидеть, как использовать наследование для упрощения ваших программ, рассмотрим простейшее банковское приложение.

«Предположим, что нам надо написать простейшую банковскую программу. ( Описание этой программы имеется на прилагаемом компакт-диске. )»

[Диск]

Я мог бы до посинения рассказывать об этих классах, однако, к счастью, объектно-ориентированные программисты придумали довольно наглядный и краткий путь описания классов. Классы Checking и Savings показаны на рис. 22.1.

Для того чтобы правильно понять этот рисунок, необходимо знать несколько правил.

■■■

■ Большой прямоугольник — это класс. Имя класса написано сверху.

■ Имена в меньших прямоугольниках — это функции-члены.

_________________

249 стр. Глава 22. Разложение классов         

 

Рис. 22.1. Независимые классы Checking и Savings

■ Имена не в прямоугольниках — это данные-члены.

■ Имена, которые выступают за пределы прямоугольника, ограничивающего класс, являются открытыми; к этим членам могут обращаться функции, не являющиеся членами класса или его наследников. Члены, которые находятся полностью внутри прямоугольника, недоступны снаружи класса.

■ Толстая стрелка обозначает связь типа ЯВЛЯЕТСЯ.

■ Тонкая стрелка обозначает связь типа СОДЕРЖИТ.

■■■

Автомобиль ЯВЛЯЕТСЯ транспортным средством и при этом СОДЕРЖИТ мотор.

На рис. 22.1 вы можете увидеть, что классы Checking и Savings имеют много общего. Например, оба класса включают функции-члены withdrawal( ) и deposit( ). Поскольку эти классы не идентичны, они, конечно же, должны оставаться раздельными ( в реальном банковском приложении эти два класса отличались бы гораздо существеннее ). Однако мы должны найти способ избежать дублирования.

Можно сделать так, чтобы один из этих классов наследовал другой. Класс Savings имеет больше членов, чем Checking, так что мы могли бы унаследовать Savings от Checking. Такой путь реализации этих классов приведён на рис. 22.2. Класс Savings наследует все члены класса Checking. Кроме того, в классе добавлен член noWithdrawal и переопределена функция withdrawal( ). Эта функция переопределена, поскольку правила снятия денег со сберегательного счёта отличаются от правил снятия с чекового счёта ( хотя меня эти правила вообще не касаются, поскольку у меня нет денег, которые можно было бы снять со счёта ).

Хотя наследование Savings от Checking и сберегает наш труд, нас оно не очень удовлетворяет. Главная проблема состоит в том, что оно искажает истинное положение вещей. При таком использовании наследования подразумевается, что счёт Savings является специальным случаем счёта Checking.

"Ну и что? — скажете вы. — Такое наследование работает и сохраняет нам силы и время". Это, конечно, так, но мои предупреждения — это не просто сотрясание воздуха. Такие искажения запутывают программиста уже и сейчас, но ещё больше будут мешать в дальнейшем. Однажды программист, не знакомый с нашими "приёмчиками", будет читать нашу программу, пытаясь понять, что же она делает. Вводящие в заблуждение представления очень трудны для понимания и ведения программы.

_________________

250 стр. Часть 4. Наследование

    

Рис. 22.2. Класс Savings реализован как подкласс checking

Кроме того, такие искажения могут привести к проблемам в будущем. Например, представьте себе, что банк изменит свою политику относительно чековых счетов. Скажем, он решит взимать гонорар за обслуживание чековых счетов только в том случае, если минимальный баланс упадёт ниже некоторого значения в течение месяца.

Такое изменение политики банка можно легко отразить в классе Checking. Все, что нужно сделать, — это добавить новый член в класс Checking, чтобы следить за минимальным балансом в течение месяца. Назовём его minimumBalance.

Однако теперь возникает проблема. Если Savings наследует Checking, значит, Savings тоже получает этот член. При этом он не используется, поскольку в сберегательных счетах минимальный баланс не нужен. Так что дополнительный член просто присутствует в классе. Итак, каждый объект чекового счёта имеет дополнительный член minimumBalance. Один дополнительный член — это не так уж и много, но он вносит свою лепту в общую неразбериху.

Такие изменения имеют свойство накапливаться. Сегодня это один член, а завтра — изменённая функция-член. В результате объекты класса Savings будут содержать множество дополнительных данных, которые нужны исключительно в классе Checking. Если вы будете невнимательны, изменения в классе Checking могут перейти к классу Savings и привести к его некорректной работе.

Далее банк решил изменить правила работы с чековыми счетами. Для этого вам требуется изменить некоторые функции в Checking. Эти изменения автоматически перейдут в подклассы. Предположим, например, что банк решил начислять дополнительные проценты по чековым вкладам ( ну, бывают же чудеса... ) — но главное чудо будет в том, что при нашей системе наследования эти же проценты будут начисляться и на сберегательных счетах.

Как же этого избежать? Если поменять местами Checking и Savings, проблема не исчезнет. Нужен некий третий класс ( назовём его Account ), который будет воплощать в себе всё то общее, что есть у Checking и Savings. Такая связь приведена на рис. 22.3.

Каким образом создание нового класса Account решит наши проблемы? Во-первых, такой класс сделает более аккуратным описание реального мира ( чем бы он ни являлся ). В нашей концепции мира ( по крайней мере, в моей ) действительно есть нечто, что можно назвать счётом.

_________________

251 стр. Глава 22. Разложение классов

Сберегательные и чековые счета являются частным случаем этой более фундаментальной концепции.

Кроме того, класс Savings отмежёвывается от изменений в классе Checking ( и наоборот ). Если банк решит провести фундаментальные изменения во всех счетах, можно просто изменить класс Account, и все подклассы автоматически унаследуют эти изменения. Но если банк изменит политику только для чековых счетов, можно просто модифицировать класс Checking, не изменяя при этом класс Savings.

Такая процедура отбора общих свойств похожих классов и называется разложением. Этот процесс очень важен в объектно-ориентированных языках по причинам, которые были приведены выше, а также потому, что разложение помогает избавиться от избыточности. Позвольте мне повториться: избыточность — это не просто плохо, это очень плохо...

 

Рис. 22.3. Классы checking и Savings, базирующиеся на классе Account

«Разложение будет обоснованным только в том случае, когда взаимосвязь, представляемая наследованием, соответствует реальности. Выделение общих свойств класса Mouse и Joystick и разложение их на "множители" вполне допустимо. И мышь и джойстик являются аппаратными устройствами позиционирования. Но выделение общих свойств классов Mouse и Display ничем не обосновано.»

[Атас!]

Разложение может давать ( и обычно даёт ) результат на нескольких уровнях абстракции. Например, программа, написанная для более "продвинутого" банка, может иметь структуру классов, показанную на рис. 22.4.

Из этого рисунка видно, что между классами Checking и Savings и более общим классом Account вставлен ещё один класс. Он называется Conventional и объединяет в себе особенности обычных счетов. Другие типы счетов, например счета ценных бумаг и биржевые счета, также объявляются как отдельные классы.

Такая многослойная структура классов весьма распространена и даже желательна ( пока отношения, которые она представляет, отражают реальность. Однако не забывайте, что для любого заданного набора классов не существует одной единственно правильной иерархии классов ).

_________________

252 стр. Часть 4. Наследование

 

 

Рис. 22.4. Развитая структура банковских счетов

Представим, что банк позволяет держателям счетов удаленно обращаться к чековым счетам и счетам ценных бумаг. Снимать же деньги с других типов счетов можно только в банке. Хотя структура классов, приведённая на рис. 22.4, выглядит естественной, в данных условиях более приемлема другая структура ( рис. 22.5 ). Программист должен решить, какая структура классов лучше всего подходит к данным условиям, и стремиться к наиболее ясному и естественному представлению.

 

Рис. 22.5. Альтернативная иерархия классов