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

PrIS

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

231

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

Відповідно до цього визначення не всі мови програмування є об'єктно-орієнтованими. Якщо термін об'єктно-орієнтована мова взагалі що-небудь означає, то він повинен означати мову, що має засоби підтримки об'єктно-орієнтованого стилю програмування. Забезпечення такого стилю, своєю чергою означає, що в мові зручно користуватися цим стилем. Якщо написання програм у стилі OOP вимагає спеціальних зусиль або воно неможливе зовсім, то ця мова не відповідає вимогам OOP. Теоретично можлива імітація об'єктно-орієнтованого програмування на звичайних мовах, таких як Pascal і навіть COBOL або асемблер, але це вкрай складно. Мова програмування є об'єктноорієнтованою тоді і лише тоді, коли виконуються такі умови:

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

об'єкти відносяться до відповідних типів (класів);

типи (класи) можуть успадковувати атрибути супертипів (суперкласів).

Підтримка успадкування в таких мовах означає можливість встановлення відношення "is-а" ("є", "це є" " ─ це"), наприклад, червона троянда ─ це квітка, а квітка ─ це рослина. Мови, що не мають таких механізмів, не можна віднести до об'єктно-орієнтованих. Карделлі і Веґнер назвали такі мови об'єктними, але не об'єктно-орієнтованими.

Згідно із цим визначенням об'єктно-орієнтованими мовами є Smalltalk, Object Pascal, C++ і CLOS, а Ada ─ об'єктна мова. Але, оскільки об'єкти і класи є елементами обох груп мов, бажано для них використовувати методи об'єктно-орієнтованого проектування.

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

Об'єктно-орієнтоване проектування ─ це методологія проектування, що об’єднує в собі процес об'єктної декомпозиції і прийоми подання логічної, фізичної, статичної і динамічної моделей проектованої системи.

У цьому означенні містяться дві важливі частини: об'єктноорієнтоване проектування 1) ґрунтується на об'єктно-орієнтованій декомпозиції; 2) використовує різні прийоми подання моделей, що

232

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

Саме об'єктно-орієнтована декомпозиція відрізняє об'єктноорієнтоване проектування від структурного; у першому випадку логічна структура системи відображається абстракціями у вигляді класів і об'єктів, у другому ─ алгоритмами. Інколи ми будемо використовувати абревіатуру OOП, для позначення методу об'єктно-орієнтованого проектування.

Об'єктно-орієнтований аналіз. На об'єктну модель вплинула модель життєвого циклу програмного забезпечення. Традиційна техніка структурного аналізу заснована на потоках даних в системі. Об'єктноорієнтований аналіз (або OOA, object-oriented analysis) напрямлений на створення моделей реальної дійсності на основі об'єктно-орієнтованого світогляду.

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

Як співвідносяться ООА, OOП і OOP? На результатах ООА формуються моделі, на яких ґрунтується OOП; OOП, своєю чергою створює фундамент для остаточної реалізації системи з використанням методології OOP.

14.2. Складові частини об'єктного підходу

14.2.1. Парадигми програмування

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

процедурно-орієнтований

алгоритми

об'єктно-орієнтований

класи і об'єкти

логіко-орієнтовані

цілі, часто виражені в термінах

 

числення предикатів

орієнтований на правила

правила "якщо-то"

233

орієнтований на обмеження

інваріантні співвідношення

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

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

абстрагування;

інкапсуляцію;

модульність;

ієрархію.

Ці елементи є головними в тому сенсі, що без будь-якого з них модель не буде об'єктно-орієнтованою. Окрім головних, є ще три додаткові елементи:

типізація;

паралелізм;

збережуваність.

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

Без такої концептуальної основи можна програмувати на мові типу

Smalltalk, Object Pascal, C++, CLOS, Eiffel або Ada, але з-під зовнішньої краси визиратиме стиль FORTRAN, Pascal або С. Виразна здатність об'єктно-орієнтованої мови буде або втрачена, або спотворена. Але ще важливішим буде те, що при цьому буде мало шансів впоратися зі складністю вирішуваних завдань.

14.2.2. Абстрагування

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

234

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

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

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

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

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

Абстракція сутності. Об'єкт є корисною моделлю деякої сутності ПО.

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

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

Довільна абстракція. Об'єкт включає набір операцій, які не мають одна з одною нічого спільного.

Ми прагнемо будувати абстракції сутності, оскільки вони прямо відповідають сутностям предметної області.

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

235

іншими об'єктами). Контракт фіксує всі зобов'язання, які об'єкт-сервер має перед об'єктом-клієнтом. Іншими словами, цей контракт визначає відповідальність об'єкту - ту поведінку, за яку він відповідає.

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

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

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

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

236

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

Приклади абстракцій. Для ілюстрації сказаного вище наведемо декілька прикладів. Ми сконцентруємо увагу не стільки на виділенні абстракцій для конкретної задачі (це детально розглянемо в розділі 16), скільки на способі вираження абстракцій.

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

