Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Штерн В. - Основы C++. Методы программной инженерии - 2003

.pdf
Скачиваний:
267
Добавлен:
13.08.2013
Размер:
28.32 Mб
Скачать

540

Часть III • Программирование с orpempoeaHneivi и наследованием

 

Например, при создании объекта Account можно установить поле тега в О,

 

если объект будет использоваться как расчетный счет. Если планируется работать

 

с объектом как со сберегательным счетом, можно установить поле тега в 1. Это

 

означает, что конструктор должен каким-то образом определять, какой именно

 

создается объект Account.

 

В данном примере предположим, что конструкторы для двух разных видов

 

объектов должны иметь разное число параметров. Кроме того, использование

 

числового значения поля тега — не очень хорошая практика программирования.

 

Разработчик знает, что означают О или 1 в этом поле, а другие программисты

 

могут запутаться. Как сообщить о поле разработчику? Для этого в C++ использу­

 

ются перечисления. Сделаем поле Kind типа перечисления локальным для класса

 

Account. Поскольку тип Kind не предполагается использовать вне класса Account,

 

удобно сделать Kind вложенным в него. Данное имя не будет "загрязнять" гло­

 

бальное пространство имен, и его смогут использовать другие программисты,

 

работающие над проектом.

 

class Account {

enum Kind { CHECKING, SAVINGS } ;

/ /

константы вида счета

double

balance;

 

 

 

 

double

rate, fee;

 

 

/ /

поле тега для вида объекта

Kind tag;

 

 

public:

 

 

 

/ /

расчетный счет

Account(double initBalance = 0)

{

balance = initBalance;

fee

= 0.2;

 

 

 

tag

= CHECKING; }

 

 

/ /

сберегательный счет

Account (double initBalance,

double initRate)

{

balance = initBalance;

rate = initRate;

 

 

 

tag

= SAVINGS; }

 

 

 

 

 

}

 

 

/ /

остальная часть класса Account

Если бы вам улыбнулась фортуна, тип Kind мог бы быть доступным также и для клиента, который мог бы явно указывать вид создаваемого счета. Это означает, что конструктор должен включать в себя параметр вида счета.

Давайте усложним пример, предположив, что начальные проценты по вкладам для всех сберегательных счетов одинаковы, и, следовательно, их не нужно зада­ вать в клиентском коде. Таким образом, классу Account требуется всего один конструктор. Клиенту нужно задавать вид объекта счета, и тип Kind следует сделать глобальным ("загрязнив" пространство имен и увеличив степень взаимо­ действия между разработчиками). Вот как выглядит новый класс Account:

enum Kind {

CHECKING; SAVINGS }

;

/ /

константы вида счета

class

Account {

 

 

 

double

balance;

 

 

 

double

rate, fee;

 

 

 

Kind tag;

 

 

 

 

public:

 

 

 

/ /

поле тега для вида объекта

Account(double initBalance, Kind kind)

/ /

только один конструктор

{ balance

= initBalance; tag

= kind;

/ /

задание поля тега

i f

(tag

== CHECKING)

 

 

 

 

fee = 0.2;

 

 

 

else

i f

(tag == SAVINGS)

 

 

 

 

rate

=6 . 0; }

 

/ /

сберегательный счет

. .

. }

;

 

/ /

остальная часть класса Account

Обратите внимание, что мы стараемся не использовать одну и ту же область памяти для процентов по вкладу, если это объект сберегательного счета, и платы за операцию, если это объект расчетного счета. Если приложение работает

Глава 13 • Подобные классы и их интерпретация

541

с большим числом объектов Account в памяти и память является критическим ресурсом, то такой вариант заслуживает внимания. В противном случае в про­ граммном коде появятся дополнительные зависимости. Подобного альтернатив­ ного использования памяти следует избегать.

Теперь клиент явно использует перечисления для указания вида создаваемого объекта Account. Заметим, что комментарии теперь стали лишними. Они лишь повторяют то, что уже выражено в самом программном коде, поэтому идеи раз­ работчика достаточно эффективно передаются сопровождающему приложение программисту.

Account

a1(1000,CHECKING);

