АвтоАвтоматизацияАрхитектураАстрономияАудитБиологияБухгалтерияВоенное делоГенетикаГеографияГеологияГосударствоДомДругоеЖурналистика и СМИИзобретательствоИностранные языкиИнформатикаИскусствоИсторияКомпьютерыКулинарияКультураЛексикологияЛитератураЛогикаМаркетингМатематикаМашиностроениеМедицинаМенеджментМеталлы и СваркаМеханикаМузыкаНаселениеОбразованиеОхрана безопасности жизниОхрана ТрудаПедагогикаПолитикаПравоПриборостроениеПрограммированиеПроизводствоПромышленностьПсихологияРадиоРегилияСвязьСоциологияСпортСтандартизацияСтроительствоТехнологииТорговляТуризмФизикаФизиологияФилософияФинансыХимияХозяйствоЦеннообразованиеЧерчениеЭкологияЭконометрикаЭкономикаЭлектроникаЮриспунденкция

Void establishSetpoint(Temperature setpoint,

Temperature delta);

Temperature currentTemperature() const;

private:

...

};

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

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

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

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

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

};

Ми використовували структуру, а не клас, оскільки 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 як співставлення діям моменти часу, вона не обов'язково має бути реалізована буквально як таблиця даних. Дійсно, клієнтові немає жодної справи до реалізації класу, який його обслуговує, до того часу, поки той дотримує свої зобов'язання. Насправді, абстракція об'єкту завжди випереджає його реалізацію. А після того, як рішення про реалізацію прийнято, воно повинне трактуватися як секрет абстракції, прихований від більшості клієнтів. Жодна частина складної системи не повинна залежати від якої-небудь внутрішньої іншої частини. В той час, як абстракція допомагає людям думати про те, що вони роблять", інкапсуляція "дозволяє легко перебудовувати програми.

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

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

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

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

Отже, інкапсуляцію можна визначити таким чином:

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

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

Як завжди, почнемо з типів.

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

enum Boolean {FALSE, TRUE};

На додаток до трьох запропонованих вище операцій, потрібні звичайні мета-операції створення й знищення об'єкта (конструктор і деструктор). Оскільки в системі може бути кілька нагрівачів, ми будемо при створенні кожного з них повідомляти його місце, де він встановлений, як ми робили це із класом датчиків температури TemperatureSensor. Отже, маємо клас Heater для абстрактних нагрівачів, написаний на C++:

class Heater {

public:

Heater(Location);

~Heater();

void turnOn();

void tum0ff();

Boolean is0n() const;

private:

};

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

Ось клас, що виражає абстрактний послідовний порт.

class SerialPort { public:

SerialPort();

~SerialPort();

void write(char*);

void write(int);

static SerialPort ports[10];

private:

};

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

Додамо ще три параметри в клас Heater.

class Heater {

public:

...

protected:

const Location repLocation;

Boolean repIsOn;

SerialPort* repPort;

};

Ці параметри repLocation, repIsOn, repPort утворять його інкапсульований стан. Правила C++ такі, що при компіляції програми, якщо клієнт спробує звернутися до цих параметрів прямо, буде видане повідомлення про помилку.

Визначимо тепер реалізації всіх операцій цього класу.

Heater::Heater(Location 1)

: repLocation(1),

repIsOn(FALSE),

repPort(&SerialPort::ports[l]) {}

Heater::Heater() {}

void Heater::turnOn()

{

if (!repls0n) {

repPort->write("*");

repPort->write(repLocation);

repPort->write(1);

repIsOn = TRUE;

}

}

void Heater::turn0ff()

{

if (repIsOn) {

repPort->write("*");

repPort->write(repLocation);

repPort->write(0);

repIsOn = FALSE;

}

}

Boolean Heater::is0n() const

{

return repIsOn;

}

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

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

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

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

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

