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

PrIS

.pdf
Скачиваний:
45
Добавлен:
07.12.2018
Размер:
7.24 Mб
Скачать

271

Об'єкт – це щось, що має чіткі певні межі, але цього недостатньо, щоб відокремити один об'єкт від іншого або дати оцінку якості абстракції. На основі наявного досвіду можна дати таке визначення:

об'єкт володіє станом, поведінкою та ідентичністю; структура й поведінка подібних об'єктів визначає загальний для них клас; терміни "екземпляр класу" і "об'єкт" взаємозамінні.

15.1.2. Стан

Семантика. Розглянемо торговельний автомат, що продає напої. Поведінка такого об'єкту полягає в тому, що після опускання в нього монети й натискання кнопки автомат видає обраний напій. Що відбудеться, якщо спочатку буде натиснута кнопка вибору напою, а потім вже опущена монета? Більшість автоматів при цьому просто нічого не зроблять, тому що користувач порушив їхні основні правила. Інакше кажучи, автомат відігравав роль (очікування монети), яку користувач іґнорував, натиснувши спочатку кнопку. Або припустимо, що користувач автомата не звернув увагу на попереджувальний сигнал "Киньте стільки монет, скільки вартує напій" і опустив в автомат зайву монету. У більшості випадків автомати не дружні до користувача й радісно заковтують всі гроші.

У кожній з таких ситуацій ми бачимо, що поведінка об'єкту визначається його історією: важлива послідовність вчинених над об'єктом дій. Така залежність поведінки від подій і від години пояснюється тим, що в об'єкту є внутрішній стан. Для торгового автомата, наприклад, стан визначається сумою грошей опущених до натискання кнопки вибору. Інша важлива інформація - це набір монет, які сприймає автомат і запас напоїв.

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

стан об'єкту характеризується переліком (зазвичай статичним) всіх властивостей цього об'єкту й поточними (зазвичай динамічними) значеннями кожної із цих властивостей.

Однією з властивостей торговельного автомата є здатність приймати монети. Це статична (фіксована) властивість у тому розумінні, що вона – істотна характеристика торговельного автомата. З іншого боку, цій властивості відповідає динамічне значення, що характеризує кількість прийнятих монет. Сума збільшується в міру опускання монет в автомат і зменшується, коли продавець забирає гроші з автомата. У деяких випадках значення властивостей об'єкту можуть бути статичними (наприклад, заводський номер автомата), тому в цьому означенні використаний термін "зазвичай динамічні".

272

До властивостей об'єкту відносяться властиві йому або набуті ним характеристики, риси чи можливості, що роблять цей об'єкт самим собою. Наприклад, для ліфта характерним є те, що він сконструйований для переміщення вверх і вниз, а не горизонтально. Перелік властивостей об'єкту є, як правило, статичним, оскільки ці властивості становлять незмінну основу об'єкту. Ми говоримо "як правило", тому що в ряді випадків склад властивостей об'єкту може змінюватися. Прикладом може служити робот з можливістю самонавчання. Робот спочатку може розглядати деяку перешкоду як статичну, а потім виявляє, що це двері, які можна відкрити. У такій ситуації в міру одержання нових знань змінюється створювана роботом концептуальна модель світу.

Усі властивості мають деякі значення. Ці значення можуть бути простими кількісними характеристиками або мати посилання на інший об'єкт. Так стан ліфта може описуватися числом 3, що означає номер поверху, на якому ліфт перебуває у цей момент. Стан торговельного автомата описується в термінах інших об'єктів, наприклад, товару, що є в наявності. Конкретні напої – це самостійні об'єкти, відмінні від торговельного автомата (їх можна пити, а автоматні, і робити з ними інші дії).

Отже, ми встановили відмінність між об'єктами й простими величинами: прості кількісні характеристики (наприклад, число 3) є "постійні й незмінні", тоді як об'єкти існують у часі, змінюються, мають внутрішній стан, можуть створюватися, знищуватися й розділятися.

Той факт, що кожен об'єкт має стан, означає, що кожен об'єкт займає певний простір (фізично або в пам'яті комп'ютера).

Приклади. Припустимо, що мовою C++ нам треба створити реєстраційні записи про співробітників. Можна зробити це в такий спосіб:

struct PersonnelRecord { char name[100];

int socialSecurityNumber; char department[10]; float salary;

};

