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

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

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

630 Часть 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;

 

 

// скачивание данных
// файл компонента
// отсутствует внутренняя структура
// сохранение каждого компонента
// выходной файл клиента
// обратная связь в checkInO
// исключение CR из строки
// нет в запасе
// анализ возвращенного значения
// не найден
// поиск атрибута
// код успешного выполнения
// печать идентификатора фильма
// вывод на печать данных
// продолжение, если не найден
0)
// поиск номера телефона // останов, если телефон найден
// выход при отсутствии ввода данных

632

Часть 11! • Програм1У1ирование с агрегирование!^ и иасАВАОваниет

while

(itemsln.getltem(ttl,id,qty,category)

1)

{ switch

(category) {

 

case

' f' : type = 1; break;

 

case

'c' : type = 2; break;

 

case

'h' : type = 3; break; }

 

inv. appenclltem(ttl, id,qty, type); }

 

File custlnC'Cust.dat", ios: :in);

 

char name[25], phone[15]; int movies[10], count;

while

(custIn.getCustomer(name,phone,count,movies)

{ inv.appendCust(name,phone,count,movies); }

}

//считывание

//определение категории для подтипа

//база данных клиента

//скачивание данных

int Store::findCustomer(Inventory&

inv)

 

{

char buffer[200]; char name[25], phone[13];

 

int count, movies[10];

 

 

 

 

 

cout «

"Enter customer phone (or press Return to quIt)

 

cin.getline(buffer,15) ;

 

 

 

 

if (strcmp(buffer,"") == 0) return 0;

 

 

bool found = false;

 

 

 

 

 

while

(inv.getCustomer(name,phone,count,movies) !

 

{ if (strcmp(buffer,phone) == 0)

 

 

 

{ found = true; break;

}

}

 

 

if (!found)

 

 

 

 

 

 

{ cout «

"\nCustomer isnot found" « endl;

 

return

1; }

 

 

 

 

 

 

cout.setf(ios::left,ios::adjustfield);

endl;

 

cout.width(22); cout «

name «

phone «

 

for (int j = 0; j < count; j++)

 

 

 

{ inv.printRental(movies[j]);}

 

 

cout «

endl;

 

 

 

 

 

 

return 2; }

 

 

 

 

 

void store: :processItem(Inventory& inv)

 

 

{ int cmd,

result,

id;

 

 

 

 

 

cout «

" Enter movie id: ";

 

 

 

cin »

id;

 

 

 

 

 

 

cout «

" Enter 1 tocheck out, 2 tocheck in: ";

 

cin »

cmd;

 

 

 

 

 

 

if (cmd == 1)

 

 

 

 

 

 

{

 

result = inv.checkOut(id);

 

 

 

if (result == 0)

 

 

 

endl;

 

 

 

 

cout «

"Movie isnot found " «

 

 

else if (result == 1)

 

 

 

 

 

 

cout «

"Movie isout of stock" « endl

 

 

else

 

 

 

 

// успешно

 

 

 

 

cout «

" Renting isconfirmed\n"; }

 

else if (cmd == 2)

 

 

 

 

 

inv.checkln(id);

 

 

 

 

 

cin.getO; }

 

 

 

 

 

void Store::saveData(Inventory

&inv)

 

{

File itemsOut("Item.out",ios

::out); Item item;

 

while

(inv.getltem(item))

 

 

 

 

itemsOut.saveltem(item);

 

 

 

 

File custOut ("Cust. out",ios::out) ;

 

 

char name[25], phone[13]; int count, movies[10];

 

cout «

"Saving database

... " «

endl;

 

while(inv. getCustomer(name,phone,count,movies)) custOut.saveCustomer(name,phone,count,movies);)

Глава 14 • Выбор между наслвАОвонием и композицией

633

Метод findCustomerO запрашивает номер телефона клиента и завершается (возвращая нуль), если оператор нажал клавишу Enter без ввода каких-либо дан­ ных. Если вводится номер телефона, то f inclCustomer() извлекает данные клиента, отправляет сообщение getCustofner() объекту Inventory, который пересылает его findCustomerO как аргумент. Если номер телефона не находится, выводится со­ общение об ошибке и findCustomerO возвращает 1 для уведомления клиента. В противном случае выводятся имя клиента, номер телефона и данные о фильме,

аметод возвращает 2.

Вметоде processItemO также имеется параметр типа Inventory. Метод за­ прашивает у оператора ввод идентификатора фильма и команду (для регистрации выдачи или возврата), а затем отправляет либо сообщение checkOutO, либо checkInO его параметру. Когда возвращается checkout(), processItem() анализи­ рует возвращенное значение и выводит на печать следующее сообщение. Когда возвращается checkInO, processItemO просто завершается, потому что checkInO анализирует результаты операции и выводит на печать сообщения оператору.

Метод saveData() точно повторяет действия loadData(). Он создает локальные объекты класса File и отправляет сообщения saveltemO и saveCustomerO с ин­ формацией, которая была извлечена saveDataO из параметра Inventory. Исполь­ зуются сообщения getltemO и getCustomer().

Листинг 14.16. Реализация функции main() Store (файл video, срр)

/ /

f i l e

video.срр

 

 

 

 

#include

<iostream>

 

 

 

using namespace std;

 

/ /

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

#include

"store.h"

 

int

mainO

 

 

 

/ /

определение объектов

{

Inventory

inv;

Store

store;

store.loadData(inv)

;

/ /

загрузка данных

while(true)

 

 

/ /

проверка результатов

 

{ int result

= store. findCustomer(inv);

 

i f

(result

== 0)

break;

/ /

завершение программы

 

 

i f

(result == 2)

/ /

1, если не найден

 

 

 

store.processltem(inv); }

/ /

обработка кассеты

store.saveData(inv);

 

/ /

сохранение базы данных

return

0;

 

 

 

 

 

}

Последним компонентом программы являет­ ся клиент Store функции main(), реализующей два объекта (см. листинг 14.16), один из класса Inventory, а другой из класса Store. Последний отправляет сообщения объекту Store и переда­ ет объект Inventory как аргумент.

На рис. 14.12 показан пример выполнения программы. Он соответствует входным файлам на рис. 14.8 и 14.9. Выходные файлы, сгенери­ рованные при выполнении программы, пред­ ставлены на рис. 14.10 и 14.11.

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

1 Loading

database ...

 

 

1

Enter customer phone (or press Return toquit) 353-2566

 

1

Shtern

 

353-2566

 

1

101

Splash

 

comedy

 

1

102

Birds

 

horror

 

1

Enter movie

Id: 101

 

|

1

Enter

1 tocheck out, 2 tocheck in: 2

1

Movie

is returned

 

 

Enter customer phone (or press Return toquit) 353-2566

 

1

Shtern

 

353-2566

1

 

102

Birds

 

horror

 

Enter movie

id: 103

 

|

 

Enter 1 tocheck out, 2 tocheck in: 1

 

Renting isconfirmed

 

1

 

Enter customer

phone (or press Return toquit)

 

Saving database ...

 

 

Рис. 14.12. Примеры

выполнения

 

 

 

 

 

программы,

приведенной

 

 

 

 

 

в листингах

14.6-14.16

 

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() является произвольным и не может быть спроектировано из анализа функциональных возможностей системы.

Видимость класса и разделение обязанностей

Рассмотрим связи классов.

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

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

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

Глава 14 • Выбор между наследованием и композицией

| 635 |

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

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

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

Использование диаграмм классов UML (см. рис. 14.4) — хороший метод ана­ лиза структуры программы. Связи класса на диаграмме показывают, каким кли­ ентским классам известно о конкретном серверном классе. К сожалению, связи классов на диаграммах UML не отображают разделение обязанностей между классами, передачу обязанностей к серверам и разделение на части того, что должно составлять одно целое. Для этого необходимо проанализировать распре­ деление элементов данных и функций-членов по классам. Диаграммы классов (см. рис. 14.3) более полезны.

Видимость класса и связи классов

* >>^

File

Рис. 1 4 . 1 3 .

 

 

На рис. 14.13 показаны связи межлу классами, опи­

O I U I C

 

санными в учебном примере в листингах 14.6—14.16.

 

 

Диаграмма классов UML позволяет понять, что класс

 

 

Inventory "владеет" произвольным количеством объектов

Inventory

классов Item и Customer. В проекте класса Inventory были

•т

ограничены размеры массива, но они были произвольны-

Y

 

ми и имели отношение к концептуальным связям между

 

 

классом Inventory и содержащимися в нем объектами.

 

 

С концептуальной точки зрения класс Inventory может

Item

Customer

содержать неограниченное число объектов Item и Customer

(см. диаграмму классов на рис. 14.13).

 

 

Диаграмма

классов для

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

программ,

приведенных

класс Store является клиентом классов Inventory и File

в листингах

14.6-14.16

и что main О представляет собой клиент класса Store

и класса Inventory. Она также показывает, что класс File является клиентом класса Item, но не класса Customer.

Возникает противоречие, о котором говорилось при проектировании классов Item и Customer. Объекты класса Item знают, как вывести себя на печать. Объек­ ты же класса Customer с этим не знакомы. Именно поэтому класс Customer пре­ доставляет метод getCustomerO, который используется клиентской программой для извлечения элемента данных Customer для вывода на печать.

Далее противоречие поддерживается структурой класса Inventory. Его метод getltem() обеспечивает клиентскую программу объектом Item. Программа осуще­ ствляет доступ к компонентам объекта Item. Метод getCustomerO класса Inventory

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 не затрагивается в данном вопросе, поскольку по ошибке забыли включить проверку того, что он делает.

Если проектировш,ик не пытается сохранить обшую информацию о классах, она распределяется вокруг программы.

Рис. 14.14. Диаграмма классов для программ в листингах 14.6-14.16

Глава 14 » Выбор между наследованием и композицией

Наследование — это хороший механизм для сохранения информации в сервер­ ных классах. Если класс Item станет базовым классом для набора производных классов, например Featureltem, Comedyltem и Horrorltem, вы можете сохранить информацию о поведении компонента в этих классах и предотвратить ее распол­ зание по программе.

В данной главе это решение не реализовано, поскольку для него требуется ис­ пользование полиморфизма.

Другой вопрос, связанный с использованием наследования в учебном примере в листингах 14.6—14.16, заключается в проектировании класса File. В данной программе объекты класса File используются для четырех целей: чтения данных компонента (фильма), чтения данных клиента, записи данных компонента и записи клиентских данных. Каждый объект подходит только для одной цели. Например, объект itemsOut класса File в методе saveDataO в листинге 14.15 может исполь­ зоваться только для записи данных компонента. Если программист клиентской части попытается отправить сообш^ение getCustomer() класса File этому объекту, компилятор примет вызов данной функции. Но во время выполнения программа будет прекраш,ена, потому что физический файл открыт для записи.

Обратите внимание, что если программист клиентской части использует этот объект класса File для приема сообш,ения saveCustomer(), то компилятор не толь­ ко примет эту программу, но выполняемая система не выдаст возражений. Невер­ ные данные будут записаны в выходной файл.

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

class FileOutltem

: public

F i l e

{ p u b l i c :

 

 

FileOutItem(const char

name[]);

void saveltem(

const Item &item);

File

TV

FileOutltem

Filelnltem

FileOutCust

FilelnCust

Featureltem

При такой структуре попытка клиентской программы отправить объекту FileOutltem сообндение getltem() или saveCustomerO будет интерпретирована компилятором как синтаксическая ошибка. Это очень хорошо. Альтернатива в том, чтобы ликвидировать в программе большое ко­

Store

main()

личество маленьких классов, и это может серь­

 

 

езно усложнить сопровождение.

 

 

Некоторые программисты утверждают, что

 

Inventory

если объект File открывается для записи дан­

 

ных компонента, он обладает ограниченными

 

 

 

 

возможностями и пытается считывать данные

 

 

из этого файла или записывать их. Подобные

 

 

ошибки не должны возникать. Но они появля­

 

 

ются. Под давлением каких-либо обстоятельств

 

 

многие программисты становятся менее внима­

 

1

тельными. Совсем неразумно отрицать реаль­

 

ность и настаивать, что если программист

Item

Customer

квалифицирован и внимателен, то ошибок не

7^

 

будет. Советуем вам избегать ситуаций, кото­

1

рые способствуют появлению ошибок.

Comedyltem

На рис. 14.14 представлена диаграмма UML

Hon^orltem

для учебного примера. Классы Item и File ис­

 

 

пользуются здесь как базовые классы для спе­

 

 

циализированных производных классов.

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