В ідеальному випадку спроби звертання до даних, закритих для доступу, повинні виявлятися під час компіляції програми. Питання реалізації цих умов для конкретних мов програмування є предметом постійних обговорень. Так, Smalltalk забезпечує захист від прямого доступу до екземплярів іншого класу, виявляючи такі спроби під час компіляції. У теж час Object Pascal не інкапсулює подання класу, так що ніщо в цій мові не оберігає клієнта від прямих посилань на внутрішні поля іншого об'єкта. Мова CLOS займає в цьому питанні проміжну позицію, покладаючи всі обов'язки з обмеження доступу на програміста. У цій мові всі слоти можуть супроводжуватися атрибутами :reader, :writer і :accessor, що дозволяють відповідно читання, запис або повний доступ до даних (тобто й читання, і запис). При відсутності атрибутів слот повністю інкапсульований. У мові C++ керування доступом і видимістю гнучкіше. Екземпляри класу можуть бути віднесені до відкритих, закритих або захищених частин. Відкрита частина доступна для всіх об'єктів; закрита частина повністю закрита для інших об'єктів; захищена частина видна тільки екземплярам цього класу і його підкласам. Крім того, в C++ існує поняття "друзів" (friends), для яких відкрита закрита частина.

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

14.2.4. Модульність

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

Модульність - це поділ програми на фрагменти, які компілюються окремо, але можуть встановлювати зв'язки з іншими модулями. Зв'язок між модулями - це їхнє подання один про одного. У більшості мов, що підтримують принцип модульності як самостійну концепцію, інтерфейс модуля відділений від його реалізації. Таким чином, модульність і інкапсуляція дуже близькі між собою. У різних мовах програмування модульність підтримується по-різному. Наприклад, в C++ модулями є роздільно компілюючі файли. Для C/C++ традиційним є зберігання інтерфейсної частини модуля в окремому файлі з розширенням.h (так звані файли- заголовки). Реалізація, тобто текст модуля, зберігається у файлах з розширенням.с (у програмах на C++ часто використовуються розширення.сс,.ср і.срр). Зв'язок між файлами оголошується директивою макропроцесора #include. Такий підхід не є строгою вимогою самої мови. У мові Object Pascal принцип модульності формалізований трохи суворіше. У цій мові визначений особливий синтаксис для інтерфейсної частини й реалізації модуля (unit). Мова Ada іде ще на крок далі: модуль (названий package) також має дві частини - специфікацію й тіло. Але, на відміну від Object Pascal, допускається роздільне визначення зв'язків з модулями для специфікації й тіла пакету. Таким чином, допускається, щоб тіло модуля мало зв'язки з модулями, які невидимі для його специфікації.

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

Модулі виконують роль фізичних контейнерів, в яких містяться класи і об'єкти під час логічного проектування системи. Така ж ситуація виникає в проектувальників бортових комп'ютерів. Логіка електронного устаткування може бути побудована на основі елементарних схем типу НЕ, І-НЕ, АБО-НЕ, але можна об'єднати такі схеми в стандартні інтегральні схеми (модулі), наприклад, серій 7400, 7402 або 7404.

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

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

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

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

Модульність - це властивість системи, що була розкладена на внутрішньо зв'язані, але слабко зв'язані між собою модулі.

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

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

На вибір розбивання на модулі можуть впливати й деякі зовнішні обставини. Під час колективного розроблення програм розподіл роботи здійснюється, як правило, за модульним принципом й правильний поділ проекту мінімізує зв'язки між учасниками. При цьому досвідченіші програмісти звичайно відповідають за інтерфейс модулів, а менш досвідчені - за реалізацію. На більшому рівні такі ж співвідношення справедливі для відношень між субпідрядниками. Абстракції можна розподілити так, щоб швидко встановити інтерфейси модулів за згодою між компаніями, що беруть участь у роботі. Зміни в інтерфейсі викликають багато розмов і сперечань, не говорячи вже про величезну витрату паперу, - всі ці фактори роблять інтерфейс вкрай консервативним. Що стосується документування проекту, то він будується, як правило, також за модульним принципом - модуль служить одиницею опису й адміністрування. Десять модулів замість одного потребують у десять разів більше описів, і тому, на жаль, іноді вимоги з документування впливає на декомпозицію проекту (у більшості випадків негативно). Можуть позначатися й вимоги таємності: частина коду може бути несекретною, а інша - секретною; остання тоді виконується у вигляді окремого модуля (модулів).

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