Кожний компонент у наведеній структурі позначає конкретну властивість нашої абстракції реєстраційного запису. Опис визначає не об'єкт, а клас, оскільки він не вводить який-небудь конкретний екземпляр. Щоб створити об'єкти цього класу, необхідно написати таке:

PersonnelRecord deb, dave, karen, jim, torn, denise, kaitlyn, krista, elyse;

У цьому випадку оголошено дев'ять різних об'єктів, кожний з яких займає певне місце в пам'яті. Хоча властивості цих об'єктів є загальними (їхній стан відображається одноманітно), у пам'яті об'єкти не

273

перетинаються й займають кожний своє місце. На практиці прийнято обмежувати доступ до стану об'єкту, а не робити його загальнодоступним, як у попередньому визначенні класу. Із врахуванням сказаного, змінимо це визначення таким чином:

class PersonnelRecord { public:

char* employeeName() const;

int employeeSocialSecurityNumber() const; char* employeeDepartment() const; protected:

char name[100];

int socialSecurityNumber; char department[10]; float salary;

};

Нове визначення трохи складніше від попереднього, але ряду міркувань краще. Зокрема, у новому визначенні реалізація класу схована від інших об'єктів. Якщо реалізація класу буде надалі змінена, код доведеться перекомпілювати, але семантично клієнти не будуть залежати від цих змін (тобто їхній код збережеться). Крім того, вирішується також проблема займаної об'єктом пам'яті за рахунок явного визначення операцій, які дозволені клієнтам над об'єктами певного класу. Зокрема, ми даємо всім клієнтам право дізнатися ім'я, код соціального захисту й місце роботи співробітника, але тільки особливим клієнтам (підкласам цього класу) дозволено встановлювати значення зазначених параметрів. Тільки цим спеціальним клієнтам дозволений доступ до відомостей про заробітну плату. Інша перевага останнього визначення пов'язана з можливістю його повторного використання. Далі ми побачимо, що механізм успадкування дозволяє повторно використовувати абстракцію, а потім уточнити й багатьма способами спеціалізувати її.

Всі об'єкти в системі інкапсулюють деякий стан, і всі стани системи інкапсульовані в об'єкти. Однак, інкапсуляція стану об'єкту – це тільки початок, якого недостатньо, щоб ми могли охопити повний зміст абстракцій, які ми вводимо під час розроблення програмної системи. Із цієї причини нам треба зрозуміти, як об'єкти функціонують.

15.1.3. Поведінка

Що таке поведінка. Об'єкти не існують ізольовано, а піддаються впливу або самі впливають на інші об'єкти.

Поведінка – це те, як об'єкт діє й реагує; поведінка виражається в термінах стану об'єкту й передаванні повідомлень.

274

Іншими словами, поведінка об'єкту – це його спостережувана й перевірена ззовні дія.

Операцією називається певний вплив одного об'єкту на іншій з метою викликати відповідну реакцію. Наприклад, клієнт може активізувати операції append і pop для того, щоб керувати об'єктомчергою (додати або вилучити елемент). Існує також операція length, що дозволяє визначити розмір черги, але не може змінити це значення. У чисто об’єктно-орієнтованій мові, такій як Smalltalk, прийнято говорити про передавання повідомлень між об'єктами. У мовах типу C++, в яких чіткіше відчувається процедурне минуле, ми говоримо, що один об'єкт викликає функцію-член іншого. В основному поняття повідомлення збігається з поняттям операції над об'єктами, хоча механізм передавання різний. Для наших цілей ці два терміни можуть використовуватися як синоніми.

В об’єктно-орієнтованих мовах операції, що виконуються над певним об'єктом, називаються методами і належать до визначення класу об'єкту. У C++ вони називаються функціями-членами. Ми будемо використовувати ці терміни як синоніми.

Передавання повідомлень – це одна частина рівняння, що задає поведінку. З нашого означення випливає, що стан об'єкту також впливає на його поведінку. Розглянемо торговельний автомат. Ми можемо зробити вибір, але поведінка автомата буде залежати від його стану. Якщо ми не опустили в нього достатню суму, наймовірніше нічого не відбудеться. Якщо ж грошей досить, автомат видасть нам бажане (і тим самим змінить свій стан). Отже, поведінка об'єкту визначається виконуваними над ним операціями і його станом, причому деякі операції мають побічну дію: вони змінюють стан. Концепція побічної дії дозволяє уточнити наше визначення стану.