Одна з ключових абстракцій в такій задачі ─ сенсор. Відомо декілька різновидів сенсорів. Все, що впливає на урожай, має бути виміряне, так що ми повинні мати сенсори температури води і повітря, вологості, рН, освітлення і концентрації живильних речовин. Із зовнішньої погляду датчик температури ─ це об'єкт, який здатний вимірювати температуру там, де він розташований. Що таке температура? Це числовий параметр, що має обмежений діапазон значень і певну точність, означає кількість градусів за Фаренґейтом, Цельсієм або Кельвіном. Що таке місце розташування сенсора? Це деяке місце, що ідентифікується в теплиці, температуру в якому нам необхідно знати; таких місць, ймовірно, небагато. Для сенсора температури істотне не стільки саме місце розташування, скільки той факт, що цей сенсор розташований саме в певному місці і це відрізняє його від інших сенсорів. Тепер можна поставити запитання про те, які обов'язки сенсора температури? Ми вирішуємо, що сенсор повинен знати температуру у місці свого розташування і повідомляти її за запитом. Які ж дії може виконувати по відношенню до сенсора клієнт? Ми приймаємо рішення про те, що клієнт може калібрувати сенсор і отримувати від нього значення поточної температури.

Для демонстрації проектних вирішень буде використана мова C++. Ось опис, який встановлює абстрактний датчик температури на C++.

// Температура за Фаренґейтом typedef float Temperature;

237

// Число, що однозначно визначає положення сенсора

typedef unsigned int Location; class TemperatureSensor { public:

TemperatureSensor (Location); ~TemperatureSensor();

void calibrate(Temperature actualTemperature); Temperature currentTemperature() const; private:

...

};

Тут для двох операторів визначення типів Temperature і Location вводять зручні псевдоніми для простих типів, і це дозволяє нам виражати свої абстракції на мові ПО. На жаль, конструкція typedef не визначає нового типу даних і не забезпечує його захисту. Наприклад, такий опис в C++: "typedef int Count;" просто вводить синонім для примітивного типу int. Temperature ─ це числовий тип даних у форматі з плаваючою крапкою для запису температур у шкалі Фаренґейта. Значення типу Location позначає місце ферми, де можуть розташовуватися температурні сенсори.

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

Temperature temperature; Temperaturesensor greenhouse1sensor(1); Temperaturesensor greenhouse2sensor(2);

temperature = greenhouse1sensor.currentTemperature();

Розглянимо інші варіанти, пов'язані з операцією currentTemperature. Передумова включає припущення, що сенсор встановлений у правильному місці в теплиці, а післяумова ─ що сенсор

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

238

class ActiveTemperatureSensor { public:

ActiveTemperatureSensor (Location, void (*f)(Location, Temperature)); ~ActiveTemperatureSensor();

void calibrate(Temperature actualTemperature); void establishSetpoint(Temperature setpoint, Temperature delta);

Temperature currentTemperature() const; private:

...

};

Новий клас ActiveTemperatureSensor став лише трішки складніший, але цілком адекватно виражає нову абстракцію. Створюючи екземпляр сенсора, ми передаємо йому при ініціалізації не лише місце, але і вказівник на функцію зворотного виклику, параметри якої визначають місце встановлення і температуру. Нова функція установки establishSetpoint дозволяє клієнтові змінювати поріг опрацювання сенсора температури, а відповідальність сенсора полягає в тому, щоб викликати функцію зворотного виклику кожного разу, коли поточна температура actualTemperature відхиляється від setpoint більше, ніж на delta. При цьому клієнтові стає відоме місце спрацювання сенсора і температура в ньому, а далі вже він сам повинен знати, що з цим робити.

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

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

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

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

239

зростання планом передбачено підтримку протягом 16 годин температури 20º С, з них 14 годин з освітленням, а потім пониження температури до 15º С на решту часу доби. Крім того, може бути потрібне внесення добрив посередині дня, щоб підтримати задане значення кислотності.

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

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

На C++ план вирощування виглядатиме таким чином. Спочатку введемо нові типи даних, наближаючи наші абстракції до словника предметної області (день, година, освітлення, кислотність, концентрація):

//Число, що позначає день року typedef unsigned int Day;

//Число, що позначає годину дня typedef unsigned int Hour;

//Булевий тип

enum Lights {OFF, ON};

//Число, що позначає показник кислотності в діапазоні від 1 до 14

typedef float pH;

//Число, що позначає концентрацію у відсотках: від 0 до 100

typedef float Concentration;

Далі, в тактичних цілях, опишемо таку структуру:

//Структура, що позначає умови в теплиці

struct Condition { Temperature temperature; Lights lighting;

pH acidity;

Concentration concentration; };

240

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

План вирощування: class GrowingPlan ( public:

GrowingPlan (char *name); virtual ~GrowingPlan(); void clear();

virtual void establish(Day, Hour, const Condition&); const char* name() const;

const Condition& desiredConditions(Day, Hour) const; protected:

...

};

Зазначимо, що ми передбачили один новий обов'язок: кожен план має ім'я, його можна встановлювати і викликати. Крім того, операція establish описана як virtual для того, щоб підкласи могли її перевизначати.

У відкриту (public) частину опису винесені конструктор і деструктор об'єкту (визначальні процедури його породження і знищення), дві процедури модифікації (очищення всього плану clear і визначення елементів плану establish) і два селектори-визначники стану (функції name і desiredCondition). Ми опустили в описі закриту частину класу, замінивши її багатьма крапками, оскільки зараз нам важливі зовнішні посилання, а не внутрішнє подання класу.

14.2.3. Інкапсуляція

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

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