Приклади модульності. Подивимося, як реалізується модульність у гідропонній городній системі. Припустимо, замість закупівлі спеціалізованого апаратного забезпечення, вирішено використовувати стандартну робочу станцію із графічним інтерфейсом користувача GUI (Graphical User Interface). За допомогою робочої станції оператор може формувати нові плани вирощування, модифікувати наявні плани й спостерігати за їхнім виконанням. Тому що абстракція плану вирощування - одна із ключових. Створимо модуль, що містить всі ці операції стосовно плану вирощування. На C++ нам буде потрібний приблизно такий файл-заголовок (нехай він називається gplan.h).

// gplan.h

#ifndef _GPLAN_H

#define _GPLAN_H 1

#include "gtypes.h"

#include "except.h"

#include "actions.h"

class GrowingPlan...

class FruitGrowingPlan...

class GrainGrowingPlan...

#endif

Тут ми імпортуємо у файл три інших заголовних файли з визначенням інтерфейсів, на які будемо посилатися: gtypes.h, except.h і actions.h. Власне код класів ми помістимо в модуль реалізації, у файл із ім'ям gplan.cpp.

Ми могли б також зібрати в один модуль всі програми, що відносяться до діалогових вікон, які є специфічними для цієї програми. Цей модуль напевно буде залежати від класів, оголошених в gplan.h, і від інших файлів-заголовків з описом класів GUI.

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

14.2.5. Ієрархія

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

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

Ієрархія - це впорядкування абстракцій, розташування їх за рівнями.

Основними видами ієрархічних структур стосовно складних систем є структура класів (ієрархія "is-a") і структура об'єктів (ієрархія "part of").

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

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

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

 

// Тип урожаю

typedef unsigned int Yield;

class FruitGrowingPlan: public GrowingPlan {

public:

FruitGrowingPlan(char* name);

virtual ~FruitGrowingPlan();

virtual void establish(Day, Hour, Condition&);

void scheduleHarvest(Day, Hour);

Boolean isHarvested() const;

unsigned daysUntilHarvest() const;

Yield estimatedYield() const;

protected:

Boolean repHarvested;

Yield repYield;

};

Це означає, що план вирощування фруктів FruitGrowingPlan є різновидом плану вирощування GrowingPlan. У нього додані параметри repHarvested і repYield, визначені чотири нові функції й перевизначена функція establish. Тепер ми могли б продовжити спеціалізацію - наприклад, визначити на базі "фруктового" плану "яблучний" клас AppleGrowingPlan.

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

Принципи абстрагування, інкапсуляції й ієрархії перебувають між собою в якомусь „здоровому” конфлікті. Абстрагування даних створює непрозорий бар'єр, що приховує стан і функції об'єкта; принцип успадкування вимагає відкрити доступ і до стану, і до функцій об'єкта для довільних об'єктів. Для будь-якого класу звичайно існують два види клієнтів: об'єкти, які маніпулюють із екземплярами даного класу, і підкласи-спадкоємці. Існують три способи порушення інкапсуляції через успадкування: підклас може одержати доступ до змінних екземпляра свого суперкласу, викликати закриту функцію й, нарешті, звернутися прямо до суперкласу свого суперкласу. Різні мови програмування по-різному знаходять компроміс між успадкуванням і інкапсуляцією; найгнучкішою щодо цього є C++. У ній інтерфейс класу може бути розділений на три частини: закриту (private), видиму тільки для самого класу; захищену (protected), видиму також і для підкласів; і відкриту (public), видиму для всіх.

Приклади ієрархії: множинне успадкування. У попередньому прикладі розглядалося одиничне спадкування, коли підклас FruitGrowingPlan був створений тільки з одного суперкласу GrowingPlan. Бувають випадки, коли корисно реалізувати успадкування від декількох суперкласів. Припустимо, що потрібно визначити клас, що представляє різновиди рослин.

class Plant {

public:

Plant(char* name, char* species);

virtual ~Plant();

void setDatePlanted(Day);

virtual establishGrowingConditions(const Condition&);

const char* name() const;

const char* species() const;

Day datePlantedt) const;

protected:

char* repName;

char* repSpecies;

Day repPlanted;

private:

...

};

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

Вивчаючи предметну область, ми робимо висновок, що різні групи культивуючих рослин - квіти, фрукти й овочі, - мають свої особливі властивості, істотні для технології їхнього вирощування. Наприклад, для квітів важливо знати час цвітіння й дозрівання насінь. Аналогічно, час збору врожаю важливо для абстракцій фруктів і овочів. Створимо два нових класи - квіти (Flower) і фрукти-овочі (FruitVegetable); вони обоє успадковуються від класу Plant. Однак деякі квіткові рослини мають плоди! Для цієї абстракції прийдеться створити третій клас, FlowerFruitVegetable, що буде успадковуватися від класів Flower і FruitVegetablePlant.