Стан об'єкту відображає сумарний результат його поведінки.

Найцікавіші ті об'єкти, стан яких не статичний: їхній стан змінюється й запитується операціями.

Приклади. Опишемо мовою C++ клас Queue (черга): class Queue {

public:

Queue(); Queue(const Queue&); virtual ~Queue();

virtual Queue& operator=(const Queue&); virtual int operator==(const Queue&) const; int operator!=(const Queue&) const; virtual void clear();

virtual void append(const void*);

275

virtual void pop();

virtual void remove(int at); virtual int length() const; virtual int isEmpty() const; virtual const void* front() const; virtual int location(const void*); protected:

...

};

У визначенні класу використовується звична для С ідіома посилання на дані невизначеного типу за допомогою void*, завдяки чому в чергу можна вставляти об'єкти різних класів. Ця техніка небезпечна – клієнт повинен розуміти, з яким (якого класу) об'єктом він має справу. Крім того, під час використання void* черга не "володіє" об'єктами, які до неї вміщені. Деструктор Queue() знищує чергу, але не її учасників. Далі ми розглянемо параметризовані типи, які допомагають справлятися з такими проблемами.

Оскільки визначення Queue задає клас, а не об'єкт, ми повинні оголосити екземпляри класу, з якими можуть працювати клієнти:

Queue a, b, c, d;

Ми можемо виконувати операції над об'єктами: a.append(&deb);

a.append(&karen); a.append (&denise); b = a;

a.pop();

Тепер черга а містить двох співробітників (першим стоїть karen), а черга b – трьох (першим стоїть deb). Отже, черги мають певний стан, який впливає на їхню майбутню поведінку, наприклад, одну чергу можна безпечно просунути (pop) ще два рази, а іншу – три.

Операції. Операція – це послуга, яку клас може надати своїм клієнтам. На практиці типовий клієнт здійснює над об'єктами операції п'яти видів. Нижче наведені три найпоширеніші операції:

Модифікатор

Операція, яка змінює стан об'єкту

Селектор

Операція, яка читає стан об'єкту, але не змінює

 

його

Ітератор

Операція, що дозволяє організувати доступ до всіх

 

частин об'єкту у строгій послідовності

Оскільки логіка цих операцій досить різна, корисно вибрати такий стиль програмування, що враховує ці розходження в коді програми. У нашій специфікації класу Queue ми спочатку перелічили всі

276

модифікатори (функції-члени без специфікаторів const clear, append, pop, remove), а потім всі селектори (функції зі специфікаторами const length, isEmpty, front і location).

Дві операції є універсальними; вони забезпечують інфраструктуру, необхідну для створення і знищення екземплярів класу:

Конструктор

Операція створення об'єкту і/або його ініціалізації

Деструктор

Операція, що звільняє стан об'єкту і/або руйнує сам

 

об'єкт

Умові C++ конструктор і деструктор становлять частину опису класу, тоді як в Smalltalk і CLOS ці оператори визначені у протоколі метакласу (тобто у класі класу).

Учисто об’єктно-орієнтованих мовах операції можуть бути тільки методами, тому що процедури й функції поза класами в цій мові визначати не можна. В інших мовах допускається описувати операції як незалежні від об'єктів підпрограми. У C++ вони називаються функціяминечленами; ми ж будемо тут називати їх вільними підпрограмами. Вільні підпрограми – це процедури і функції, які виконують роль операцій високого рівня над об'єктом або об'єктами одного або різних класів. Вільні процедури групуються відповідно до класів, для яких вони створюються. Це дає підставу називати такі пакети процедур утилітами класу. Наприклад, для визначеного вище класу Queue можна написати таку вільну процедуру:

void copyUntilFound(Queue& from, Queue& to, void* item)

{

while ((!from.isEmpty()) && (from.front() != item))

{

to.append(from.front());

from.pop();

}

}

Зміст полягає в тому, що вміст однієї черги переходить в іншу доти, поки на початку першої черги не виявиться заданий об'єкт. Це операція високого рівня; вона будується на операціях-примітивах класу Queue.

У C++ (і Smalltalk) прийнято збирати всі логічно зв'язані вільні підпрограми й повідомляти їх як частина деякого класу, що не має стану. Всі такі функції будуть статичними.

Отже, можна стверджувати, що всі методи – операції, але не всі операції – методи: деякі з них є вільними підпрограми. Ми схильні використовувати тільки методи, хоча, як буде показано далі, іноді важко втриматися від спокуси, особливо якщо операція за своєю природою

