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

PrIS

.pdf
Скачиваний:
55
Добавлен:
07.12.2018
Размер:
7 Мб
Скачать
☆

291

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

Ми можемо розділити інтерфейс класу на три частини:

відкриту (public) – видиму для всіх клієнтів;

захищену (protected) – видиму для самого класу, його підкласів і друзів

(friends);

закриту (private) – видиму тільки для самого класу і його друзів.

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

Зокрема, в C++ всі три перерахованих рівні доступу визначаються явно. На додаток до цього є ще й механізм друзів, за допомогою якого стороннім класам можна надати привілей бачити закриту й захищену область класу. Тим самим порушується інкапсуляція, тому, як і в житті, друзів треба вибирати обережно. В Ada оголошення можуть бути зроблені закритими або відкритими. У Smalltalk всі змінні - закриті, а методи – відкриті. У CLOS узагальнені функції відкриті, а слоти можуть бути закритими, хоча довідатися їх значення можна.

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

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

DisplayItem item1;

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

Константи й змінні, які складають подання класу, відомі під різними іменами. У Smalltalk їх називають змінними екземпляра, в Object Pascal – полями, в C++ – членами класу, а в CLOS – слотами. Ми часто будемо використовувати ці терміни як синоніми.

292

15.3.3. Життєвий цикл класу

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

15.4. Відношення між класами

15.4.1. Типи відношень

Розглянемо подібності й відмінності між такими класами: квіти, стокротки, червоні троянди, жовті троянди, пелюстки і сонечка. Зазначимо, що:

стокротка – квітка;

троянда – (інша) квітка;

червона і жовта троянди – троянди;

пелюстка є частиною обох видів квітів;

сонечка харчуються шкідниками, що вражають деякі квіти.

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

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

Відомі три основні типи відношень між класами. По-перше, це відношення "узагальнення/спеціалізація" (загальне й часткове), відоме як "is-a". Троянди є спеціалізованим частковим випадком квітів, тобто

293

підкласом загальнішого класу "квіти". По-друге, це відношення "ціле/ частина", відоме як "part of". Так, пелюстки є частиною квітів. По-третє, це семантичні, смислові відношення, асоціації. Наприклад, сонечка асоціюються із квітами – хоча, здавалося б, що в них спільного? Або ось: троянди і свічки – і одне, і друге можна використовувати для прикрашання столу.

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

асоціація,

успадкування,

аґреґація,

використання,

інсталювання,

метаклас.

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

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

Успадкування, ймовірно, варто вважати семантично найцікавішим. Воно виражає відношення загального й частини. Однак лише успадкування недостатньо, щоб виразити все різноманіття явищ і відношень життя. Корисна також аґреґація, яка задає відношення цілого й частини між екземплярами класів. Не зайво додати відношення використання, що означає наявність зв'язку між екземплярами класів. "Метакласові" відношення – це щось зовсім інше, в явному вигляді воно зустрічається тільки в мовах Smalltalk і CLOS. Метаклас – це клас класів, що дозволяє нам трактувати класи як об'єкти.

15.4.2. Асоціація

Приклад 15.2. Бажаючи автоматизувати роздрібну торговельну мережу, ми виявляємо дві абстракції – товари й продажі. На рис. 15.4 показана асоціація, яку ми при цьому вбачаємо. Клас Product – це те, що ми продали за деякою угодою, а клас Sale – сама угода, за якою продано кілька товарів. Асоціація працює в обидва боки: знаючи товар, можна

294

вийти на угоду, за якою він був проданий, а маючи угоду, знайти, що було згідно з нею продано.

Рис. 15.4. Асоціація.

У C++ це можна виразити за допомогою прихованих показників. Ось два коди оголошення відповідних класів:

class Product; class Sale; class Product { public:

...

protected: Sale* lastSale; };

class Sale { public:

...

protected:

Product** productSold; };

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

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

Потужність. У попередньому прикладі ми мали асоціацію "один- до-багатьох". Тим самим ми позначили її потужність (тобто, грубо кажучи, кількість учасників). На практиці важливо розрізняти три випадки потужності асоціації:

"один-до-одного",

"один-до-багатьох",

"багато-до-багатьох".

295

Відношення "один-до-одного" позначає дуже вузьку асоціацію. Наприклад, у роздрібній системі продажів прикладом міг би бути зв'язок між класом Sale і класом CreditCardTransaction: кожний продаж відповідає рівно одному зняттю грошей з цієї кредитної картки. Відношення "багато-до-багатьох" теж рідкісні. Наприклад, кожний об'єкт класу Customer (покупець) може ініціювати транзакцію з декількома об'єктами класу Saleperson (торговельний аґент), і кожний торговельний аґент може взаємодіяти з кількома покупцями. Всі три види потужності мають різного роду варіації.