Щоб не було надмірності, у цьому випадку використаємо множинне успадкування. Спочатку давайте опишемо окремо квіти й фрукти-овочі.

class FlowerMixin {

public:

FlowerMixin(Day timeToFlower, Day timeToSeed);

virtual ~FlowerMixin();

Day timeToFlower() const;

Day timeToSeed() const;

protected:

...

};

class FruitVegetableMixin {

public:

FruitVegetableMixin(Day timeToHarvest);

virtual ~FruitVegetableMixin();

Day timeToHarvest() const;

protected:

...

};

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

class Rose:

public Plant,

public FlowerMixin...

А ось морква:

class Carrot:

public Plant,

public FruiteVegetableMixin {};

В обох випадках класи успадковуються від двох суперкласів: екземпляри підкласу Rose включають структуру й поведінку із класів Plant та FlowerMixin. Тепер визначимо вишню, у якої є як квіти, так і плоди:

class Cherry:

public Plant,

public FlowerMixin, FruitVegetableMixin...

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

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

Приклади ієрархії: агрегація. Якщо ієрархія "is_а" визначає відношення "узагальнення/спеціалізація", то відношення "part_of" (частина) вводить ієрархію агрегації. Ось приклад.

class Garden {

public:

Garden();

virtual ~Garden();

protected:

Plant* repPlants[100];

GrowingPlan repPlan;

};

Це - абстракція городу, що складається з масиву рослин і плану вирощування. Маючи справу з такими ієрархіями, ми часто говоримо про рівні абстракції, які вперше запропонував Дейкстра. В ієрархії класів вищестояща абстракція є узагальненням, а нижчестояща - спеціалізацією. Тому ми говоримо, що клас Flower перебуває на вищому рівні абстракції, ніж клас Plant. В ієрархії "part_of" клас перебуває на вищому рівні абстракції, ніж кожний, що використовувався при його реалізації. Так клас Garden стоїть на вищому рівні, ніж клас Plant.

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

У зв'язку з агрегацією виникає проблема приналежності об'єктів. У нашому абстрактному городі одночасно росте багато рослин, і від видалення або заміни одного з них город не стає іншим городом. Якщо ми знищуємо город, рослини залишаються (їх звісно можна пересадити). Інакше кажучи, город і рослини мають свої окремі й незалежні строки життя; ми досягли цього завдяки тому, що город містить не самі об'єкти Plant, а показники на них. Об'єкт GrowingPlan внутрішньо пов'язаний з об'єктом Garden і не існує незалежно. План вирощування фізично міститься у кожному екземплярі городу й знищується разом з ним. Докладніше про семантику приналежності ми будемо говорити в наступній главі.

14.2.6. Типізація

Що таке типізація? Поняття типу взяте з теорії абстрактних типів даних. Тип - це точна характеристика властивостей, включаючи структуру й поведінку, яка відноситься до деякої сукупності об'єктів. Для наших цілей досить вважати, що терміни тип і клас взаємозамінні. (Тип і клас не цілком те саме; у деяких мовах їх розрізняють. Наприклад, ранні версії мови Trellis/Owl дозволяли об'єкту мати й клас, і тип. Навіть в Smalltalk об'єкти класів SmallInteger, LargeNegativeInteger, LargePositiveInteger відносяться до одного типу Integer, хоча до різних класів. Досить сказати, що клас реалізує поняття типу). Проте, про поняття типу варто обговорити окремо, оскільки воно виражає зміст абстрагування в зовсім іншому світлі. Зокрема, ми стверджуємо, що:

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

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

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

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

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

// Число, що позначає рівень від 0 до 100 відсотків

typedef float Level;

Оператори typedef в C++ не вводять нових типів. Зокрема, і Level і Concentration - насправді інші назви для float, і їх можна вільно використовувати в обчисленнях. У цьому змісті C++ має слабку типізацію: значення примітивних типів, таких, як int або float нерозрізнені в межах даного типу. Ada і Object Pascal надають сильну типізацію для примітивних типів. В Ada можна оголосити самостійним типом інтервал значень або підмножину з обмеженою точністю.

