
Штерн В. - Основы C++. Методы программной инженерии - 2003
.pdf630 Часть III « Программирование с агрегированием и наследованием
if (!f) return 0; return 1; }
void File::saveCustomer(const char *nm, const char *ph,
{ |
|
|
int cnt, int*m) |
// в saveDataO |
|
f.setf(ios::left,ios:ladjustfield); |
f.width(NWIDTH); |
||||
|
f « nm; |
|
f.width(PWIDTH); |
||
|
f.setf(ios::right,ios::adjustfield); |
||||
|
f « |
ph « endl « |
cnt; |
|
// знает структуру файла |
|
for (int i=0; i < cnt; i++) |
|
|
||
|
{ f .width(6); f « |
m [i]; } |
|
|
|
|
f « |
endl; } |
|
|
|
void File::trim(char |
buffer[]) |
|
/ / в getltem(), getCustomer() |
||
{ |
for (int'j = strlen(buffer)-1; j>0; j - ) |
|
|||
|
i f |
(buffer[j]==' |
• ||buffer[j]=='\n') |
|
|
|
|
buffer[j] = ЛО'; |
|
|
|
|
else |
|
|
|
|
|
|
break; } |
|
|
|
|
|
Метод getltem() считывает одну строку данных из входного файла в локальный |
|||
|
|
массив buffer[], отбрасывает конечные пробелы и копирует данные в выходной |
|||
|
|
массив ttl[ ]. Затем он считывает данные из файла в другие компоненты данных |
|||
|
|
элемента (идентификационный номер, имеющееся в наличии количество, катего |
|||
|
|
рия). |
Окончательный вызов getlineO порождает вьщачу условия конца файла |
||
|
|
(end of file), если только что считанная строка является последней строкой физиче |
|||
|
|
ского файла. В этом случае файловый объект становится нулевым, а getltemO |
|||
|
|
возвращает нуль, чтобы указать конец входных данных для вызывающей програм |
|||
|
|
мы (класс Store). Иначе возвращается единица, показывающая, что еще имеются |
|||
|
|
данные для чтения. |
|
|
|
|
|
Метод saveltem() сохраняет данные элемента в физическом файле. Убедиться, |
|||
|
|
что категория целого типа правильно преобразуется в соответствующий символь |
|||
|
|
ный тип, можно с помош^»ю оператора switch. |
|||
|
|
Метод getCustomer() считывает имя клиента, отбрасывает конечные пробелы, |
|||
|
|
считывает номер телефона клиента и количество фильмов, взятых напрокат, |
|||
|
|
а затем идентификаторы взятых напрокат фильмов. |
|||
|
|
Метод saveCustomerO записывает в физический файл имя клиента, номер |
|||
|
|
телефона, счетчик фильмов и идентификаторы фильмов. |
|||
|
|
Метод trim() удаляет конечные пробелы из имени, потому что getlineO не |
|||
|
|
останавливается, обнаружив конец слова во входном файле. Иногда бывает нужно |
|||
|
|
указать количество считываемых символов либо признак конца (возврат каретки). |
|||
|
|
Строка, в которой удаляются конечные пробелы, передается как параметр. Метод |
|||
|
|
trim О не затрагивает другие элементы данных класса. Следовательно, функция |
|||
|
|
trimO должна быть объявлена статической. Метод trim(), выполняющий от |
|||
|
|
брасывание конечных пробелов, вызывается только из методов File: getltem() |
|||
|
|
и getCustomerO. Функция trimO должна объявляться закрытой. |
|||
|
|
В этом проекте классом |
верхнего уровня является класс Store. В листин |
ге 14.14 представлены его спецификации. Класс Store — сервер только одного программного компонента, глобальной функции main(), однако все равно рас смотрим условную компиляцию.
Этот файл не будет компилироваться без включения заголовочного файла "inventory, h", поскольку компилятор не будет знать, что означает имя Inventory. Однако можно скомпилировать его без заголовочного файла "file, h", поскольку имя класса File упоминается только в реализации функций-членов класса Store (см. листинг 14.15).
Глава 14 • Выбор между наследованием и ко1У1позицией |
[ 631 р |
Листинг 14.14. Спецификации класса для класса Store (файл store, h)
/ / file store.h
#ifndef STORE_H «define STORE_H
«include "inventory.h" «include "file.h"
class Store { public:
void loadData(Inventory &inv); int findCustomer(Inventory& inv); void processItem(Inventory& inv); void saveData(Inventory &inv);
} ;
#endif
|
|
|
Следовательно, вы можете включить заголовочный файл "file, h" в файл реа |
|||||
|
|
|
лизации, а не в заголовочный файл для класса Store. Компилятор не столкнется |
|||||
|
|
|
с трудностями при вычислении. Возможно, для пользователя это не очень хорошая |
|||||
|
|
|
адея. Лучше все серверные заголовочные файлы хранить в одном месте, в заго |
|||||
|
|
|
ловочном файле класса. Тогда программист, осуш,ествляюи;ий сопровождение, |
|||||
|
|
|
сможет сразу увидеть все серверные классы, используемые данным классом. |
|||||
|
|
|
Некоторые |
проектировш,ики |
включают заголовочные файлы для серверов |
|||
|
|
|
в серверы, например "item, h" и "customer, h". Правда, из-за этого создается не |
|||||
|
|
|
разбериха в клиентских заголовочных классах. |
|||||
|
|
|
Как показано в листинге 14.14, класс Store не содержит элементов данных. |
|||||
|
|
|
Это могло бы вызвать тревогу для класса в середине иерархии классов, но нор |
|||||
|
|
|
мально для клиентского класса верхнего уровня. Методы класса Store отвечают |
|||||
|
|
|
за операции верхнего уровня, которые описывают внешние интерфейсы системы: |
|||||
|
|
|
за загрузку базы данных в начале работы системы, поиск клиента в базе данных, |
|||||
|
|
|
обработку запросов напрокат фильмов клиентами и за сохранение базы данных |
|||||
|
|
|
после завершения программы. |
|
|
|||
|
|
|
В листинге 14.15 приведена реализация класса Store. Метод loadDataO созда |
|||||
|
|
|
ет локальный объект класса File и отправляет ему сообш,ения get Item () для счи |
|||||
|
|
|
тывания данных с внешнего файла. Каждый набор данных Item используется как |
|||||
|
|
|
аргумент в вызове appendltem(). Это сообш^ение отправляется объекту Inventory, |
|||||
|
|
|
а loadDataO получает его как параметр. Затем loadDataO создает другой локаль |
|||||
|
|
|
ный объект класса File, считывает клиентские данные из файла и сохраняет их |
|||||
|
|
|
в объекте Inventory. Локальный объект класса File исчезает, когда завершается |
|||||
|
|
|
loadDataO. При этом разрывается связь между физическими файлами "Item, dat" |
|||||
|
|
|
и "Cust.dat" и объектами File. |
|
|
|||
Листинг 14.15. Реализация класса Store |
(файл store, срр) |
|
||||||
/ / |
f i l e |
store.срр |
|
|
|
|
|
|
#include |
<iostream> |
|
|
|
|
|
||
using namespace std; |
|
|
|
|
|
|||
#include |
"store.h" |
|
|
|
/ / |
это необходимость |
||
void Store::loadData(Inventory |
&inv) |
|
|
|
||||
{ |
File |
itemsInC'Item.dat", ios: :in); |
|
/ / |
компонент базы данных |
|||
|
char |
t t l [ 2 7 ] , category; int id, |
qty, |
type; |
/ / |
компонент данных |
||
|
cout |
« |
"Loading database ... |
" |
« endl; |
|
|
634 |
Часть !i! # Прогр01^мирование с arpi |
|
|
|
||||||
|
Это понятно. Все становится менее определенными при переходе к клиентским |
|||||||||
|
классам на вершине иерархии классов. Класс Store не имеет каких-либо интуи |
|||||||||
|
тивно |
понятных обязанностей. Разделение |
обязанностей между классом Store |
|||||||
|
и main О совершенно произвольное. Некоторые проектировш,ики чувствуют, что |
|||||||||
|
main О должен |
создать экземпляр |
приложения для |
начального объекта. После |
||||||
|
вызова этого конструктора будут осуш^ествляться остальные действия. |
|||||||||
|
При таком подходе содержимое main() должно передаваться конструктору |
|||||||||
|
Store. Объект Store не нужен в конструкторе, поскольку функции-члены Store |
|||||||||
|
доступны в конструкторе немедленно, без целевого объекта. Поскольку функции- |
|||||||||
|
члены Store вызываются только из конструктора Store, они не должны быть |
|||||||||
|
обш,едоступными (public), они могут быть объявлены как закрытые. |
|||||||||
|
Class |
Store |
{ |
|
|
|
|
|
|
|
|
private: |
|
|
|
|
|
|
|
|
|
|
void |
loadData(Inventory &inv); |
|
|
|
|
||||
|
int |
findCustomer(Inventory& |
inv); |
|
|
|
||||
|
void |
processItem(Inventory& |
inv); |
|
|
|
||||
|
void |
saveData(Inventory &inv); |
|
|
|
|
||||
|
public: |
|
|
|
|
|
|
|
|
|
|
Store(void) |
|
|
|
|
|
|
|
||
|
{ |
Inventory inv; |
|
|
|
|
/ / |
определение объектов |
||
|
|
loadData(inv); |
|
|
|
|
/ / |
загрузка данных |
||
|
|
while (true) |
|
|
|
|
|
|
||
|
|
{ |
int |
result |
= findCustomer(inv) |
; |
/ / |
проверка результатов |
||
|
|
|
i f |
(result |
== 0) break; |
|
/ / |
завершение программы |
||
|
|
|
i f |
(result |
== 2) |
|
|
|
|
/ / 1 , если не найден |
|
|
|
|
processltem(inv); |
} |
|
/ / |
обработка кассеты |
||
|
|
saveData(inv); |
} |
|
|
|
/ / |
сохранение базы данных |
Функция mainO становится совсем простой.
int mainO
{Store store; return 0; }
Как уже упоминалось ранее, разделение обязанностей между начальными классами в иерархии классов и функцией main() является произвольным и не может быть спроектировано из анализа функциональных возможностей системы.
Видимость класса и разделение обязанностей
Рассмотрим связи классов.
В первой части книги обсуждалась идея разделения обязанностей между функ циями, чтобы избежать чрезмерного взаимодействия между ними (и усиленного сотрудничества разработчиков). Избыточное обидение часто происходит в резуль тате разделения на части того, что должно составлять одно целое.
Здесь мы поговорим о разделении обязанностей между классами, чтобы избе жать избыточного обмена сообш^ениями между классами и чрезмерного обш^ения разработчиков, отвечаюш,их за различные классы.
Избыточный обмен сообидениями между классами часто происходит в резуль тате разделения на части целого, например, при разделении обязанностей между различными функциями и различными классами, так что они должны осуш,ествлять связь через параметры функций и элементы данных класса. Чем обширнее передача сообш,ений между классами, тем больше подробностей должны помнить проектировш.ики. Повышается вероятность возникновения ошибок.
I |
.^^, |
^^ |
Част 1!1 ^ Програ1У11^ирован14е с агрешрованиег^ ш наследованием |
636 |
I |
||
|
|
|
обеспечивает клиентскую программу компонентами Customer, но не объектом |
|
|
|
Customer. Именно поэтому класс File видит класс Item, а не класс Customer. |
|
|
|
Видимость одного класса в другом классе этой же программы является важной |
|
|
|
характеристикой, которую проектировщики могут использовать для уменьшения |
|
|
|
до минимума зависимостей классов и координации проектировщиков. |
|
|
|
Когда объект в клиентском методе определяется как локальный объект, он ви |
|
|
|
ден. Размер координации минимальный. Примером является класс File, объекты |
|
|
|
которого определяются только в методах loadDataO и saveDataO класса Store |
|
|
|
и не видны в других классах или в других методах класса Store. |
|
|
|
Когда объект определяется как элемент данных в клиентском классе, его ви |
|
|
|
дят все методы клиентского класса. Это более сильная степень зависимости — |
|
|
|
клиентские методы должны координировать использование серверных объектов. |
|
|
|
Примером является класс Item и класс Customer. Их объекты задаются как эле |
|
|
|
менты данных класса Inventory и индексов custldx и itemldx, которые обозначают |
|
|
|
эти объекты. Все методы класса Inventory имеют доступ к этим двум массивам |
|
|
|
и к индексам. |
|
|
|
Рассмотрим, например, листинг 14.11, где представлена реализация класса |
|
|
|
Inventory. Метод getCustomerO, который вызывается из метода findCustomerO |
|
|
|
класса Store, устанавливает индекс custldx, обозначающий объект Customer. Он |
|
|
|
будет участвовать в операциях регистрации выдачи напрокат и возврата фильма. |
|
|
|
Методы checkout О и checkInO осуществляют доступ к одному и тому же объекту |
|
|
|
и используют ту же переменную индекса custldx. Однако они должны вычитать 1 |
|
|
|
для получения правильного объекта. Это пример связи, создаваемой посредством |
|
|
|
доступа к одному и тому же вычислительному объекту из различных методов. |
|
|
|
Когда клиентский объект определяется в методе собственного клиента, его |
|
|
|
сервер можно отправить его методам как параметр. Например, на рис. 14.13 по |
|
|
|
казано, что класс Store является клиентом класса Inventory. Клиентский объект |
|
|
|
(Store) определяется как локальная переменная его клиента (функция main()), |
аобъект-сервер (Inventory) посылается методам как параметр.
Влистинге 14.16 представлена реализация этой связи. Функция main() явля ется клиентом обоих классов Inventory и Store. Она определяет объекты Inventory и Store и посылает объект Inventory методам Store как аргумент. Проектировщи ки main О и Store должны знать о классе Inventory.
Это хорошо знакомый вопрос о намеренном сокрытии информации (о деталях реализации) от пользователя, который можно обсудить с позиции видимости объекта. Если объект Inventory определяется как элемент данных класса Store, а не как переменная в main(), то речь идет о методах класса Store, которые имеют доступ к этому объекту.
class Store {
Inventory inv; public:
void loadDataO; int findCustomerO; void processItemO; void saveDataO;
Принудительная передача обязанностей серверным классам
Принудительная передача обязанностей серверным классам является хорошим способом рационализации программы в клиентских методах и исключения деталей обработки нижнего уровня, которые затрудняют чтение клиентской программы и не позволяют быстро понять смысл обработки.
Глава 14 • Выбор между наследованием и композицией |
Г |
637 1 |
Например, в листинге 14.6 класс Item предусматривал методы |
getld() |
и getQuantO. Это общие методы, предоставляющие действительный идентифи катор элемента и количество элементов. Вследствие такой общности подобный проект отвечает любым требованиям, предусматривающим использование этих данных.
Это хорошо в библиотечном классе, который желательно продать как можно большему количеству возможных клиентов. Но хуже в той части программы, которую требуется спроектировать для удовлетворения конкретных запросов, полученных от клиентских классов, принадлежащих к этой же программе или к ее
. следующей версии. С общей структурой "библиотечного типа" клиентские классы должны быть гибкими, чтобы использовать сервисы, которые обеспечивают сер верные классы. Обычно клиентские классы получают от серверов намного больше информации, чем необходимо в действительности. Она должна отвечать текущим потребностям клиента.
В листинге 14.11 клиентская функция printRentalO просматривает каждый объект Item в классе Inventory и возвращает значение идентификатора объекта Item. Теперь функция printRentalO может делать с этим значением все, что ей угодно, но ей требуется только сравнить его со значением параметра.
void Inventory::printRental(int id) |
/ / используется в findCustomer() |
|
{ for |
(itemldx = 0; itemldx < itemCount; |
itemldx++) |
{ i f |
(itemList[itemIdx].getId() == id) |
|
|
{ itemList[itemIdx]. printltemO; break; } } |
|
itemldx = 0;} |
|
Эта информация избыточна, поскольку клиентской программе требуется знать только, является ли идентификатор в следующем объекте Item тем же, что и зна чение параметра. Клиентская программа получает больше информации, чем ей необходимо (значение идентификатора), но она должна много работать с этой информацией. При разделении обязанностей клиентская программа должна полу чить значение параметра серверной функции, тогда серверная программа сможет выполнить работу от имени клиента (сравнить идентификаторы). Клиентская программа будет иметь следующий вид:
void Inventory::printRental(int id) |
/ / |
используется в findCustomer() |
|
{ for |
(itemldx = 0; itemldx < itemCount; |
itemldx++) |
|
{ i f |
(itemList[itemIdx].sameld(id)) |
/ / |
важное отличие |
{ itemList[itemIdx]. printltemO; break; } } itemldx = 0;}
Клиентская функция checkout() в листинге 14.10 вызывает серверную функ цию getQuantoO, чтобы решить, можно ли выдать напрокат данный фильм. Теперь клиентская функция может делатьс этим значением, что требуется, но она просто сравнивает его снулем.
int Inventory::checkOut(int id) // используется в processItem() { for (itemldx = 0; itemldx < itemCount; itemldx++)
if (itemList[itemIdx].getId() == id) break;
if (itemldx == itemCount) |
|
{ itemldx = custldx = 0; return 0; } |
// какое значение? |
if (itemList[itemIdx].getQuant()==0) |
|
{ itemldx = custldx = 0; return 1; } |
|
itemList[itemIdx].incrQty(-1); |
|
custList[custIdx - 1].addMovie(id); |
|
itemldx = custldx = 0; |
|
return 2; } |
|
638 |
Часть III * Программирование с агрегированием и наследованием |
Снова эта информация является избыточной, поскольку клиентской программе нужно знать только, имеется ли нужный компонент. Клиентская программа полу чает больше информации, чем необходимо, но она должна много работать с ней. При разделении обязанностей серверную функцию надо сравнить с нулем, так что клиентская программа даже не будет знать об используемых правилах проверки доступности компонента. Чтобы избежать передачи информации для обработки от серверной к клиентской программе, сервер может предоставить функцию inStockO. Клиентская программа будет выглядеть так:
i nt |
Inventory::checkOut(int |
id) |
|
|
/ / |
используется в processItemO |
||
{ for (itemldx = 0; itemldx |
< itemCount; |
itemldx++) |
|
|||||
|
i f |
(itemList[itemIdx].sameId(id)) |
break; |
|
|
|||
i f |
(itemldx == itemCount) |
|
|
|
|
|
|
|
{ |
itemldx = custldx =0; return |
0; |
} |
|
|
|
||
i f |
(itemList[itemIdx].inStock)) |
|
/ / |
значение очевидно |
|
|||
{ |
itemldx = custldx = 0; return |
1; |
} |
|
|
|
||
itemList[itemIdx].incrQty(-l); |
|
|
/ / |
задание передается |
серверу |
|||
custList[custIdx-l].addMovie(id); |
|
/ / |
задание передается |
серверу |
||||
itemldx |
= custldx = 0; |
|
|
|
|
|
|
|
return |
2; } |
|
|
|
|
|
|
Обратите внимание, что функция checkOutO может сохранить значение коли чества фильмов, имеющихся в наличии, проверить, больше ли оно нуля, умень шить его на 1 и сохранить новое значение количества в объекте Item. Это другой пример передачи обязанностей KjmcHTCKoft программе. Вместо этого функция checkOutO говорит объекту компонента: "Мне неизвестно, сколько здесь компо нентов и не стоит беспокоиться о точном числе, поскольку я знаю, что в наличии имеются фильмы для выдачи напрокат". Это хороший пример передачи обязан ностей от клиентского класса серверному классу.
Использование наследования
На диаграмме UML (см. рис. 14.13) наследование не используется, поскольку это скорее реализация метода, чем модель связи между объектами реального мира.
Наследование применяется для упрощ.ения проекта серверных классов, про граммы клиентских классов и для уменьшения количества обш,ей информации для классов в приложении.
Например, учебный пример в листингах 14.6—14.16 реализует некоторый вид идиосинкратического поведения компонентов Inventory. Во входном файле вид фильма обозначается буквой, например "f". Это же происходит и в выходном файле. В отображенном компоненте вид фильма указывается словом, например "художественный" (feature). В памяти во время выполнения он обозначается целым числом, например 1.
Это обычные требования. Убедитесь в том, что клиенты серверного класса защиш,ены от подобных действий. Проект в листингах 14.6—14.16 не очень хорошо отвечает этим требованиям. Класс Item знает об этом в своем методе print Item (), он решает, какое слово отобразить. Так же поступает клиент Item — File: в своем методе saveltem() принимает решение, какую букву записывать в выходной файл. Аналогично действует класс Store в своем методе loadData(): Store проверяет, какое целое значение нужно сохранить в памяти компонента для последуюш^его использования. И только класс Inventory не затрагивается в данном вопросе, поскольку по ошибке забыли включить проверку того, что он делает.
Если проектировш,ик не пытается сохранить обшую информацию о классах, она распределяется вокруг программы.