
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdfГлава 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; |
|
/ / |
поле тега для вида объекта |
Глава 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; } |
|
Часть 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; |
|