Побудуємо тепер ієрархію класів для ємкостей:

class StorageTank {

public:

StorageTank();

virtual ~StorageTank();

virtual void fill();

virtual void startDraining();

virtual void stopDraining();

Boolean isEmpty() const;

Level level() const;

protected:

...

};

class WaterTank: public StorageTank{

public:

WaterTank();

virtual ~WaterTank();

virtual void fill();

virtual void startDraining();

virtual void stopDraining();

void startHeating();

void stopHeating();

Temperature currentTemperature() const;

protected:

...

};

class NutrientTank: public StorageTank {

public:

NutrientTank();

virtual ~NutrientTank();

virtual void startDrainingt();

virtual void stopDraining();

protected:

...

};

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

Припустимо, що ми маємо наступні описи:

StorageTank s1, s2;

WaterTank w;

NutrientTank n;

Зверніть увагу, що такі змінні як s1, s2, w або n - це не екземпляри відповідних класів. Насправді, це просто імена, якими ми позначаємо об'єкти відповідних класів: коли ми говоримо "об'єкт s1 " ми насправді маємо на увазі екземпляр StorageTank, який позначається змінною s1. Ми повернемося до цього тонкого питання в наступній главі.

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

Level l = s1.level();

w.startDrainingt();

n.stopDraining();

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

s1.startHeating(); // Неправильно

n.stopHeating(); // Неправильно

Таких функцій немає ні в самих класах, ні в їхніх суперкласах. Запис

n.fill();

вірний: функції fill немає у визначенні NutrientTank, але вона є у вищому за ієрархією класі.

Отже, сильна типізація змушує нас дотримуватися правил використання абстракцій, тому вона тим корисніша, чим більший проект. Однак у неї є й інша сторона. А саме, навіть невеликі зміни в інтерфейсі класу вимагають перекомпіляції всіх її підкласів. Крім того, не маючи параметризованих класів, важко уявити собі, як можна було б створити спільноти різнорідних об'єктів. Припустимо, що ми хочемо ввести абстракцію інвентарного списку, у якому збирається все майно, пов'язане з теплицею. Звичайна для С ідіома застосовна й в C++: потрібно використовувати клас-контейнер, що містить показники на void, тобто на об'єкти довільного типу.

class Inventory {

public:

Inventory();

~Inventory();

void add(void*);

void remove(void*);

void* mostRecent() const;

void apply(Boolean (*)(void*));

private:

...

};

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

Маючи екземпляр класу Inventory, ми можемо додавати й знищувати показники на об'єкти будь-яких класів. Але ці дії небезпечні з погляду типів - у списку можуть виявитися як відчутні об'єкти (ємкості), так і невловимі (температура або план вирощування), що порушує нашу матеріальну абстракцію. Більше того, ми могли б внести до списку об'єкти класів WaterTank і TemperatureSensor, і через необережність, очікуючи від функції mostRecent об'єкта класу WaterTank одержати StorageTank.

Загалом кажучи, для вирішення цієї проблеми є два підходи. По-перше, можна зробити контейнерний клас, безпечний з погляду типів. Щоб не маніпулювати з нетипізованими показниками void, ми могли б визначити інвентаризаційний клас, що маніпулює тільки з об'єктами класу TangibleAsset (відчутного майна), а цей клас буде додаватися до всіх класів, таке майно додається, наприклад, до WaterTank, але не до GrowingPlan. Тим самим можна відсікти проблему першого роду, коли неправомірно змішуються об'єкти різних типів. По-друге, можна ввести перевірку типів під час ходу виконання, для того, щоб знати, з об'єктом якого типу ми маємо справу в цей момент. Наприклад, в Smalltalk можна запитувати в об'єктів їхній клас. В C++ така можливість в стандарт донедавна не входила, хоча на практиці, звичайно, можна ввести в базовий клас операцію, що повертає код класу (рядок або значення перечисленого типу). Однак для цього треба мати дуже серйозні причини, оскільки перевірка типу в ході виконання послаблює інкапсуляцію. Як буде показано в наступному розділі, необхідність перевірки типу можна пом’якшити, використовуючи поліморфні операції.

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

s1 = s2;

s1 = w;