277

виконується над кількома об'єктами різних класів і немає жодних причин оголосити її операцією саме одного класу, а не іншого.

Ролі й відповідальності. Сукупність всіх методів і вільних процедур, що відносяться до конкретного об'єкту, утворює протокол цього об'єкту. Отже протокол визначає поведінку об'єкту, що охоплює всі його статичні й динамічні аспекти. У самих нетривіальних абстракціях корисно поділяти протокол на часткові аспекти поведінки, які ми будемо називати ролями. Роль – це маска, яку носить об'єкт; вона визначає контракт між абстракцією та її клієнтами.

Поєднуючи наші означення стану й поведінки об'єкту, введемо поняття відповідальності. Відповідальність об'єкту має два боки – знання, які об'єкт підтримує, і дії, які об'єкт може виконувати. Вони виражають зміст його призначення і місце в системі. Відповідальність розуміється як сукупність всіх послуг і всіх контрактних зобов'язань об'єкту. У такий спосіб можна сказати, що стан і поведінка об'єкту визначають ролі, а ті, своєю чергою, необхідні для виконання відповідальності цієї абстракції.

Дійсно більшість цікавих об'єктів виконують у своєму житті різні ролі, наприклад:

банківський рахунок може бути в хорошому або поганому стані (дві ролі), і від цієї ролі залежить, що відбудеться при спробі зняття з нього грошей;

для фондового брокера пакет акцій – це товар, який можна купувати або продавати, а для юриста це ознака володіння певними правами;

протягом дня одні і та ж сама особа може відігравати роль матері, лікаря, садівника й кінокритика.

Ролі банківського рахунку є динамічними й взаємовиключними. Ролі пакету акцій трохи перекриваються, але кожна з них залежить від того, що клієнт з ними робить. У випадку особи ролі динамічно змінюються щохвилини.

Об'єкти як автомати. Наявність внутрішнього стану об'єктів означає, що порядок виконання операцій має істотне значення. Це наводить на думку подати об'єкт як маленьку незалежну машину. Дійсно, для деяких об'єктів такий часовий порядок настільки істотний, що найкращим способом їхнього формального опису буде кінцевий автомат.

Продовжуючи аналогію з машинами, можна сказати, що об'єкти можуть бути активними і пасивними. Активний об'єкт має свій потік керування, а пасивний – не має. Активний об'єкт, у загальному випадку, автономний, тобто він може проявляти свою поведінку без впливу зі сторони інших об'єктів. Пасивний об'єкт, навпаки, може змінювати свій стан тільки під впливом інших об'єктів. Отже, активні об'єкти системи –

278

джерела керівних впливів. Якщо система має кілька потоків керування, то й активних об'єктів може бути кілька. У послідовних системах зазвичай у кожний момент часу існує тільки один активний об'єкт, наприклад, головне вікно, диспетчер, який опрацьовує всі повідомлення. У такому випадку інші об'єкти пасивні: їхня поведінка проявляється, коли до них звертається активний об'єкт. В інших видах послідовних архітектур (системи опрацювання транзакцій) немає явного центру активності, і керування розподілене серед пасивних об'єктів системи.

15.1.4. Ідентичність

Семантика.

Ідентичність – це така властивість об'єкту, що відрізняє його від всіх інших об'єктів.

У більшості мов програмування і керування базами даних для ідентифікації тимчасових об'єктів їх іменують, тим самим інтеґруючи адресованість та ідентичність. Більшість баз даних розрізняють постійні об'єкти за ключовим атрибутом, тим самим змішуючи ідентичність і значення даних. Джерелом помилок в об’єктно-орієнтованому програмуванні є невміння відрізняти ім'я об'єкту від самого об'єкту.

Приклади. Почнемо з визначення крапки на площині. struct Point {

int x; int y;

Point() : x(0), y(0) {}

Point(int xValue, int yValue) : x(xValue), y(yValue) {}

};

Ми визначили Point як структуру, а не як повноцінний клас. Правило, на підставі якого ми так здійснили, дуже просте. Якщо абстракція являє собою сукупність інших об'єктів без якої-небудь власної поведінки, ми робимо її структурою. Однак, коли наша абстракція визначає складнішу поведінку, ніж простий доступ до полів структури, то треба визначати клас. У цьому випадку абстракція Рoint – це просто пара координат (x,y). Для зручності передбачено два конструктори: один ініціалізує точку нульовими значеннями координат, а інший – деякими заданими значеннями.