/ /

a1

-

расчетный счет

Account

a2(1000,SAVINGS);

/ /

a2

-

сберегательный счет

"Загрязнения" пространства имен перечислением типа Kind можно избежать, даже если клиенту нужно использовать значения данного типа (как в приведенных выше примерах). Один из способов добиться этого — сделать тип локальным в классе Account:

class

Account {

 

 

 

double

balance;

 

 

 

double

rate, fee;

 

 

 

Kind tag;

 

 

 

 

public:

 

 

 

/ /

поле тега для вида объекта

enum Kind { CHECKING; SAVINGS } ;

/ /

константы вида счета

Account(double initBalance,

Kind kind)

/ /

только один конструктор

{ balance = initBalance; tag

= kind;

/ /

задание поля тега

i f

(tag

== CHECKING)

 

/ /

расчетный счет

 

fee

= 0.2;

 

else

i f

(tag == SAVINGS)

 

 

 

 

rate

= 6.0;

 

/ /

сберегательный счет

 

}

;

 

 

/ / остальная часть класса Account

Теперь клиенту при работе с литеральными значениями перечисления в аргу­ ментах конструктора придется использовать операцию области действия.

Account

a1(1000.Account::Kind::CHECKING);

/ /

a1

расчетный счет

 

Account

a2(1000, Account::Kind::SAVINGS);

/ /

a2

сберегательный

счет

Чтобы эта конструкция работала, тип Kind нельзя определять как закрытую часть класса Account. Обратите внимание, что при использовании данного типа внутри класса Account (для элемента данных tag) не обязательно следовать определению типа. Хотя компиляторы C + -I- являются однопроходными, внутри определений классов они делают два прохода.

Это относится только к новым компиляторам. Некоторые старые компиляторы будут сообш^ать, что тип Kind в определении поля tag не определен. Для таких компиляторов определение типа Kind должно предшествовать определению поля tag. Чтобы в клиенте оно было видимым, его нужно включить в обидедоступную часть определения класса (public). Для согласования этих противоречивых тре­ бований следует добавить в определение класса дополнительные секции public и private.

class Account

{

 

 

double

balance;

 

 

double

rate,

fee;

 

 

public:

 

 

 

 

enum Kind {

CHECKING; SAVINGS } ;

/ /

константы вида счета

private:

 

 

 

 

Kind tag;

 

/ /

поле тега для вида объекта

Начальные балансы: 1000 1000 Расчетный счет: недопустимая операция Итоговые балансы: 899.8 1100.18

542

Часть III • Программирование с агрегированием и насдедовонием

public:

 

 

 

Account(double initBalance,

Kind kind)

{

balance

= initBalance; tag

= kind;

 

i f

(tag

== CHECKING)

 

 

 

fee = 0.2;

 

 

else i f

(tag == SAVINGS)

 

 

 

rate

= 6.0; }

 

.

. .

} ;

 

 

/ /

только один конструктор

/ /

задание поля тега

/ /

сберегательный

счет

/ /

остальная часть

класса Account

Если поле тега правильно инициализируется в конструкторе, разработчик класса Account может защитить клиента от несогласованностей. Чтобы програм­ мист, занимающийся клиентской частью, ошибочно не взял плату за операцию после вызова withdraw() для сберегательного счета, серверный класс Account проверяет характер объекта и применяет плату за операцию только к расчетному счету.

void withdraw(double amount)

/ /

общее для обоих счетов

{ i f (balance > amount)

 

 

{ balance

-= amount;

. / /

только для расчетных счетов

i f (tag

== CHECKING)

balance -= fee; }

Как можно видеть, функциональность applyFeeO теперь реализуется функциейчленом withclraw(), так что клиенту не нужно помнить, какой именно вид объекта следует вызывать. Остается надеяться, что читатели понимают, как действуют принципы сокрытия информации и переноса обязанностей на серверы.

Метод paylnterestO проверяет, является ли объект-получатель сообщения сберегательным счетом. Если это так, то начисляются проценты за день. Если счет является накопительным, то выводится ошибка этапа выполнения, уведом­ ляющая тестировщика, что программист сделал ошибку, вызвав функцию для неверного объекта. Операция прерывается.

Обратите внимание на терминологию. Именно создатель класса Account вы­ полняет работу от имени клиента. В "дообъектно-ориентированном программиро­ вании" клиент вынужден защищать себя собственными силами (или обеспечивать отсутствие ошибок). Во времена объектно-ориентированного программирования обязанности переносятся с клиента на серверный класс.

Это очень распространенный архитектурный подход. Надо научиться его использовать.

Рис. 13-1, Результат

программы

из листинга

13,2

Листинг 13.2 показывает реализацию класса Account, применяющего данную технику для проверки допустимос­ ти действий клиента. Обратите внимание, что тип Kind определен вне класса Account. Результат программы пред­ ставлен на рис. 13.1.

Листинг 13.2. Пример проверки корректности операций клиента на этапе выполнения

#include

<iostream>

 

 

 

using namespace std;

 

 

 

enum Kind { CHECKING. SAVINGS }

;

/ /

константы для вида счета

class Account {

 

 

 

double

balance;

 

 

 

double

rate, fee;

 

 

 

Kind tag;

 

/ /

поле тега для вида объекта

public:

 

 

 

 

Account(double initBalance,

Kind kind)

 

 

{ balance = initBalance; tag

- kind;

/ /

установка поля тега

// для проверки счета // для сберегательного счета
// общая для обоих счетов // общая для обоих счетов
// только для проверки счетов

Глава 13 • Подобные классы и их интерпретация

543

i f (tag == CHECKING) fee = 0.2;

else i f (tag == SAVINGS) rate = 6.0; }

double getBalO

{ return balance; }

void withdraw(double amount)

{if (balance > amount)

{( balance -=amount;

if (tag == CHECKING)

 

balance -= fee; } }

 

 

 

 

 

 

 

void deposit(double amount)

 

 

 

 

 

 

 

{ balance += amount; }

 

 

 

 

 

 

 

void paylnterestO

 

 

 

 

// только для сберегательных

счетов

{ if (tag == SAVINGS)

 

 

 

 

 

 

 

 

balance += balance * rate / 365 /100;

 

 

 

 

 

else if (tag == CHECKING)

 

 

операция\п";

 

 

cout «

"Расчетный счет: недопустимая

 

 

int mainO

 

 

 

 

 

 

 

 

 

 

{ Account a1(1000,CHECKING);

 

 

/ /

a1 -

расчетный счет

 

Account a2(1000,SAVINGS);

 

 

/ /

a2 -

сберегательный счет

 

cout «

"Начальные балансы: " «

al.getBalO

 

 

 

 

«

" " « a2.getBal() «

endl;

 

 

 

 

 

a1.withdraw

(100);

a2.deposit

(100);

/ /

нет проблем

 

 

a1. paylnterestO;

a2. paylnterestO;

/ /

неплохо?

 

 

cout «

"Итоговые балансы: " «

al.getBalO

 

 

 

 

 

«

" " « a2.getBal () « endl;

 

 

 

 

 

return

0;

 

 

 

 

 

 

 

 

 

 

}

 

 

 

 

 

 

 

 

 

 

 

 

 

Так как тип Kind теперь глобальный, клиент может задавать вид счета с помоидью

 

 

одних идентификаторов CHECKING и SAVINGS в вызовах конструктора.

 

 

Account

a1(1000,CHECKING)

 

/ /

a1 -

расчетный счет

 

 

 

Account

a2(1000,SAVINGS);

 

/ /

a2 -

сберегательный

счет

Конечно, это проще того, с чем приходилось работать ранее, когда тип Kind был локальным (Account: :Kind: :CHECKING и Account: :Kind: :SAVINGS).

Такой код проще писать, однако предыдущая версия показывает программисту, сопровождающему приложение, что литералы перечисления принадлежат классу Account и никакому другому классу. Хотя данная версия проще в написании, разработчик должен координировать использование глобального имени Kind с другими программистами, которым оно может потребоваться для других целей. Как уже говорилось в главе 1, в современном подходе к программированию пред­ почтительнее "многословная" запись, требующая больше операторов, а не более "компактный" вариант, если последний требует больше координации между программистами при разработке программы и больше усилий, чтобы разобраться в ней. Не пишите раздутые программы, но сравните код с требованиями легкого понимания программы.

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

544Часть III « Программирование с агрегированием и наследованием

вслучае ошибки. Между тем, в сервере необходим дополнительный анализ типов. Каждый метод контролирует законность операций независимо от других, в соот­ ветствии со значением тега объекта. Для крупной системы с большим числом различных видов объектов такая зависимость от вида объекта делает код сервера слишком объемным.

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

Основная проблема данного подхода в том, что ошибки программирования клиента будут проявляться на этапе выполнения, а не компиляции. Кому-то при­ дется изучить все эти сооби;ения и контролировать клиентскую часть. Хорошо было бы сделать так, чтобы некорректное использование разных видов объектов приводило к синтаксическим ошибкам, а не к ошибкам на этапе выполнения.

Отдельные классы для каждого серверного объекта

Хороший способ решения данной проблемы — создание отдельных классов, чтобы каждый класс реализовывал специализированный класс, а не просто свой­ ства всех подклассов объектов. В нашем примере это означает создание классов CheckingAccount и SavingsAccount.

Каждый из этих классов придется проектировать сначала. CheckingAccount со­ держит все необходимое для работы с расчетным счетом, без каких-либо попыток включить туда средства, связанные со сберегательным счетом.

class

CheckingAccount

{

 

 

 

 

double

balance;

 

 

 

 

 

double

fee;

 

 

 

 

/ /

нет процентов

public:

 

 

 

 

 

 

 

CheckingAccount(double

initBalance)

 

 

{

balance = initBalance;

fee = 0.2; }

/ /

расчетный счет

double

getOal

()

 

 

 

/ /

общий для обоих счетов

{

return balance; }

 

 

 

/ /

общая для обоих счетов

void withdraw(double

amount)

 

 

{

i f

(balance

> amount)

 

 

 

 

 

 

balance

= balance

-

amount - fee; }

/ /

безусловная плата

void deposit(double

amount)

 

 

{

balance += amount;

}

 

 

 

 

} ;

 

 

 

 

 

 

 

 

 

Аналогично, класс SavingsAccount содержит все необходимое для поддержки операций с накопительными счетами. В нем реализованы все нужные функции и не обраш,ается внимание на потребности клиентов с расчетными счетами.

class SavingsAccount {

 

 

double

balance;

 

 

double

rate;

/ /

нет платы за операцию

public:

 

 

 

SavingsAccount(double initBalance)

 

 

{ balance = initBalance; rate = 6.0; }

/ /

сберегательный счет

double

getBalO

 

 

{ return balance; }

/ /

общая для счетов

 

 

 

 

Глава 13 • Подобные классы и их интерпретация

545

 

 

void

withclraw(ciouble

amount)

 

 

 

 

 

{

i f

(balance > amount)

/ /

TOT же интерфейс, разный код

 

 

 

 

balance -= amount; }

/ /

общее для счетов

 

 

 

void

deposit(double

amount)

 

 

 

{

balance += amount;

}

 

 

 

 

 

void

paylnterestO

 

/ /

только для сберегательных

счетов

 

 

{ balance += balance * rate /

365 /

100;

 

 

 

Листинг 13.3 показывает исходный код данной программы, реализующий такой

 

подход. Обратите внимание на отсутствие перечисления для типа Kind. Теперь нет

 

необходимости

ни в локальном,

ни в глобальном аргументе. Хотя каждый вид

 

 

 

 

 

счета использует при инициализации одно и то же число пара­

Начальные балансы:

1000

1000

 

метров, Jклиeнтy не нужно применять тип перечисления для

Итоговые балансы:

899.8

1100.18

указания вида создаваемого счета. Программа явно определяет

 

 

 

 

 

объекты счетов a1 и а2 как объекты класса CheckingAccount

Рис. 13.2. Результат

 

 

или SavingsAccount. Следовательно, каждое определение объ­

 

 

екта вызывает соответствующий конструктор CheckingAccount

из листинга

13.3

или SavingsAccount. Результат программы показан на рис. 13.2.

 

 

 

 

 

Листинг 13.3. Пример отдельных классов для разных подтипов объектов

#include

<iostream>

 

using

namespace std;

 

class

CheckingAccouni: {

 

double

balance;

// нет процентов

double

fee;

public:

 

 

CheckingAccount(double initBalance)

// расчетный счет

{ balance = initBalance; fee = 0.2; }

double getBal ()

// общий для обоих счетов

{ return balance ;}

void withdraw(double amount)

 

{ if (balance > amount)

// безусловная плата

 

balance = balance - amount - fee;

void deposit(double amount)

 

{ balance +=amount; }

 

} ;

 

 

 

class SavingsAccount {

 

double balance;

// нет платы заоперацию

double rate;

public:

 

 

SavingsAccount(double initBalance)

// сберегательный счет

{ balance = initBalance; rate = 6.0; }

double getBalO

// общая для счетов

{ return balance; }

void withdraw(double amount)

// тот же интерфейс, разный код

{ if (balance > amount)

 

 

balance -=amount; }

 

void deposit(double amount)

// общее для счетов

{ balance += amount; }

 

546

Часть III • Прогроммирование с агрегирование!^^ т наследованиег^

void paylnterestO

 

 

// только для сберегательных счетов

{ balance += balance * rate / 365 / 100; }

 

 

 

}

 

 

 

 

 

 

 

int mainO

 

 

 

 

 

 

{

 

 

 

 

 

 

 

CheckingAccount a1(1000);

 

 

/ /

a1 -

расчетный счет

SavingsAccount

a2(1000);

 

 

/ /

a2 -

сберегательный счет

cout «

"Начальные балансы:

" « a1.getBal()

 

 

«

" " «

a2.getBal ()

«

endl;

/ /

нет проблем

a1.withdraw(100); a2.deposit(100);

/ /

теперь это синтаксическая ошибка!

/ / a1. paylnterestO;

 

 

 

 

 

a2.paylnterestO;

 

 

/ /

это нормально

cout «

"Итоговые балансы:

" «

a1.getBal()

 

 

 

«

" " «

a2. getBal ()

«

end1;

 

 

 

return

0;

 

 

 

 

 

 

}

 

 

 

 

 

 

 

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

a1. paylnterestO;

/ / синтаксическая ошибка: метод не найден

Единственная проблема состоит в том, что идеи разработчика не очень хорошо передаются сопровождаюш^ему приложение программисту. Здесь вы видите два класса, у которых много обш^его: элемент данных баланса, операции снятия со счета, размеш,ения вклада, доступ к данным. Однако сама архитектура программы не обозначает обидность классов. Разработчик классов знает, что у них обш.ие свойства, но в программе это никак не обозначается.

Классы имеют общие имена, но этого недостаточно для большой программы.

В листинге 13.3 оба класса помещаются на одной странице в исходном файле, но

вреальной жизни они могут разделяться. Если один класс изменяется, нет ника­ кой гарантии, что будет изменен и другой. При увеличении в программе числа объектов разных видов общие свойства этих классов не идентифицируются. Зна­ ния разработчика не выражаются в исходном коде.

Применение наследования С+ + для связывания родственных классов

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

Например, можно обобщить понятие накопительного и расчетного счета, введя понятие счета. Вместо соединения всех свойств и сохранения счетов в классе Account лучше включить в него только свойства, общие для обоих видов счетов. Подобные свойства — элемент данных balance, методы getBalO, withdrawO и depositO.

class Account {

/ / общие свойства базового класса

protected:

 

double balance;

 

public:

 

Account(double InitBalance

= 0)

{ balance = InitBalance; }

 

 

Глава 13 • Подобные классы и их интерпретация

547

double getBalO

 

 

 

 

{

return balance; }

 

/ /

общая для обоих счетов

 

void withdraw(double

amount)

/ /

общая для обоих счетов

 

{

i f (balance > amount)

 

 

 

 

( balance -= amount; }

 

 

 

void deposit(double

amount)

 

 

 

{

balance += amount;

}

 

 

 

} ;

Единственная разница между классами С4-4-, которые встречались ранее, и этим классом в том, что ключевое слово private заменено на ключевое слово protected. Это ключевое слово предотвращает доступ к компонентам класса извне подобно ключевому слову private, однако есть важное отличие: protected разре­ шает доступ наследникам данного класса.

В терминологии C++ класс, обобщающий свойства других классов и объеди­ няющий их oбш^^e характеристики, называется базовым классом. Он использу­ ется как базовый класс для дальнейшего наследования. Специализированные классы, добавляюндие новые свойства к общим свойствам, заданным в базовом классе, называются производными классами. В C++ термин ''производный'' означает "нacлeдyюш^^й". В Java используется другое понятие — расширение (extension).

Кроме того, базовые классы называют суперклассами или родительскими классами, а производный класс — дочерним. В контексте, когда базовый класс определяет тип данных, производный тип называется подтипом.

Производные классы добавляют и иногда заменяют свойства обобщенного ба­ зового класса. Дополнительные данные и методы в производном классе отражают связь между классами.

Например, классы CheckingAccount и SavingsAccount конструируются как от­ дельные специализации обобщенного класса Account. Они добавляют свойства, относяш^1еся к взиманию платы за операцию и выплаты процентов по вкладу, которых обобщенный класс Account не имеет.

Производный класс SavingsAccount добавляет к базовому классу Account эле­ мент данных rate и функцию-член paylnterest(). Он использует элемент данных balance и функции-члены getBalO, withdraw(), depositO базового класса, не заменяя ни на одно из этих свойств. Следующий фрагмент показывает, что нужно сделать для определения производного класса. Здесь не повторяются все свойства, наследуемые из базового класса. Описываются только те свойства, которые до­ бавляются к свойствам базового класса или заменяют их на собственную версию. (Синтаксис наследования описан в следующем разделе.)

class SavingsAccount

: public Account

{

/ /

производный класс

double rate;

 

 

 

 

 

public:

 

 

 

 

 

SavingsAccount(double

initBalance)

 

 

 

{ balance = initBalance; rate = 6.0;

}

/ /

сберегательный счет

void paylnterestO

 

 

 

/ /

не для расчетных счетов

{ balance += balance

*

rate /365 / 100;

} } ;

 

Производный класс CheckingAccount добавляет к классу Account элемент данных fee. Он использует элемент данных balance и функции-члены getBalO и depositO, заменяет функцию-член базового класса withdraw() на собственную функцию withdrawO, которая, в отличие от withdrawO базового класса, взимает плату за операцию по счету.

class CheckingAccount : public Account {

/ / производный класс

double fee;

 

Часть III # Програ^^г^шроеоние с агрешрованиег^ т наследование^^

ШШШШШШШШШШШ11^ШШШШШШШШЯШШШШШ1^1ШШШШШШШШШШШШШШШШШШШШШШШШШШШШШШШ

 

|| 11 liil

public:

 

 

 

 

 

CheckingAccount(double

initBalance)

 

 

{

balance = initBalance;

fee = 0.2

}

/ /

расчетный счет

void withdraw(double amount)

 

 

 

{

i f

(balance > amount)

 

 

 

 

 

 

balance = balance - amount -

fee; }

/ /

не для сберегательного счета

} ;

 

 

 

 

 

 

 

Таким образом, на этапе разработки применение наследования становится ин­

струментом для проектирования программы с учетом повторного использования

ее компонентов. В каждом производном классе можно определить общие свойства

класса Account,— в базовом классе. В результате архитектура программы стано­

вится более компактной (не нужно повторять общие свойства), а производитель­

ность разработчика повышается.

 

 

 

 

В данных примерах концепции счетов, служащих, инвентарных единиц пред­

ставляли скорее абстрактные, чем реальные объекты, которые должны моделиро­

ваться в приложении. Имейте в виду, что у вас есть расчетные и сберегательные

счета, болты и гайки, служащие на окладе и с почасовой оплатой.

 

Среди реальных объектов часто можно встретить "естественные" связи супер­

класс/подкласс, которые отражаются в связях между классами. Например, каждый

автомобиль — это транспортное средство, и ка>едый "Запорожец" — автомобиль.

Такую связь можно выразить с помощью наследования.

Наследование может быть прямым или косвенным. Транспортное средство —

прямой

суперкласс или базовый класс автомобиля. Автомобиль — прямой супер­

класс или базовый класс "Запорожца".

Вполне естественно, что класс (например, автомобиль) является производным классом одного класса (транспортное средство) и базовым другого ("Запорожец").

Кроме того, наследование можно использовать для дальнейшего развития программы. Если требуется реализовать более специализированные операции, в производном классе достаточно определить только то, что для этого необходимо. Все остальное предоставляет базовый класс.

Как и в случае любого распределения обязанностей между классами, наследо­ вание может использоваться как инструмент для разделения труда при разработке ПО. Один монолитный класс создает один программист, а базовый и производный классы — разные.

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

На время написания производных классов CheckingAccount и SavingsAccount код базового класса Account замораживается. Это мощная парадигма управления проектами. Если в будущем класс Account изменится, распространение изменений на все производные классы произойдет автоматически.

Еще одной популярной областью применения наследования является связыва­ ние методов на этапе выполнения. Это называют также привязкой этапа выполне­ ния, динамическим связыванием или полиморфизмом с виртуальными функциями. Многие считают, что объектно-ориентированное программирование заключается в использовании наследования и полиморфизма. Это не так.

Полиморфизм — особый случай объектно-ориентированного программирова­ ния, когда программа обрабатывает набор родственных объектов, выполняющих аналогичные, но не идентичные операции над различными видами объектов.

Глава 13 • Подобные классы и их интерпретация

549

Данные виды объектов настолько похожи, что их можно получать как производные из обидего базового класса (например, овал, прямоугольник, треугольник как производные от фигуры). Операции также похожи, поэтому в каждом классе для ее осуществления можно использовать одно имя (например, drawO).

Полиморфизм позволяет обрабатывать список объектов, отправлять одинако­ вое сообндение для каждого объекта независимо от того, к какому классу он при­ надлежит. В зависимости от класса, к которому относится тот или иной объект, вызывается та или иная функция, хотя формально вызов выглядит как вызов функции базового класса (виртуальная функция).

Синтаксис наследования в C+ +

 

Основной прием в использовании наследования в С+Н

двоеточие, за кото­

 

рым следует имя производного класса. Оно обозначает место имени базового

Итоговые

балансы

 

класса и описание режимов наследования — общедо­

1200

ступного (public), закрытого (private) или защищенного

объект Account:

(protected).

 

объект расчетного счета:

1099.8

Листинг 13.4. показывает программу из листинга 13.3,

объект сберегательного счета: 900.148

 

 

 

реализованную с применением наследования. Клиент

Рис. 13.3. Результат

программы

представляет собой расширение клиента из листинга 13.3.

Поэтому и результат программы (см. рис. 13.3) является

 

из листинга

13 А

расширением результата программы из листинга 13.3.

 

 

 

Л и с т и нг

13.4. Пример иерархии наследования для классов Account

 

#inclucle <iostream>

 

 

 

using

namespace std;

 

 

 

class

Account {

 

// базовый класс иерархии

 

protected:

 

 

 

double balance;

 

 

 

public:

 

 

 

Account(double initBalance = 0)

 

 

{ balance = initBalance; }

 

 

double getBalO

 

// общая для обоих счетов

 

{ return balance; }

 

 

void withdraw (double amount)

// общая для обоих счетов

 

{ if (balance > amount)

 

 

balance -=amount; }

 

 

void deposit(double amount)

 

 

{ balance +=amount; }

 

 

}

 

 

 

 

class CheckingAccount

: public Account {

// первый производный класс

double fee;

 

 

 

public:

 

 

 

CheckingAccount(double initBalance)

// расчетный счет

 

{ balance = initBalance; fee = 0.2; }

 

void withdraw(double amount)

 

 

{ if (balance > amount)

// безусловная плата

 

 

balance = balance - amount -fee;

 

Соседние файлы в предмете Программирование на C++