Перше присвоювання припустиме, оскільки змінні мають той самий клас, а друге - оскільки присвоювання йде знизу нагору за типами. Однак у другому випадку відбувається втрата інформації (відома в C++ як "проблема зрізання"), тому що клас змінної w, WaterTank, семантично багатший, ніж клас змінної s1, тобто StorageTank.

Такі присвоювання неправильні:

w = s1; // Неправильно

w = n; // Неправильно

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

Іноді необхідно перетворити типи. Наприклад, подивимося на наступну функцію:

void checkLevel(const StorageTank& s);

Ми можемо звести значення вищестоящого класу до підкласу в тому і тільки в тому випадку, якщо фактичним параметром у виклику виявився об'єкт класу WaterTank.

Розгонимо такий випадок:

if (((WaterTank&)s).currentTemperature() < 32.0)...

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

Переваги строго типізованих мов:

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

¨ у більшості систем процес редагування-компіляція-відлагодження складний, і раннє виявлення помилок є неоціненно корисним,

¨ оголошення типів поліпшує документування програм,

¨ багато компіляторів генерують ефективніший об'єктний код, якщо їм явно відомі типи.

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

Приклади типізації: статичне й динамічне зв'язування. Сильна й статична типізація - різні речі. Строга типізація стежить за відповідністю типів, а статична типізація (інакше названа статичним або раннім зв'язуванням) визначає час, коли імена зв'язуються з типами. Статичний зв'язок означає, що типи всіх змінних і виразів відомі під час компіляції; динамічне зв'язування (назване також пізнім зв'язуванням) означає, що типи невідомі до моменту виконання програми. Концепції типізації й зв'язування є незалежними, тому в мові програмування може бути: типізація - сильна, зв'язування - статичне (Ada), типізація - сильна, зв'язування - динамічне (C++, Object Pascal), або й типів немає, і зв'язування динамічне (Smalltalk). Мова CLOS займає проміжне місце між C++ і Smalltalk: визначення типів, зроблені програмістом, можуть бути або прийняті до уваги, або відхилені.

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

void balanceLevels(StorageTank& s1, StorageTank& s2);

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

Під час реалізації цієї функції ми можемо мати приблизно такий запис:

if (s1.level()> s2.level()) s2.fill();

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

Інша справа fill. Цей селектор визначений в StorageTank і перевизначений в WaterTank, тому його прийдеться зв'язувати динамічно. Якщо при виконанні змінна s2 буде класу WaterTank, то функція буде взята із цього класу, а якщо - NutrientTank, то з StorageTank. В C++ є спеціальний синтаксис для явної вказівки джерела; у нашім прикладі виклик fill буде дозволений, відповідно, як WaterTank::fill або StorageTank::fill.

Ця особливість називається поліморфізмом: те саме ім'я може означати об'єкти різних типів, але, маючи загального предка, всі вони мають і загальну підмножину операцій, які можна над ними виконувати. Протилежність поліморфізму називається мономорфізмом; він характерний для мов із сильною типізацією й статичним зв'язуванням (Ada).

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

14.2.7. Паралелізм

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

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

Багато сучасних операційних систем передбачають пряму підтримку паралелізму, і ця обставина сприятливо впливає на можливість забезпечення паралелізму в об’єктно-орієнтованих системах. Наприклад, системи UNIX передбачають системний виклик fork, що породжує новий процес. Системи Windows NT і OS/2 - багатопоточні; крім того вони забезпечують програмні інтерфейси для створення процесів і маніпулювання ними.

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

Об'єктна модель найкраще підходить для розподілених систем, оскільки вона неявно розбиває програму на (1) розподілені одиниці й (2) обмінні суб'єкти.

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

Паралелізм - це властивість, що відрізняє активні об'єкти від пасивних.

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

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

По-друге, можна використовувати бібліотеку класів, що реалізують який-небудь різновид "легкого" паралелізму. Наприклад, бібліотека AT&T для C++ містить класи Shed, Timer, Task і т.д. Її реалізація, природно, залежить від платформи, хоча інтерфейс досить легко переноситься. При цьому підході механізми паралельного виконання не вбудовуються в мову (і, виходить, не впливають на системи без паралельності), але в той же час практично сприймаються як вбудовані.

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


1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 |

Поиск по сайту:



Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Студалл.Орг (0.09 сек.)