Поняття простого поліморфізму
Інкапсуляція
Сутність інкапсуляції. При розгляді абстракції описувалася тільки поведінка об’єкту, тобто укладалися контрактні умови, що повинен виконувати об’єкт і як клієнт може взаємодіяти з об’єктом. В загальному клієнта не цікавить яким чином сервер організовано і яких зусиль він (сервер) докладає, щоб виконати умови контракту (зобов’язання) зі своєї сторони. Ніяка частина складної системи не повинна залежати від внутрішньої будови якої-небудь іншої частини [Інглас]. Абстракція допомагає обдумувати те, що робиш, а інкапсуляція дозволяє легко перебудовувати програми.
Означення інкапсуляції:
Інкапсуляція – це процес розмежування елементів об’єкту, що визначають його влаштування та поведінку; інкапсуляція послуговує для того, щоб ізолювати контрактні зобов’язання абстракції від їх реалізації.
Абстракція та інкапсуляція доповнюють одне одного: абстрагування направлено на явну поведінку об’єкту, а інкапсуляція займається внутрішнім облаштуванням. Інкапсуляція забезпечується через закриття інформації про внутрішню структуру об’єкту, маскуванням внутрішніх деталей які явно не впливають на зовнішню поведінку. Інкапсуляція таким чином, визначає чіткі межі між різними абстракціями.
Наявність в об’єктно-орієнтованому стилі абстракції та інкапсуляції зумовлює присутність в класі двох частин: інтерфейсу та реалізації. Інтерфейс відображає зовнішню поведінку об’єкту, що описує абстракцію поведінки всіх об’єктів даного класу. Внутрішня реалізація описує подання цієї абстракції та механізми за якими досягається бажана поведінка об’єкту.
Приклади інкапсуляції. Продовжимо розглядати тепличну систему. Допустимо, що в тепличному господарстві для підтримування температури на певному рівні є підсистема нагрівачів, або підсистеми для підтримування інших параметрів в актуальному стані. Причому нагрівачі можуть бути різних типів та ґрунтуватися на різних принципах дії. Клієнтів в загальному не цікавить як влаштовані нагрівачі, для них тільки важливо, щоб нагрівання відбувалося. Тоді абстрактний клас нагрівача можна описати наступним чином:
// Булевий тип
enum Boolean {FALSE, TRUE};
class Heater {
public:
Heater(Location);
~ Heater();
void turnOn();
void turnOff();
Boolean isOn() const;
private:
. . .
};
Нижче ключового слова public описано інтерфейс класу, все що необхідно користувачу знати про об’єкт.
Якщо, нагрівач буде керуватися із-зовні теплиці через послідовний комунікаційний порт, то необхідно добавити клас, що його реалізує:
class SerialPort {
public:
SerialPort(Location);
~ SerialPort();
void write(char*);
void write(int);
static SerialPort port[10];
private:
. . .
};
Тоді захищена частина класу Heater матиме вигляд:
class Heater {
public:
. . .
protected:
const Location repLocation;
Boolean repIsOn;
SerialPort* repPort;
};
Змінні repLocation, repIsOn, repPort утворюють інкапсульований стан об’єкту. До них обмежене пряме звертання з інших об’єктів (компілятор генеруватиме помилку).
Методи (або функції-члени) можна реалізувати наступним чином:
Heater::Heater(Location loc) : repLocation (loc),
repIsOn(FALSE),
repPort(&SerialPort::ports(1))
{ }
Конструктор за замовчуванням:
Heater::Heater() { }
void Heater::turnOn()
{
if (!repIsOn) {
repPort->write(“*”);
repPort->write(repLocation);
repPort->write(1);
repIsOn = TRUE;
}
}
void Heater::turnOff()
{
if (repIsOn) {
repPort->write(“*”);
repPort->write(repLocation);
repPort->write(0);
repIsOn = FALSE;
}
}
Boolean Heater::isOn() const { return repIsOn; }
Якщо, при такому описанні класів помінявся принцип комунікації нагрівача з керуючим пристроєм (через пам’ять, паралельний порт, чи ін.), то достатньо змінити реалізацію захищеної частини, а інтерфейс залишиться незмінним.
Іншим прикладом інкапсуляції може служити абстракція файлу операційної системи. Для роботи з файлом існує ряд основних функцій: відкрити, читати, писати та закрити. Але де фізично розміщена інформація, що зв’язана з файлом і який механізм доступу до неї в загальному не розголошується. Хоча відомості про реалізацію файлової системи в загальному дозволить ефективніше оперувати з вмістом файлу.
Грамотна інкапсуляція локалізує ті особливості проекту, які можуть піддаватися частим змінам. Це дозволяє модифікувати та оптимізувати реалізацію класу без необхідності відслідковувати ці зміни в цілому проекті. Зачатки інкапсуляції закладені і в мові С через заголовкові файли в яких описувався інтерфейс модулів (бібліотек).
Поняття простого поліморфізму
Розглянемо реалізацію функції send класу PrimeData:
Void PrimeData::send(){
// передати номер
// передати час
{;
void FullData::Send {
PrimeData::Send();
// передати температуру
// передати потужність
};
Тепер визначимо екземпляри об’єктів двох вищезгаданих класів
PrimeData prime;
FullData full (12);
та функцію
Void SendData (PrimeData &D) {D.Send();}:
Що відбувається при виконанні наступних операторів:
SendData (Prime);
SendData (full);
В першому випадку здійснюється пересилання ідентифікатора станції та поточного часу, в другому випадку – тих же даних та додатково ще температури та потужності. Чому? Адже тіло функції SendData містить лише один оператор D.Send(), який не відрізняється в обох випадках. Це пояснюється поліморфізмом.
Поліморфізм –це такий елемент теорії типізації, який дозволяє використовувати одне ім’я (параметр D) для позначення об’єктів різних класів, що мають спільний суперклас.
В результаті об’єкт з таким ім’ям може по-різному реагувати на виконання спільного набору операцій.
Традиційні типізовані мови програмування вважають, що функції та операнди повинні мати певний тип. Ця властивість називається мономорфізмом, тобто кожна змінна та кожне значення віднесено до одного певного типу. В протилежність мономорфізму поліморфізм дозволяє віднесення значень та змінних до кількох типів.
Вперше поліморфізм описав Страчі[]. Він ввів особливий вид поліморфізму, в якому символи, такі як “+”, можуть мати різне значення (зараз це називають перевантаженням - overloading). В С++ дозволяється об’являти декілька функцій (наприклад, конструкторів), що мають однакове ім’я, але відрізняються за кількістю та типами параметрів. Накінець, так званий параметричний поліморфізм, або просто поліморфізм, який ми вже розглянули.
Що було б за відсутності поліморфізму: ми змушені були б ввести ще одну додаткову змінні kind – вид даних та присвоїти їй значення 0 – для класу PrimeData та 1 – для класу FullData. В операторі switch(kind) перевірялося б значення цієї змінної і в залежності від нього викликалася б та чи інша функція.
За умови введення нового типу даних прийшлося б додавати новий case, тобто ймовірність помилок зросла б.
Поліморфізм базується на ідеї пізнього пов’язування. Розглянемо це більш детально.
В традиційних мовах програмування виклик підпрограми є певною операцією. В стек заноситься адреса, куди слід повернутися по закінченні підпрограми, далі в стек заносяться аргументи, що передаються в підпрограму, далі йде перехід на вказану адресу.
push a1
push a2
jmp 1256
Всі адреси визначаються на стадії компіляції, тобто ще до виконання програми компілятору мають бути відомі конкретні адреси всіх підпрограм. Але в нашому випадку це неможливо, оскільки лише під час виконання програми в функцію прийде аргумент, що визначає, який тип об’єкта і яку відповідно функцію слід використовувати.
Тому для реалізації поліморфізму від такого підходу відмовились.
Альтернативний варіант запропоновано в мові SmallTalk. Це повністю об’єктна мова, там не існує підпрограм в звичайному розумінні, тільки методи певних класів. Об’єкти обмінюються повідомленнями. Коли об’єкт отримує повідомлення, він здійснює пошук цього повідомлення в словнику свого класу. Якщо повідомлення знайдено, управління передається на реалізацію відповідного метода. Якщо ні, пошук переходить в базовий клас. Процес повторюється, поки не буде знайдено відповідного повідомлення або процес не досягне базового класу.
Як бачимо, тут гарантується виклик методу для найвищого по ієрархії об’єкта (що нам і треба), але ефективність цієї реалізації доволі низька (в 1,5 рази триваліше за звичайний виклик підпрограми).
Як же сумістити ідеї поліморфізму та високу ефективність виклику звичайної підпрограми?
В С++ операції, що вимагають пізнього пов’язування, об’являються віртуальними (virtual), а всі інші обробляються компілятором як звичайні виклики підпрограм.
Для управління реалізацією віртуальних функцій використовується концепція V-таблиць (таблиць віртуальних методів). Вони створюються для кожного класу об’єктів. Така таблиця містить список покажчиків на віртуальні функції. Якщо в деякому класі віртуальна функція не перевизначена, то у V-таблицю записується адреса реалізації цієї функції в найближчому за ієрархією суперкласі.
Згідно з цим здійснюється пошук потрібної функції під час виконання програми: відбувається звертання через відповідний покажчик до функції об’єкта і одразу реалізується правильно обраний код без будь-якого пошуку.
Невіртуальні методи можуть бути задекларовані як такі, що підставляються (inline). При цьому відповідні підпрограми цілком включаються в код програми (без всяких стеків та переходів) – так зване макровизначення. Але це призводить до більших витрат пам’яті (хоча швидкість підвищується).
Виклик віртуальної функції тільки трохи поступається за ефективністю виклику звичайної функції (вимагає трьох-чотирьох операцій адресації додатково) – Страустрап.
Borland C та Borland Pascal.
|
class X : public Y
Присвоювання об’єкту Y значення X можливо, якщо тип об’єкта Х співпадає з типом об’єкта Y або Х є підкласом Y. Чому?
Крім того, і в С++, і в Pascal допускається явне перетворення значень одного типу в інший (але тільки в тих випадках, коли між двома типами існує відношення виду клас/підклас).
Наслідування
Наслідування – це відношення між класами, коли клас повторює структуру та поведінку іншого класу (одиночне наслідування) або інших класів (множинне наслідування).
Клас, структура та поведінка якого наслідується називається суперкласом (надкласом), а похідний від нього клас називають підкласам. Наслідування встановлює між класами відношення (ієрархію) загального та часткового.
Механізм наслідування відрізняє об’єктно-орієнтовані мови від об’єктних.
Підклас може перевизначити, розширити та звузити (використовуючи механізм інкапсуляції) поведінку базового класу.
class TelemetryData{
public:
TelemetryData();
virtual ~TelemetryData();
virtual void transmit();
Time currentTime();
protected:
int id;
Time timeStamp;
};
class ElectricalData : public TelemetryData{
public:
ElectricalData(float v1, float v2,float a1,float a2);
virtual ~ ElectricalData();
virtual void transmit();
float currentPower() const;
protected:
float fuelCeill1Voltage, fuelCeill2Voltage;
float fuelCeill1Amperes, fuelCeill2Amperes;
};
Класи, екземпляри яких не створюються називаються абстрактними класами. В С++ абстрактний клас визначається через наявність оголошених але не реалізованих віртуальних функцій. Доки така функція не буде реалізована компілятор не дозволить створити екземпляру цього класу.
voidTelemetryData::transmit()
{
// передати id
// передати timeStamp
}
voidElectricalData::transmit()
{
TelemetryData::transmit();
// передати напругу
// передати силу струму
}
В С++ класи мають деревоподібну структуру (ліс). В інших мовах програмування (Smalltalk, Java) існує базовий клас від якого походять усі інші класи.
Поведінка підкласу може перевизначитися через зміни в реалізації методів. В Smalltalk перевизначитися може довільний метод. В С++, тільки ті методи, що позначені ключовим словом virtual, а інші ні.
Одиночний поліморфізм. Дозволяє розрізняти оператори та операції в залежності від кількості параметрів та їх типів. Стосовно до операторів така властивість називається перевантаженням оператора. Найбільш гнучко поліморфізм реалізовано в мові С++. Поліморфізм дозволяє обійтися без операторів вибору так як об’єкти самі знають свій тип.
voidtransmitFreshData(TelemetryData & d, const Time & t)
{
if (d. currentTime() >= t) d. transmit();
}
Наслідування без поліморфізму можливе, але не ефективне. Поліморфізм тісно пов’язаний з механізмом пізнього зв’язування. В С++ цей механізм застосовується тільки для віртуальних функцій, для решти функцій він не має змісту оскільки вони відомі на етапі компіляції і зв’язуються за механізмом раннього зв’язування.
Наслідування та типізація. В більшості об’єктно-орієнтованих мов програмування при реалізації методів підкласу є механізм виклику методів надкласу напряму. В С++ для цього використовуються кваліфікатори, якими є імена відповідних надкласів. Дозволяється звертатися до довільного рівня вкладеності. Об’єкт може посилатися сам на себе за допомогою вказівника this. Методи надкласів викликаються до або після уточнюючих дій (поведінки) підкласу.
Реалізований в С++ механізм типізації дозволяє застосовувати наступне присвоювання.
TelemetryData telemetry;
ElectricalData electrical;
telemetry = electrical;
Але таке присвоювання небезпечне, бо довільні дані, що є розширеннями підкласу (чотири додаткових змінні-члени) втрачаються. Зворотне присвоювання недопустиме, так як telemetry не є підтипом electrical.
Множинне наслідування. Застосовується коли в підкласі необхідно зібрати властивості декількох, здебільшого, різнопланових надкласів, у той же час важко вибрати який саме надклас найбільше підходить для наслідування.
При застосування множинного наслідування виникають дві основні проблеми це розв’язання конфліктів між надкласами (конфлікт імен) та повторне наслідування.
Для подолання конфлікту імен застосовують три способи. Перший, одинакові імена вважаються помилками і код відповідно коректується (Smalltalk, Eiffel). Другий, одинакові імена вважаються одним атрибутом (CLOS). Третій (найбільш гнучкий), використовуються префікси (кваліфікатори) С++.
Проблема повторного наслідування також розв’язується трьома способами. Перший, повторне наслідування забороняється на етапі компілювання (Smalltalk, Eiffel). Другий, явно розводять класи джерела через систему префіксів (С++). Третій, множинні посилання розглядають як посилання на один і той же клас (С++).
Множинний поліморфізм. В С++ можна реалізувати механізм подвійної диспетчеризації. Спочатку реалізується поліморфна поведінка за однією гілкою наслідування в залежності від типу об’єкту, пізніше поліморфізм відслідковується за типом аргументу.
Агрегація
Відношення агрегації між класами та об’єктами тісно пов’язані між собою. З першого випливає друге.
Розглянемо приклад:
class TemperatureController {
public:
TemperatureController(Location);
~TemperatureController();
void process(const TemperatureRamp &);
Minute schedule(const TemperatureRamp &) const;
protected:
Heater h;
};
Об’єкт класу Heater агрегований в клас TemperatureController.
В об’єктно-орієнтованому програмування розрізняють два види агрегації:
- за значенням, фізичне включення;
- за посиланням.
При фізичному включення обидва об’єкти створюються та знищуються одночасно. При включенні за посиланням об’єкти створюються асинхронно і один об’єкт може пережити (в часі) іншого.
Агрегацію за посиланням часто можна переплутати з асоціацією. Тут слід пам’ятати, що асоціація двох-направлений зв’язок, а агрегація одно-направлений і виражає відношення “цілого/частини”. Тобто, один з об’єктів повинен бути фізичною чи логічною (акціонер та акції) частиною іншого.
В практичному програмуванні агрегація може бути альтернативою множинного наслідування, це дозволяє обійти проблеми, що виникають при ньому.
Рис. Агрегація
Порядок виконання роботи
Cтворити проект, у якому реалізувати поліморфізм, інкапсуляцію та наслідування з використанням предметної області згідно варіанту.
Варіанти завдань
1. Клас котів.
2. Клас собак.
3. Клас викладачів.
4. Клас робітників.
5. Клас дітей.
6. Клас тварин.
7. Клас птахів.
8. Клас рослин.
9. Клас прямокутників.
10. Клас трикутників.
11. Клас кіл.
12. Клас відрізків.
13. Клас свят.