Тепер визначимо екранний об'єкт (DisplayItem). Ця абстракція досить звична для систем із графічним інтерфейсом (GUI) – вона є базовим класом для всіх об'єктів, які можна відображати у вікні. Ми хочемо зробити її чимось значнішим, ніж просто сукупністю точок. Треба, щоб клієнти могли рисувати, вибирати об'єкти й переміщувати їх

279

по екрані, а також запитувати місце їх знаходження та стан. Ми записуємо нашу абстракцію у вигляді такого оголошення на C++:

class DisplayItem { public:

DisplayItem();

DisplayItem(const Point& location); virtual ~DisplayItem();

virtual void draw(); virtual void erase(); virtual void select(); virtual void unselect();

virtual void move(const Point& location); int isSelected() const;

Point location() const;

int isUnder(const Point& location) const; protected:

};

У цьому оголошенні ми навмисно опустили конструктори, а також оператори для копіювання, присвоєння й перевірки на рівність. Їх ми розглянемо нище.

Ми очікуємо, що в цього класу буде багато спадкоємців, тому деструктор і всі модифікатори оголошені віртуальними. Особливо це відноситься до draw. Селектори швидше за все не будуть перевизначатися у підкласах. Зверніть увагу, що один з них, isUnder, повинен обчислити, чи накриває цей об'єкт задану точку, а не просто повертати значення якоїсь властивості.

Оголосимо екземпляри вказаних класів:

DisplayItem item1;

DisplayItem* item2 = new DisplayItem(Point(75, 75));

DisplayItem* item3 = new DisplayItem(Point(100, 100));

DisplayItem* item4 = 0;

Рис. 15.1а показує, що при виконанні цих операторів виникають чотири імені й три різних об'єкти. Конкретно, у пам'яті будуть відведені чотири місця під імена item1, item2, item3, item4. При цьому item1 буде іменем об'єкту класу DisplayItem, а три інших будуть вказівниками. Крім того, лише item2 і item3 будуть насправді вказувати на об'єкти класу DisplayItem. В об'єктів, на які вказують item2 і item3, до того ж немає імен, хоча на них можна посилатися: наприклад, *item2. Тому ми можемо сказати, що item2 вказує на окремий об'єкт класу DisplayItem, на ім'я якого ми можемо опосередковано посилатися через *item2. Унікальна

280

ідентичність (але не обов'язкове ім'я) кожного об'єкту зберігається на весь час його існування, навіть якщо його внутрішній стан змінився. Ця ситуація нагадує парадокс Зенона про ріку: чи може ріка бути тією ж самою, якщо в ній щодня тече різна вода?

 

 

 

 

 

item2

 

item3

 

item4

a)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

item1:Point=(0,0)

 

 

 

 

:Point=(75,75)

 

 

 

 

:Point=(100,100)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

0

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

item2

 

item3

 

item4

б)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

item1:Point=(75,75)

 

 

 

:Point=(75,75)

 

 

 

 

:Point=(38,100)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

item2

 

item3

 

item4

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

в)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

item1:Point=(75,75)

 

 

 

:Point=(75,75)

 

 

 

 

:Point=(75,75)

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 15.1. Ідентичність об'єктів.

Розглянемо результат виконання таких операторів (рис. 15.1б): item1.move(item2->location());

item4 = item3; item4->move(Point(38, 100));

Об'єкт item1 і об'єкт, на який вказує item2, тепер відносяться до однієї й тієї ж самої точки екрану. Вказівник item4 почав указувати на той самий об'єкт, що й item3. До речі, зверніть увагу на різницю між виразами "об'єкт item2" і "об'єкт, на який вказує item2". Другий вираз точніший, хоча для стислості ми часто будемо використовувати їх як синоніми.

Хоча об'єкт item1 і об'єкт, на який вказує item2, мають однаковий стан, вони залишаються різними об'єктами. Крім того, ми змінили стан об'єкту *item3, використавши його нове непряме ім'я item4. Це ситуація, яку ми називаємо структурною залежністю, маючи на увазі під цим ситуацію, коли об'єкт іменується більше ніж одним способом декількома синонімічними іменами. Структурна залежність породжує в об’єктноорієнтованому програмуванні багато проблем. Складність розпізнання