15.4.3. Успадкування

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

class Time...

struct ElectricalData { Time timeStamp;

int id;

float fuelCell1Voltage, fuelCell2Voltage; float fuelCell1Amperes, fuelCell2Amperes; float currentPower;

};

Однак такий опис має ряд недоліків. По-перше, структура класу ElectricalData не захищена, тобто клієнт може викликати зміну такої важливої інформації, як timeStamp або currentPower (потужність, що розвивається обома електробатареями, яку можна обчислити з показників струму й напруги). По-друге, ця структура є повністю відкритою, тобто її модифікація (додавання нових елементів у структуру або зміна типу наявних елементів) впливають на клієнтів. Як мінімум, доводиться наново компілювати всі описи, зв'язані яким-небудь чином із цією структурою. Ще важливіше, що внесення у структуру змін може порушити логіку відношень із клієнтами, а отже, логіку всієї програми. Крім того, наведений опис структури дуже складний для сприйняття. Стосовно такої

296

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

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

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

class TelemetryData { public: TelemetryData();

virtual ~TelemetryData(); virtual void transmit(); Time currentTime() const; protected:

int id;

Time timeStamp; };

У цьому прикладі введений клас, який має конструктор, деструктор (який нащадки можуть перевизначити) і функції transmit і currentTime, які бачать всі клієнти. Захищені елементи id і timeStamp трохи краще інкапсульовані – вони доступні тільки класу і його підкласам. Зауважимо, що функція currentTime є відкритою, завдяки чому значення timeStamp можна читати (але не змінювати).

Тепер розглянемо ElectricalData:

class ElectricalData : public TelemetryData { public:

ElectricalData(float v1, float v2, float a1, float a2);

virtual ~ElectricalData(); virtual void.transmit(); float currentPower() const; protected:

float fuelCell1Voltage, fuelCell2Voltage; float fuelCell1Amperes, fuelCell2Amperes;

297

};

Цей клас – спадкоємець класу TelemetryData, але вихідна структура доповнена (чотирма новими елементами), а поведінка – перевизначена (змінена функція transmit). Крім того, додана функція currentPower.

Одиничне успадкування. Успадкування – це таке відношення між класами, коли один клас повторює структуру й поведінку іншого класу (одиничне успадкування) або інших (множинне успадкування) класів. Клас, структура й поведінка якого успадковуються, називається суперкласом. Так, TelemetryData є суперкласом для ElectricalData. Похідний від суперкласу клас називається підкласом. Це означає, що успадкування встановлює між класами ієрархію загального й часткового. У цьому сенсі ElectricalData є спеціалізованішим класом загальнішого класу TelemetryData. Ми вже бачили, що у підкласі структура й поведінка вихідного суперкласу доповнюються й перевизначаються. Наявність механізму успадкування відрізняє об’єктно-орієнтовані мови від об'єктних.

Підклас розширює або обмежує наявну структуру й поведінку свого суперкласу. Наприклад, підклас GuardedQueue може додавати до поведінки суперкласу Queue операції, які захищають стан черги від одночасної зміни декількома незалежними потоками. Зворотний приклад: підклас UnselectableDisplayItem може обмежити поведінку свого суперкласу DisplayItem, заборонивши виділення об'єкту на екрані. Часто підкласи роблять і те, й інше.

Відношення одиничного успадкування від суперкласу TelemetryData показані на рис. 15.5. Стрілки позначають відношення загального до часткового. Зокрема, Cameradata – це різновид класу SensorData, який, своєю чергою є різновидом класу TelemetryData. Такий же тип ієрархії характерний для семантичних мереж, які часто використовуються фахівцями з розпізнавання образів і штучного інтелекту для організації баз знань. Далі ми покажемо, що правильна організація ієрархії абстракцій – це питання логічної класифікації.

298

TelemetryData

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ElectricalData

 

PropulsionData

 

SensorData

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Spectrometer

 

Camera

 

Radiation

Data

 

Data

 

Data

 

 

 

 

 

 

 

 

 

 

 

 

Рис. 15.5. Одиничне успадкування.

Для деяких класів на рис. 15.5 можна створити екземпляри, а для інших – ні. Найімовірніше утворення об'єктів самих спеціалізованих класів ElectricalData і SpectrometerData (такі класи називають конкретними класами, або листками ієрархічного дерева). Утворення об'єктів із класів, що займають проміжне положення (SensorData або TelemetryData), менш імовірне. Класи, екземпляри яких не створюються, називаються абстрактними. Підкласи абстрактних класів довизначають їх до життєздатної абстракції, наповнюючи клас змістом. У мові Smalltalk розробник може змусити підклас перевизначити метод, вміщуючи в реалізацію методу суперкласу виклик методу SubclassResponsibility. Якщо метод не перевизначений, то при спробі його виконання ґенерується помилка. Аналогічно, в C++ існує можливість повідомляти функції віртуальними. Якщо вони не перевизначені, екземпляр такого класу неможливо створити.

Найзагальніший клас в ієрархії класів називається базовим. У більшості інформаційних систем базових класів буває декілька, і всі вони відбивають найзагальніші абстракції предметної області. Насправді, особливо в C++, добре зроблена структура класів – це швидше ліс із дерев успадкування, ніж одна багатоповерхова структура успадкування з одним коренем. Але в деяких мовах програмування є базовий клас найвищого рівня, що є єдиним суперкласом для всіх інших класів. У мові Smalltalk цю роль відіграє клас object.

Клас зазвичай має два види клієнтів:

екземпляри;

299

підкласи.

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

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

Успадкування припускає, що підкласи повторюють структури їх суперкласів. У попередньому прикладі екземпляри класу ElectricalData містять елементи структури суперкласу (id і timeStamp) і

спеціалізованіші елементи (fuelCell1Voltage, fuelCell2Voltage, fuelCell1Amperes, fuelCell2Amperes).

Поведінка суперкласів також успадковується. Стосовно об'єктів класу ElectricalData можна використовувати операції currentTime (успадкована від суперкласу), currentPower (визначена у класі) і transmit (перевизначена у підкласі). У більшості мов допускається не тільки успадковувати методи суперкласу, але також додавати нові і перевизначати наявні методи. У Smalltalk будь-який метод суперкласу можна перевизначити у підкласі.

У C++ ступінь контролю за цим трохи вищий. Функція, оголошена віртуальною (функція transmit у попередньому прикладі), може бути в підкласі перевизначена, а інші (currentTime) – ні.

Одиничний поліморфізм. Нехай функція transmit класу

TelemetryData реалізована в такий спосіб: void TelemetryData::transmit()

{

//передати id

//передати timeStamp

};

У класі ElectricalData та ж сама функція перевизначена: void ElectricalData::transmit()

{

TelemetryData::transmit();

//передати напругу

//передати силу струму

300

};

Ця функція спочатку викликає однойменну функцію суперкласу за допомогою її явно кваліфікованого імені TelemetryData::transmit(). Та передасть заголовок пакету (id і timeStamp), після чого в підкласі передаються його власні дані.

Визначимо тепер екземпляри двох описаних вище класів:

TelemetryData telemetry;

ElectricalData electrical(5.0, -5.0, 3.0, 7.0);

Тепер визначимо вільну процедуру:

void transmitFreshData (TelemetryData& d, const Time& t)

{

if (d.currentTime() >= t) d.transmit();

);

Що відбудеться, якщо виконати такі два оператори? transmitFreshData(telemetry, Time(60)); transmitFreshData(electrical, Time(120));

У першому операторі буде передано вже відомий нам заголовок. У другому буде переданий він же, плюс чотири числа у форматі із плаваючою крапкою, що містять результати вимірів електричних параметрів. Чому це так? Нібито функція transmitFreshData нічого не знає про клас об’єкту, вона просто виконує d.transmit()! Це був приклад поліморфізму. Змінна d може позначати об’єкти різних класів. Ці класи мають спільний суперклас і вони, хоча й по різному, можуть реагувати на одне і те саме повідомлення, однаково розуміючи його зміст.

Традиційні типізовані мови типу Pascal засновані на тій ідеї, що функції та процедури, а отже, і операнди повинні мати певний тип. Ця властивість називається мономорфізмом, тобто кожна змінна й кожне значення відносяться до одного певного типу. На противагу мономорфізму поліморфізм допускає віднесення значень і змінних до декількох типів. Уперше ідею поліморфізму ad hoc описав Страчі, маючи на увазі можливість перевизначати зміст символів, таких, як "+", згідно з потребою. У сучасному програмуванні ми називаємо це перевантаженням. Наприклад, у C++ можна визначити кілька функцій з тим самим іменем, і вони будуть автоматично відрізнятися за кількістю й типами своїх арґументів. Сукупність цих ознак називається сигнатурою функції; у мові Ada до цього списку додається тип значення, що повертається. Страчі говорив також про параметричний поліморфізм, який ми зараз називаємо просто поліморфізмом.

При відсутності поліморфізму код програми змушений містити багато операторів вибору case або switch. Наприклад, мовою Pascal