Об’єктно-орієнтоване програмування
(лекції)
Зміст
Частина І Основи об’єктно-орієнтованого програмування 2
Тема № 1: Концепція об’єктно-орієнтованого програмування. Об’єктна модель. 2
Тема № 2: Об’єктна модель. Складові об’єктного підходу. 6
Тема № 3: Класи та об’єкти. 16
Тема № 4: Процес проектування. 29
Частина ІІ Об’єктно-орієнтоване програмування під ОС Windows 31
Тема № 5: Основи операційної системи Windows 31
Тема № 6: Структура програм під ОС Windows 36
Тема № 7: Бібліотека базових класів Microsoft (MFC) 40
Тема № 8: Структура програми на основі класів MFC 44
Тема № 9: Основні типи програм на основі класів MFC 51
Тема № 10: Елементи інтерфейсу користувача на основі класів MFC 57
Тема № 11: Графічні об’єкти в MFC 65
Контрольні запитання 68
До модулю 1 68
До модулю 2 70
Екзаменаційні 72

Частина І Основи об’єктно-орієнтованого програмування
Тема № 1: Концепція об’єктно-орієнтованого програмування. Об’єктна модель.
План
1. Складні системи. Декомпозиція, абстрагування та ієрархія.
2. Еволюція об’єктної моделі. Основи об’єктної моделі.
3. Об’єктно-орієнтовані програмування, проектування та аналіз.
Складні системи
Складність систем ми будемо розглядати стосовно промислових програмних продуктів, що розробляються групою розробників або навіть і декількома групами. Суттєва риса промислових програм - рівень складності. Одинокий розробник або навіть й група розробників не в стані охопити всі аспекти такої системи. Складність таких систем здебільшого перевищує можливості людського інтелекту. Згаданою складністю очевидно володіють усі великі системи. Цю складністю можна подолати, але уникнути її неможливо. Тому необхідно розглянути надійні способи побудови складних систем, які б з високою ймовірністю гарантували успіх в їх проектуванні.
Аспекти складності стосовно розробки програмного забезпечення:
- складність реального світу, або тої його частини яку моделює програма;
- складність управління процесом розробки;
- проблеми описання поведінки великих дискретних систем.
Структура складних систем
Основні властивості складних систем, що обґрунтовують об’єктний підхід:
- складні системи є ієрархічними і складаються із взаємопов’язаних частин, що також можуть ділитися на підсистеми;
- розділ систем на елементарні компоненти є відносно довільний і здебільшого залежить від дослідника;
- зв’язок в середині компонентів набагато сильніший ніж між компонентами, це обумовлює “високочастотну” взаємодію всередині компонентів та “низькочастотну” між компонентами;
- ієрархічні системи складаються з невеликої кількості типів підсистем різним чином скомбінованих та організованих;
- діюча складна система є результатом розвитку більш простої системи, що працювала раніше. (Складна система, що спроектована з “нуля” навряд чи запрацює.)
Досвід проектування складних систем показує [Буч], що найбільш успішними є ті системи в яких закладено добре продумані структури класів та об’єктів і які володіють згаданими вище властивостями.
Структури класів та об’єктів системи разом називають архітектурою системи.
При проектуванні складних систем розглядають три основні аспекти:
- декомпозиція;
- абстрагування;
- ієрархія.
Декомпозиція
На древньому принципі управління “розділяй та владарюй” і зараз базується побудова та управління складною системою. При проектування програмна система розкладається на все менші та менші підсистеми, що можуть розроблятися та вдосконалюватися незалежно.
До сих пір ми мали справу зі структурним проектуванням “зверху вниз” і декомпозиція сприймається як розділення алгоритмів, де кожен модуль виконує якусь частину загального процесу.
При об’єктно-орієнтованій декомпозиції система розбивається на об’єкти, кожен з яких живе своїм життям (знаходиться у якомусь стані). Обмінюючись повідомленнями об’єкти об’єднуються у систему для виконання спільних дій.
При проектуванні систем важливі як алгоритмічна так і об’єктна декомпозиції. Вони є взаємодоповнюючими а не взаємозамінними. Але об’єктно-орієнтована декомпозиція значно зменшує об’єми програмних систем за рахунок повторного використання загальних механізмів, що приводить до економії засобів вираження сутності системи.
Абстракція
Абстракція полягає у виділенні (на певному рівні) найбільш важливих ознак об’єктів (інформації про об’єкти) реального світу які моделюються та в абстрагуванні від незначних деталей. Таким чином формується узагальнююча, ідеалізована модель об’єкту. Незначні, на певному рівні абстракції, елементи моделі можуть уточнюватися на нижчих рівнях і на їх основі об’єкти розрізняються між собою.
Ієрархія
Ієрархія класів та об’єктів є способом розширення інформативності елементів системи. Об’єктна структура ілюструє схему взаємодії об’єктів між собою. Структура класів визначає узагальнення структур та їх поведінку в середині системи.
Визначити ієрархічні зв’язки в системі не завжди просто, але після їх визначення структура складної системи та сама система стає зрозумілішою.
Зміст проектування
Під проектування розуміють деякий уніфікований підхід за допомогою якого шукають шляхи вирішення певної проблеми. Під проектуванням розглядають побудову системи, що
- задовольняє заданим (часто неформальним) специфікаціям функціонування;
- узгоджується з обмеженнями, що накладаються обладнанням;
- задовольняє явні та неявні вимоги з експлуатації та ресурсоспоживанню;
- відповідає вимогам до самого процесу розробки, таким як тривалість та вартість, а також використання додаткових інструментальних засобів.
При проектуванні здебільшого застосовується моделювання в значній мірі тому, що воно реалізує принципи декомпозиції, абстракції та ієрархії. Кожна модель описує певну частину системи, а ми будуємо нові на базі старих, в яких ми більше чи менше впевнені.
Методи проектування володіють деякими спільними властивостями:
- умовні позначення – мова для описання кожної моделі;
- процес правила проектування моделей;
- інструменти – засоби, які пришвидшують процес створення моделей, і в яких вже закладені закони функціонування моделей. Інструменти дозволяють виявити помилки в процесі розробки.
Еволюція об’єктної моделі
Протягом розвитку мов програмування високого рівня розвилася та закріпилася тенденція переходу від імперативних (які вказують комп’ютеру, що робити) до декларативних (які описують ключові абстракції проблемної галузі) мов.
Мови програмування умовно розбиваються на чотири покоління в залежності від мовних конструкцій, що в них реалізовані.
Перше покоління FORTRAN I, ALGOL58 (математичні формули). Вони орієнтувалися в основному на інженерні та наукові розрахунки та підтримували виключно математичні абстракції.
Друге покоління FORTRAN II (підпрограми, роздільна компіляція), ALGOL60 (блокова структура, типи даних), COBOL (описання даних, робота з файлами), Lisp (обробка списків, вказівними, збирання сміття). Основна тенденція цього покоління мов розвиток алгоритмічних абстракцій.
Третє покоління PL/1 (FORTRAN + ALGOL + COBOL), ALGOL68 (строгий нащадок ALGOL60), Pascal (спрощений нащадок ALGOL60), Simula (класи, абстрагування даних) характеризується більш суттєвим кроком від підтримки машини до предметної галузі.
У четвертому поколінні було створено багато мов програмування високого рівня, але мало з них вижили. Але механізми, що були запропоновані ними втілювалися у нових версіях старіших мов. Саме тоді розвинулися об’єктні та об’єктно-орієнтовані мови Smalltalk (по новому розвинутий спадок від Simula). Ada (нащадок ALGOL68 та Pascal з елементами Simula) CLOS (об’єднав Lisp, LOOPS та Flavors), C++ (гібрид C та Simula), Eiffel (об’єднання Simula та Ada). Ці мови можна назвати зараз об’єктно-орієнтованими.
Приклад (графічний). Структура об’єктно-орієнтованої програми представляється графом а не деревом, як у випадку процедурно-орієнтованої.
В основному об’єктно-орієнтований підхід пов’язаний з такими подіями як:
- значний прогрес в галузі архітектури ЕОМ;
- розвиток мов програмування, таких як Simula, Smalltalk, Ada;
- розвиток технології програмування, що включає принципи модульності та ховання даних [];
- розвиток теорії баз даних;
- дослідження в галузі штучного інтелекту;
- досягнення філософії та теорії пізнання.
Об’єктно-орієнований підхід зарекомендував себе як ідея, що уніфікує всю комп’ютерну науку та застосовується не тільки в програмуванні, але й при проектування інтерфейсу користувача, баз даних і навіть архітектур комп’ютерів.
Об’єктно-орієнований аналіз та проектування визначають еволюційний, а не революційний розвиток проектування.
Основи об’єктної моделі
Основними принципами об’єктної моделі є: абстрагування, інкапсуляція, ієрархічність, модульність, типізація, паралелізм та збережуваність. В об’єктній моделі всі вони зібрані разом, хоча кожен з них окремо або в певних комбінаціях застосовувався і раніше.
Розглянемо основні складові об’єктно-орієнований підхіду.
Об’єктно-орієнтоване програмування – методологія програмування, що ґрунтується на поданні програми у вигляді набору об’єктів, кожен з яких є екземпляром певного класу, а класи утворюють ієрархію наслідування [Буч].
Як випливає з означення, програма буде об’єктно-орієнованою тільки тоді, коли виконуються три основні вимоги: 1) як базові елементи використовуються об’єкти, а не алгоритми; 2) кожен об’єкт є екземпляром певного класу; 3) класи організовано ієрархічно.
Мову програмування можна назвати об’єктно-орієнованою тільки тоді, якщо у ній реалізовані механізми:
- підтримки об’єктів, тобто абстракції даних, що мають інтерфейс у вигляді іменованих операцій і власні дані з обмеженням доступу до них;
- об’єкти відносяться до відповідних типів;
- типи можуть успадковувати атрибути надтипів (надкласів).
За цією класифікацією до об’єктно-орієнованих мов програмування можна віднести Smalltalk, (чистий), C++, Object Pascal, CLOS, Java (гібридні), Ada (об’єктний).
Об’єктно-орієнтоване проектування – методологія проектування, що об’єднує в собі процес об’єктної декомпозиції та прийоми представлення логічної і фізичної, а також статичної і динамічної моделей системи, що проектується [Буч].
Об’єктно-орієнтований аналіз – методологія, при якій вимоги до системи сприймаються з точки зору класів та об’єктів, що виявлені в предметній області.
За результатами OOA формуються моделі, на яких ґрунтується OOD (проектування), OOD створює фундамент для реалізації системи з використанням методології OOP.

Тема № 2: Об’єктна модель. Складові об’єктного підходу.
План
1. Парадигми програмування. Парадигма об’єктно-орієнтованого стилю.
2. Основні складові частини об’єктно-орієнтованого стилю.
3. Додаткові складові частини об’єктно-орієнтованого стилю.
Парадигми програмування
Програмісти в основному притримуються однієї мови та одного стилю програмування. Вони програмують в парадигмі, що нав’язується цією мовою, і часто обходять альтернативні підходи до мети. Їм деколи тяжко побачити переваги стилю, що краще підходить для вирішення задачі яка розв’язується.
Стиль програмування це спосіб побудови програм, що ґрунтується на певних принципах програмування та виборі відповідної мови, що робить зрозумілими програми які написані в цьому стилі [Бобров, Стетік].
На даний час вирізняються п’ять основних стилів програмування з відповідними їм абстракціями:
- процедурно-орієнтований (алгоритми);
- об’єктно-орієнтований (класи та об’єкти);
- логіко-орієнтований (цілі виражені в термінах обчислення предакатів);
- орієнтований на правила (правила “якщо-то”);
- орієнтований на обмеження (інваріантні відношення).
Не існує одного стилю, який би найкраще підходив для вирішення всіх задач. Але об’єктно-орієнтований стиль часто є архітектурним фундаментом для інших парадигм програмування.
Парадигма об’єктно-орієнтованого стилю
Концептуальною базою для об’єктно-орієнтованого є об’єктна модель, що має чотири основних елементи:
- абстрагування;
- інкапсуляція;
- модульність;
- ієрархія.
Це основні елементи, тому що без наявності одного з них модель не можна буде назвати об’єктно-орієнтованою. Крім основних розрізняють ще три додаткових елементи:
- типізація;
- паралелізм;
- збережуваність.
Їх називаються додатковими через те що, вони корисні в об’єктно-орієнтованому стилю, але не є обов’язковими.
Основні складові частини об’єктно-орієнтованого стилю
Складові частини об’єктно-орієнтованого стилю розглядаються в порядку їх важливості в концепції об’єктно-орієнтованого програмування.
Абстрагування
Зміст абстрагування. Абстрагування є одним з основних методів, що застосовується до рішення складних задач. Його основна задача відділити основне від дріб’язкового (зерно від полови).
Означення абстракції:
Абстракція виділяє суттєві характеристики деякого об’єкту, що відрізняють його від інших видів об’єктів і, таким чином, чітко визначає його концептуальні межі з точки зору спостерігача.
Вибір правильного набору абстракцій для заданої предметної області є основною задачею об’єктно-орієнтованого проектування.
Існує цілий ряд корисних абстракцій, що застосовуються для класифікації об’єктів реального світу:
- абстракція сутності (об’єкт є корисною моделлю деякої сутності в предметній області);
- абстракція поведінки (об’єкт складається з узагальненої множини операцій);
- абстракція віртуальної машини (об’єкт згруповує операції, які або використовуються вищим рівнем керування, або самі використовують деякий набір операцій нижчого рівня);
- довільна абстракція (об’єкт включає в себе набір властивостей, що не мають одна з одною нічого спільного).
Найчастіше використовують абстракцію сутності, оскільки саме вона відповідає сутностям предметної області і дозволяє тонко класифікувати об’єкти.
Для глибшого розуміння складових об’єктно-орієнтованого програмування вводиться поняття контрактної моделі [Мейер]. Зовнішня поведінка об’єкту розглядається з точки зору його контракту з іншими об’єктами, відповідно до цього повинна реалізовуватися його внутрішня структура (часто у взаємодії з іншими об’єктами). Наприклад, контракт фіксує всі зобов’язання об’єкту-серверу (надає послуги) перед об’єктом-клієнтом (користується послугами). Цей контракт визначає відповідальність об’єкту за ту поведінку, яка за ним закріплена.
Кожна операція, що передбачена цим контрактом, однозначно визначена її формальними параметрами та типом значення, що повертається. Повний набір операцій, які клієнт може виконувати над об’єктом-сервером, разом з правильним порядком їх застосування – називається протоколом. Протокол визначає всі можливі способи, якими об’єкт може діяти або піддаватися впливу, тобто визначає статичну та динамічну поведінку абстракції.
Центральною ідеєю абстракції є поняття інваріанту. Інваріант – деяка логічна умова, значення якої (істина чи хиба) повинна зберігатися. Для кожної операції об’єкту можна задавати передумови (інваріанти, що допускаються операцією) і посляумови (інваріанти, яким задовольняє операція).
Усі абстракції володіють як статичними так і динамічними властивостями. Наприклад, файл як об’єкт вимагає певного об’єму на носії. Має ім’я та вміст (статичні атрибути). В той же час, значення кожної з приведених властивостей може бути змінено в процесі використання файлу.
На відміну від інших стилів, об’єктно-орієнтований стиль пов’язаний з впливом на об’єкти. Операція над об’єктом приводить до деякої реакції цього об’єкту. Операції, що їх можна виконати над об’єктом та реакція об’єкту на зовнішні чинники визначають поведінку цього об’єкту.
Приклади абстракцій. Для прикладу розглянемо тепличне господарство де вирощують різноманітні культури. Керування режимом роботи теплиці для вирощування деякої однієї культури, а тим більше декількох складна задача. Необхідно одночасно контролювати багато параметрів які впливають на ріст культур та їх врожайність. Наприклад: температуру, вологість, освітлення, кислотність, концентрацію поживних речовин та ін.
Одна з ключових абстракцій в цій системі – давач. Повинні бути давачі температури, вологості, освітленості і т.д. Давачі розрізняються типом величини яку вони вимірюють, допустимими значеннями цієї величини, положенням в просторі (точкою де проводяться вимірювання) та іншими. Але мають спільну рису, вони повинні вимірювати значення деякої величини та інформувати систему про це значення. А вже система керування проаналізувавши виміри повинна прийняти рішення про свою поведінку.
Розглянемо тепер властивості давача як такого. Які у нього обов’язки перед клієнтом? На певному рівні абстракції давач повинен знати своє положення та біжуче значення величини яку він вимірює. Які ж дії може виконувати клієнт по відношенню до давача? На примітивному рівні, клієнт може калібрувати давач та отримувати від нього біжуче значення величини, яку той вимірює.
Проілюструємо сказане вище з використанням мови програмування С++.
// Значення величини, що вимірюється
typedef float Value;
// Число, що однозначно визначає положення давача
typedef unsigned int Location;
class Sensor {
public:
Sensor(Location);
~Sensor();
void calibrate(Value actualValue);
Value currentValue() const;
private:
. . .
};
Клас Sensor – це специфікація давача, а його начинка схована в закритій частині (за ключовим словом private). Описаний клас ще не є об’єктом. Власне об’єкти – це екземпляри класу, і їх необхідно створити. Наприклад, засобами мови С++ це записується:
Value value;
Sensor SensorFirst(1);
Sensor SensorSecond(2);
Value = SensorFirst.currentValue();
. . .
Розглянемо інваріанти, що пов’язані з операцією currentValue. Передумова виражає допущення, що давач розміщено у відповідному місці теплиці, а постумова – що давач видає дійсне значення у відповідних одиницях.
У даному прикладі не вказано яку саме величину вимірює давач, тому ця абстракція може бути базовою для довільних давачів даного типу (вихідна абстракція для наслідування).
Розглянутий давач відноситься до класу пасивних давачів. Для того, щоб отримати значення величини, що вимірюється необхідно опитати його. Іншим прикладом абстракції може бути активний давач, який за певних умов (час, відхилення від заданої величини) сам повідомляє клієнту про свій стан. У цьому випадку тільки дещо змінюється відповідальність об’єкту. Для рішення цієї проблеми є зворотній виклик (callback). Клієнт надає серверу функцію (функцію зворотного виклику), а сервер викликає її при необхідності. Код у цьому випадку запишеться:
class ActiveSensor {
public:
ActiveSensor(Location, void (*f)(Location, Value));
~ ActiveSensor();
void calibrate(Value actualValue);
void establishSetPoint(Value setPoint, Value Delta);
Value currentValue() const;
private:
. . .
};
У даній абстракції залишено клієнту можливість безпосередньо опитувати давач. Але сам давач став активним і може через функцію f повідомляти клієнта про свій стан (положення та значення величини, що вимірюється).
Інкапсуляція
Сутність інкапсуляції. При розгляді абстракції описувалася тільки поведінка об’єкту, тобто укладалися контрактні умови, що повинен виконувати об’єкт і як клієнт може взаємодіяти з об’єктом. В загальному клієнта не цікавить яким чином сервер організовано і яких зусиль він (сервер) докладає, щоб виконати умови контракту (зобов’язання) зі своєї сторони. Ніяка частина складної системи не повинна залежати від внутрішньої будови якої-небудь іншої частини [Інглас]. Абстракція допомагає обдумувати те, що робиш, а інкапсуляція дозволяє легко перебудовувати програми.
Означення інкапсуляції:
Інкапсуляція – це процес розмежування елементів об’єкту, що визначають його влаштування та поведінку; інкапсуляція послуговує для того, щоб ізолювати контрактні зобов’язання абстракції від їх реалізації.
Абстракція та інкапсуляція доповнюють одне одного: абстрагування направлено на явну поведінку об’єкту, а інкапсуляція займається внутрішнім облаштуванням. Інкапсуляція забезпечується через закриття інформації про внутрішню структуру об’єкту, маскуванням внутрішніх деталей які явно не впливають на зовнішню поведінку. Інкапсуляція таким чином, визначає чіткі межі між різними абстракціями.
Наявність в об’єктно-орієнтованому стилі абстракції та інкапсуляції зумовлює присутність в класі двох частин: інтерфейсу та реалізації. Інтерфейс відображає зовнішню поведінку об’єкту, що описує абстракцію поведінки всіх об’єктів даного класу. Внутрішня реалізація описує подання цієї абстракції та механізми за якими досягається бажана поведінка об’єкту.
Приклади інкапсуляції. Продовжимо розглядати тепличну систему. Допустимо, що в тепличному господарстві для підтримування температури на певному рівні є підсистема нагрівачів, або підсистеми для підтримування інших параметрів в актуальному стані. Причому нагрівачі можуть бути різних типів та ґрунтуватися на різних принципах дії. Клієнтів в загальному не цікавить як влаштовані нагрівачі, для них тільки важливо, щоб нагрівання відбувалося. Тоді абстрактний клас нагрівача можна описати наступним чином:
// Булевий тип
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; }
Якщо, при такому описанні класів помінявся принцип комунікації нагрівача з керуючим пристроєм (через пам’ять, паралельний порт, чи ін.), то достатньо змінити реалізацію захищеної частини, а інтерфейс залишиться незмінним.
Іншим прикладом інкапсуляції може служити абстракція файлу операційної системи. Для роботи з файлом існує ряд основних функцій: відкрити, читати, писати та закрити. Але де фізично розміщена інформація, що зв’язана з файлом і який механізм доступу до неї в загальному не розголошується. Хоча відомості про реалізацію файлової системи в загальному дозволить ефективніше оперувати з вмістом файлу.
Грамотна інкапсуляція локалізує ті особливості проекту, які можуть піддаватися частим змінам. Це дозволяє модифікувати та оптимізувати реалізацію класу без необхідності відслідковувати ці зміни в цілому проекті. Зачатки інкапсуляції закладені і в мові С через заголовкові файли в яких описувався інтерфейс модулів (бібліотек).
Модульність
Поняття модульності. В багатьох мовах програмування модульність введена як самостійна мовна концепція (зокрема в С), інтерфейс модулю відокремлений від його реалізації, що споріднює модульність та інкапсуляцію.
За межами модулю видимі тільки ті функції прототипи котрих описані в заголовковому файлі, але в межах модулю, при відповідній організації, можуть використовуватися функції про існування яких інші модулі навіть не підозрюють.
Різні дослідники характеризують її з різними відтінками. “Модульність – це поділ програми на фрагменти, що окремо компілюються, але можуть встановлювати зв’язки з іншими модулями” [Лесков]. “Зв’язки між модулями – це їх уява один про одного” [Парнас].
В різних мовах програмування механізм модульності реалізовано по різному. Інтуїтивний в С++, інтерфейс в заголовкових файлах (*.h), а реалізація у файлах програм (*.cpp) і зв’язок між модулями реалізовується за допомогою директиви #include. В Ada та Object Pascal є спеціальні ключові слова для визначення інтерфейсу модулю та його реалізації.
Правильний поділ програми на модулі є задачею такої ж складності, що й вибір базового набору абстракцій. Модулі відіграють роль фізичний контейнерів класів та об’єктів при логічному проектуванні системи. Подібно до того як комплексуються електронні компоненти у великі інтегральні схеми (ВІС).
Відповідно до парадигми об’єктно-орієнтованого програмування об’єкти та класи необхідно фізично розподілити так, щоб вони складали логічну структуру проекту. Таким чином програміст повинен знайти розумний компроміс між двома полярними тенденціями: прагненням скрити інформацію та необхідністю забезпечити видимість тих чи інакших абстракцій в декількох модулях. Тут необхідно враховувати, що зміна інтерфейсної частини модулю може привести до ланцюгової перекомпіляції всього проекту. Тому, як правило, частину реалізації, яка вже остаточно відпрацьована відносять до інтерфейсу, а ту яка ще може піддаватися інтенсивним змінам скривають.
Необхідно прагнути об’єднати в модулі тісно логічно пов’язані абстракції і мінімізувати взаємні зв’язки між модулями.
За означенням:
Модульність – це властивість системи, яка була розкладена на внутрішньо зв’язні, але слабо зв’язані між собою модулі.
Таким чином, принципи абстрагування, інкапсуляції та модульності є взаємодоповнюючими. Об’єкт логічно визначає межі визначення певної абстракції, а інкапсуляція та модульність роблять ці межі фізично непорушними.
При колективній розробці програмного забезпечення роботу розподіляють за модульним принципом. Правильний поділ системи на модулі мінімізує зв’язки між працівниками. Це особливо актуально коли програма розробляється різними групами чи підприємствами.
Приклади модульності. Засобами мови С++ інтерфейс модулю, що реалізує теплицю може бути описаний в фалі-заголовку (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
В модулі зібрані базовий та похідні від нього класи планів вирощування фруктів та овочів, які очевидно мають сильні зв’язки між собою і слабо зв’язані з іншими бібліотеками, наприклад графічного інтерфейсу користувача за допомогою яких розроблятиметься оболонка програми. Для інших модулів класи, що визначають нагрівачі та давачі невидимі і ніби не існують.
Ієрархія
Поняття ієрархії. Абстракції, інкапсуляції та модульності в загальному недостатньо для оперування зі складними об’єктами реального світу. Спрощення розуміння складних об’єктів досягається за рахунок утворення абстракцій, що мають ієрархічну структуру. Ієрархія визначається як:
Ієрархія – це впорядкування абстракцій, розклад їх за рівнями.
Основними видами ієрархічних структур є структура класів (ієрархія “is-a” [“є”]) та структура об’єктів (ієрархія “part of” [“частина ...”])
Приклади ієрархії: одиночне наслідування. Наслідування визначає між класами відношення типу “батько/нащадок”. Коли один об’єкт переймає (успадковує) від іншого (або декількох інших) об’єкту структуру та функціональні властивості. Часто підклас (нащадок) добудовує або перебудовує компоненти надкласу (батьківського).
Наприклад. Вовк – хижак – ссавець. Капуста – городина – рослина. Суматор – цифровий пристрій – мікросхема. Наслідування породжує ієрархію “узагальнення - спеціалізація”, де класи нащадки уточнюють деякі властивості батьківських класів.
Для прикладу тепличної системи із загального плану вирощування можна отримати план культивації фруктів:
// Тип урожай
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 isHarvest() const;
unsigned daysUntilHarvest() const;
Yield estimatedYield() const;
protected:
Boolean repHarvested;
Yield repYield;
};
Ця запис значить, що план FruitGrowingPlan є різновидом плану GrowingPlan. Продовжуючи спеціалізацію можна створити клас для вирощування яблук AppleGrowingPlan на основі плану вирощування фруктів.
При відсутності ієрархії кожен з програміст міг би запропонувати свою реалізацію класів і очевидно ці реалізації відрізнялися б, незважаючи на повне дотримання інтерфейсу. Наслідування дозволяє дисциплінувати розробників за рахунок забезпечення прозорості та стрункої побудови класів.
Принципи абстрагування, інкапсуляції та ієрархії перебувають між собою в здоровому конфлікті. Інкапсуляція скриває реалізацію об’єкту а наслідування вимагає забезпечення доступу до стану та функцій об’єкту праотця об’єктам нащадкам (навіть через декілька рівнів). Найбільшу гнучкість у цьому відношення забезпечується мовою програмування С++, що володіє трьома рівнями захисту інтерфейсу: закритим (private); захищеним (protected); та повністю відкритим (public).
Приклади ієрархії: множинне наслідування. Один клас може наслідувати (успадковувати) властивості декількох класів. Для прикладу з теплицею. Для одних рослини важливо знати коли дозріває насіння, а для інших, коли можна збирати плоди. Такі об’єкти можна створити застосувавши множинне наслідування. Один з базових класів буде план вирощування а інший, або клас дозрівання насіння, або клас, що описує час збирання плодів.
Множинне наслідування породжує значні проблем та неоднозначності при реалізації мов програмування, коли виникають конфлікти через перехресне наслідування - базові класи є нащадками (можливо в різних колінах) одного і того ж класу. Або в обох класах є члени з однаковими іменами. Такі конфлікти спричиняють помилки в процесі компіляції і в С++ їх треба розв’язувати вручну.
Приклади ієрархії: агрегація. Ієрархія агрегації вводиться через відношення “part of”. Агрегація присутня у всіх мовах, що підтримують групування різнотипних даних через “структури” та “записи”. Стосовно об’єктно-орієнтованого програмування агрегація дозволяє згрупувати логічно-зв’язані структури класів.
Наприклад процесор складається різнотипних але тісно зв’язаних поміж собою вузлів: регістрів; суматорів; перемножувачів; пам’яті і ін.
Типізація
Поняття типізації. Тип – точна характеристика властивостей, що включає структуру та поведінку та відноситься до деякої сукупності об’єктів [Дойч]. Можна вважати, тип та клас це однокові поняття, але в деяких мовах вони розрізняються.
Типізація – це спосіб захиститися від використання об’єктів одного класу замість іншого, або в крайньому випадку керувати таким використанням.
Сильна та слаба типізація. Мова С++ відноситься до мови зі слабим контролем типів (хоча й спостерігається тяга до їх строгого контролю) на відміну від мов Pascal та Ada, які строго контролюють типи.
При строгій типізації некоректне використання типів може виявлятися компілятором, що накладає жорсткі вимоги до застосування операцій над об’єктами та заставляє добре обдумувати структуру та взаємодію класів, але помилки в написанні програми добре локалізують на ранньому етапі розробки. І навпаки, в мовах зі слабою типізацією досить багато помилок виявляється тільки на етапі виконання програми, що ускладнює їх виявлення та коректування.
Наприклад, існує ієрархія ємностей:
class Tank { . . . };
class WaterTank : public Tank { . . . };
class GasTank : public Tank { . . . };
З позицій типізації об’єктно-орієнтованого програмування класи WaterTank та GasTank визначають різні типи, які безумовно володіють деякими оригінальними властивостями. Оскільки вони породженні від одного класу Tank, то мають і спільні властивості. Можна створювати функції, що однаково будуть обробляти об’єкти цих різних типів, що дозволяє досягнути значної гнучкості програми (узгодження за типами досягається шляхом їх приведення), але ці функції будуть коректно працювати тільки на рівні властивостей базового типу. Використання приведення типів з подальшим застосуванням оригінальних членів класу може привести до помилок в процесі виконання. Безпомилкове присвоювання можна застосовувати тільки знизу вверх за ієрархією.
Строгий контроль типів має наступні переваги [Теслер]:
- відсутність контролю типів приводить до загадкових збоїв у процесі виконання програм;
- у більшості випадків процес редагування – компіляція – відлагодження значно ускладнює програмування, і раннє виявлення помилок просто не заміниме;
- оголошення типів покращує документування програми;
- багато компіляторів генерує більш ефективний об’єктний код, коли їм явно відомі типи.
Для проектування складних систем, надійність мов зі строгою типізацією з великим запасом компенсує деяку втрату гнучкості в порівнянні зі слабо типізованими мовами.
Приклади типізації: статичне та динамічне зв’язування. Зв’язування визначає час коли імена об’єктів ставляться у відповідність типам. Статичне (або раннє) зв’язування означає, що типи всіх змінних та виразів відомі на час компіляції. Динамічне (або пізнє) зв’язування означає, що типи невідомі до часу виконання програми. Концепції типізації та зв’язування є незалежними. В різних мовах вони зустрічається в різних комбінаціях: типізація – сильна, зв’язування – статичне (Ada); типізація – сильна, зв’язування – динамічне (C++, Object Pascal); типи – відсутні, зв’язування – динамічне (Smalltalk).
Паралелізм
Поняття паралелізму. Процес (потік керування) – розглядається як фундаментальна одиниця дії в системі. Кожна задача має хоча б один такий потік. В паралельних система для одної задачі їх може бути декілька. Реальна паралельність досягається тільки в багатопроцесорних системах, а системи з одним процесором імітують паралельність за рахунок алгоритмів розділення часу процесора.
Процеси можна поділити на “важкі” та “легкі”. “Важкі” процеси виконуються в захищеному адресному (ресурсному) просторі і обмінюються даними через операційну систему, що накладно в часі. “Легкі” процеси працюють в одному ресурсному просторі, здебільшого в адресному просторі “важких” процесів.
Об’єктно-орієнтована модель добре підходить для розподілених систем, так як вона неявно розбиває програму на (1) розподілені одиниці та (2) зв’язні суб’єкти [Блек].
Паралелізм приділяє основну увагу абстрагуванню та синхронізації процесів. Для систем побудованих на основі об’єктно-орієнтованого проектування, реальний світ може бути представлено як множина об’єктів, що взаємодіють між собою, частина з яких є активними та виступає в ролі обчислювальних центрів. Тому:
Паралелізм – це властивість, що відрізняє активні об’єкти від пасивних.
Приклади паралелізму. Прикладом паралелізму може служити об’єкт ActiveSensor, що розглядався раніше. В поведінку якого закладено, періодично вимірювати задану величину та повідомляти про її критичні відхилення клієнта через функцію зворотного виклику.
В об’єктно-орієнтованому програмуванні спостерігаються три підходи до паралелізму.
По-перше, паралелізм це внутрішня властивість деяких мов програмування (Ada, Smalltalk) закладена в їх семантику (спеціальні ключові слова).
По-друге, можна використовувати бібліотеку класів, що реалізують який-небудь різновид “легкого” паралелізму (MFC).
По-третє, можна створити ілюзію паралелізму через систему переривань.
При використанні паралелізму ключовим питанням є синхронізація потоків та розподіл доступу до спільних ресурсів.
Збережувансть
Поняття збережуваності. Спектр збережуваності об’єктів охоплює:
- проміжні результати обчислення виразів;
- локальні змінні при виклику функцій;
- власні змінні, глобальні змінні та дані, що створюються динамічно;
- дані, що зберігаються між сеансами виконання програми;
- дані, що зберігаються при переході до нової версії програми;
- дані, які взагалі переживають програму.
Традиційно першими трьома рівнями (дані, що живуть незначний час) займаються мови програмування, а останніми – бази даних. Об’єктний підхід до проектування баз даних вводить єдиний підхід до проектування прикладних галузей.
Збережуваність – це не тільки проблема збереження даних. В об’єктно-орієнтованих базах даних (ООБД) є зміст зберігати класи, так щоб розробники (програмісти) могли правильно інтерпретувати ці дані.
Одною з важливих можливостей, яку прогнуть досягти при паралелізмі є збережуваність об’єктів не тільки в часі але й в просторі, так щоб їх можна було б переносити з машини на машину навіть змінюючи форму представлення об’єктів у пам’яті.
Збережуваність можна визначити як:
Збережуваність – властивість об’єкту існувати в часі, переживаючи процес, що його створив, та (або) в просторі, переміщаючись їі свого первинного адресного простору.
Тема № 3: Класи та об’єкти.
План
1. Поняття об’єкту. Характеристики об’єкту. Відношення між об’єктами.
2. Поняття класу. Характеристики класу. Відношення між класами.
3. Відношення між класами та об’єктами. Якість класів та об’єктів.
Поняття об’єкту
З точки зору системи сприйняття людини, об’єктом може бути:
- видимий предмет, або той, що сприймається відчуттям;
- дещо, що сприймається мисленням;
- дещо, на що направлена думка або дія.
Таким чином об’єкт моделює частину реальної дійсності, що нам оточує, тобто він існує в просторі та часі. Термін об’єкт вперше введено в мові Simula і застосовувався для моделювання реальності [1].
З точки зору деяких дослідників [Сміт, Токі] “Об’єкт визначає конкретний предмет, що розпізнається, одиницю чи сутність (реальну чи абстрактну), що має чітко визначене функціональне призначення в даній предметній області”. Інакше кажучи, чітко означені межі.
До об’єктів не можна віднести такі атрибути як краса, колір, емоції, час. Але перераховане - є властивостями об’єктів.
На основі розглянутого об’єкту можна дати наступне означення.
Об’єкт володіє станом, поведінкою та ідентичністю; структура та поведінка подібних об’єктів визначає загальний для них клас; терміни “екземпляр класу” та “об’єкт” взаємозамінні.
Розглянемо основні характеристики об’єктів.
Стан.
Поведінка об’єкту визначається його передісторією: важлива послідовність дій, що виконуються над об’єктом. Така залежність поведінки об’єкту від від подій та від часу пояснюється тим, що об’єктів є свій внутрішній стан. Тому можна вважати, що:
Стан об’єкту характеризується переліком (зазвичай статичним) всіх властивостей даного об’єкту і біжучим (зазвичай динамічним) значенням кожної з цих властивостей.
(приклад автомату, що торгує напоями)
До властивостей об’єкту відносяться характеристики, риси, якості чи властивості, що йому притаманні або набуваються ним, які роблять об’єкт самим собою. Властивості об’єкту є як правило статичними, так як вони складають основу об’єкту, що не змінюється. Всі властивості мають деякі значення, вони можуть бути деякими числовими значеннями (постійні характеристики), а можуть посилатися на інший об’єкт, який володіє своїм змінним станом та характеристиками.
Приклади. Реєстраційні записи про працівників.
class PersonalRecord {
public:
char *employeeName() const;
int employeeSocialSecurityNumber() const;
char *employeeDepartment() const;
protected:
char name[100];
int socialSecurityNumber;
char department[10];
float salary;
};
Можливі стани приведеного об’єкту визначається захищеними змінними.
Поведінка.
Так як об’єкти існують не ізольовано, піддаються впливу інших об’єктів або самі на них впливають, то:
Поведінка – це те, як об’єкт діє та реагує; поведінка виражається в термінах стану об’єкту та передачі повідомлень.
Інакше кажучи, поведінка об’єкту – це діяльність, що спостерігається та провіряється ззовні.
Операцією називається певний вплив одного об’єкту на інший з метою викликати відповідну реакцію. В мові програмування Smalltalk об’єкти обмінюються між собою повідомленнями, за аналогією в С++ говорять, що один об’єкт викликає функцію-член іншого. В загальному в ООП операції над об’єктами називають методами.
Передача повідомлення – це тільки одна із складових поведінки об’єкту, другою складовою є внутрішній стан об’єкту на час отримання повідомлення. (приклад торгівельного автомату). Деякі операції є пасивними, а інші приводять до зміни внутрішнього стану об’єкту, тому означення стану варто уточнити:
Стан об’єкту є сумарним результатом його поведінки.
Приклад. Чергу засобами С++ можна описати так.
class Queue {
public:
Queue ();
Queue (const Queue&);
virtual ~Queue ();
virtual Queue& operator = (const Queue&);
virtual int Queue& operator == (const Queue&) const;
int Queue& operator != (const Queue&) const;
virtual void clear();
virtual void append(const void&);
virtual void pop();
virtual void remove(int at);
virtual int length() const;
virtual int isEmpty() const;
virtual const void * front() const;
virtual int location(const void *) const;
protected:
. . .
};
Для створення об’єктів необхідно записати наступне:
Queue a, b, c, d;
Тепер над об’єктами можна виконувати операції.
a.append(&deb);
a.append(&karen);
a.append(&denise);
b = a;
a.pop();
Операції. Операція – це послуга, яку клас може надати своїм клієнтам. На практиці найбільш поширені п’ять типів операцій. Перші три:
модифікатор – змінює стан об’єкту;
селектор – зчитує стан об’єкту але не змінює його (стану);
ітератор – дозволяє отримати доступ до всіх частин об’єкту в певній послідовності.
В прикладі класу спочатку йдуть модифікатори, а потім селектори. Ще два типи є універсальними. Вони забезпечують інфраструктуру для створення та знищення об’єкту:
конструктор – створення об’єкту та його ініціалізація;
деструктор – звільнює стан об’єкту або знищує сам об’єкт.
В деяких мовах програмування можуть існувати функції вищого рівня (так звані вільні функції), що визначені поза класами, але виконують операції над класами (приклад копіювання одної черги в іншу).
Ролі та відповідальності. Набір всіх методів та вільних функцій у відношенні до певного об’єкту утворюють протокол цього об’єкту. Протокол – визначає поведінку об’єкту, що охоплює його всі статичні та динамічні аспекти. Роль – це частина протоколу (поведінки), що може здійснюватися частково незалежно. Відповідальність – це сукупність всіх послуг та всіх контрактних зобов’язань об’єкту. Таким чином, стан та поведінка об’єкту визначають ролі які він може виконувати, а ролі необхідні для виконання відповідальностей даної абстракції.
Об’єкти можуть бути активними та пасивними, володіти або ні власним потоком керування (паралелізм). Активний може бути автономним, міняти свій стан без зовнішнього впливу.
Ідентичність.
Визначення за Хошафяном та Коуплендом:
Ідентичність – це така властивість об’єкту, яка відрізняє його від усіх інших об’єктів.
Джерелом багатьох помилок в об’єктно-орієнтованому програмування є невміння розрізняти ім’я об’єкту від власне самого об’єкту. Часто путають адресу об’єкту та ідентичність. Нехай ми маємо якийсь клас DisplayItem. Створимо його екземпляри:
DisplayItem;
DisplayItem* item2 = new DisplayItem( 75, 75);
DisplayItem* item3 = new DisplayItem(100, 100);
DisplayItem* item4 = 0;
Після такого оголошення у пам’яті виникає чотири імені та три об’єкти. item1 – ім’я об’єкту, а item2 та item3 - вказівники на об’єкти. Якщо застосувати операції
item1.move(item2->Location());
item4 = item3;
item4->move(Poinr(38, 100));
то об’єкти item1 та item2 матимуть однаковий стан, але це різні об’єкти, в той же час item3 та item4 вказують на один і той же об’єкт і переміщення буде застосоване до третього об’єкту. item3 та item4 є синонімами одного об’єкту. Присвоєння
item2 = &item1;
призводить до втрати контролю на д другим об’єктом, що в свою чергу приводить до втрати пам’яті. В деяких мовах програмування (Smalltalk, Java) є збирачі сміття, що повернуть втрачену пам’ять системі, а інших (С++, Object Pascal, Ada) пам’ять не звільниться до завершення програми, що є причиною нестабільності роботи, а часто і зависання програми.
Копіювання, присвоювання та рівність. Структурна залежність проявляється тоді, коли об’єкт має декілька імен. Передача об’єктів функції через посилання дозволяє створювати ефективний код. Але деколи виникає пряма необхідність створювати копії об’єктів, це позитивно впливає на надійність програмного забезпечення, хоч і негативно відбивається на продуктивності.
Семантика С++ дозволяє керувати процесом копіювання об’єктів, тобто визначати, чи об’єкти передаватимуться через посилання, чи через повну копію. Для копіювання в класі створюється спеціальний копіюючий конструктор. Він може бути викликаний явно (як частина описання об’єкту) або неявно (при передачі об’єкту за значенням). При відсутності такого конструктора викликається конструктор за замовчування, що здійснює по-елементне копіювання об’єкту. Якщо, в об’єкт входять вказівники на інші об’єкти, то вводиться поняття “глибокого” копіювання, рекурсивно копіюються всі об’єкти на які існують посилання.
Присвоювання – це в принципі копіювання, його семантику в С++ також можна перевизначати, забезпечуючи “поверхневе” чи “глибоке” копіювання.
При порівнянні поняття “поверхневе” чи “глибоке” мають таке ж значення, як і при присвоюванні.
Час життя об’єктів. Початком життя об’єкту вважають його момент створення (виділення ділянки пам’яті), а кінцем – повернення відведеної ділянки операційній системі.
Об’єкти створюються явно та неявно. Явно створюються: при оголошенні об’єкт розміщується в стеку (item1); при використанні оператора new, пам’ять під об’єкти виділяється з “купи” (item2 та item3).
Неявно створення об’єкту відбувається коли він передається функції за значенням і С++ створює тимчасову його копію в стеку.
При знищенні об’єктів викликається відповідний деструктор, який зобов’язаний перед знищенням самого об’єкту звільнити всі ресурси (при необхідності), що виділялися об’єкту за час його існування.
Відношення між об’єктами
Зв’язки між об’єктами ґрунтуються на уяві котру один об’єкт має про іншого: операції які один над одним може виконувати та очікуваній поведінці іншого. В об’єктно-орієнтованому програмуванню ключовими типами ієрархічних відношень є: зв’язки та агрегація.
Зв’язки.
Зв’язок – це специфічне співставлення, через яке клієнт запитує послугу в об’єкту-серверу або через яке один об’єкт знаходить шлях до іншого.
a:DisplayItem
aControler aView
b:DisplayItem
Рис. Зв’язки
Приймаючи участь у зв’язку, об’єкт може виконувати одну з трьох ролей:
Актор – об’єкт може діяти на інші об’єкти, але сам ніколи не піддається впливу інших об’єктів; в деякому розумінні це відповідає поняттю активний об’єкт (aControler).
Сервер – об’єкт може тільки піддаватися впливу зі сторони інших об’єктів, але він ніколи не виступає в ролі об’єкту, що впливає на інші (b:DisplayItem).
Агент – об’єкт може виступати як в активній так і в пасивній ролі; як правило об’єкт-агент створюється для виконання операцій в інтересах якогось об’єкту-актору або агенту (a:DisplayItem, aView).
Приклад. (Процес нагрівання, підтримування певної температури)
Видимість. Для того, щоб клієнт спілкуватися з сервером, той повинен бути видимий для клієнта. Розрізняють чотири способи забезпечення видимості одного об’єкту іншим:
- сервер глобальний у відношенні до клієнта;
- сервер (або вказівник на нього) переданий клієнту як параметр операції;
- сервер є частиною клієнта;
- сервер локально породжується клієнтом в ході якоїсь операції.
Синхронізація. Передаючи повідомлення до сервера, клієнт таким чином синхронізується з ним. В послідовних задачах синхронізація забезпечується викликом методів. Але в багато потокових системах необхідні додаткові методи синхронізації, щоб уникнути взаємного блокування. При зв’язку активного об’єкту та пасивного, розглядають три підходи до синхронізації:
- послідовний – семантика пасивного об’єкту забезпечується присутністю тільки одного активного процесу;
- захищений - семантика пасивного об’єкту забезпечується присутністю багатьох потоків керування, але активні клієнти повинні домовитися і забезпечити взаємне виключення;
- синхронний - семантика пасивного об’єкту забезпечується присутністю багатьох потоків керування; взаємне виключення забезпечує сервер.
Агрегація.
На відміну від зв’язків, що забезпечують рівноправні або “клієнт-сервер” відношення між об’єктами, агрегація описує відношення цілого та частини, що є відповідним типом ієрархії об’єктів. Виходячи від цілого (агрегати), приходимо до його частин (атрибутів). Тобто, агрегація частковий випадок асоціації.
growingRamp
rampControler
h:Heater
Рис. Агрегація
Агрегація може бути фізичною (літак та його частини) та концептуальною (акціонер та його акції – у склад не входить, хоча акціонер володіє ними безроздільно).
При проектуванні, вибираючи тип відношення необхідно враховувати, що: агрегація дозволяє скрити частини в цілому; зв’язки – це слабші відношення та накладають менші обмеження на взаємодію об’єктів.
Поняття класу
Поняття класу та об’єкту дуже тісно зв’язані між собою, тому часто важко вести мову про об’єкт не опираючись на його клас. Але об’єкт визначає певну сутність, що визначена в часі та просторі, а клас визначає тільки абстракцію потенційних об’єктів.
У контексті об’єктно-орієнтованого програмування:
Клас – це деяка множина об’єктів, що мають подібну структуру та подібну поведінку.
За своєю природою клас – це генеральний контракт між абстракцією та її клієнтами. Об’єкт, про що йшлося раніше є екземпляром певного класу. На достатньо високому рівні абстракції ми здебільшого маємо справу з об’єктами, а не з класами. Якщо описувати ці ріні як класи, то можна отримати громіздку, мало придатну для повторного використання (унікальну) структуру.
Інтерфейс та реалізація. Поняття інтерфейсу та реалізації вводиться з позиції контрактного програмування. Де інтерфейс – це зовнішній вигляд класу, а реалізація – його внутрішня побудова. До інтерфейсу відноситься оголошення операцій, що повинні підтримуватися екземплярам класу, а також загальних змінних, констант, виняткових ситуацій, що уточнюють абстракцію класу. В реалізації визначають операції, що оголошені в інтерфейсі.
Інтерфейс ділиться на три частини:
- відкриту (public) – що видима для всіх клієнтів;
- захищену (protected) – що видима тільки в самому класі, його підкласам (нащадкам) та друзям;
- закриту (private) – що видима тільки класу та його друзям.
В С++ інтерфейс описується в файлах заголовків (*.h), а реалізація в файлах програм (*.cpp).
Відношення між класами
Відомі три основних відношення між класами: “узагальнення / спеціалізація” (відношення “isa”); “ціле / частина” (відношення “part of”); семантичні, змістовні відношення, асоціації. Виходячи з цього більшість об’єктно-орієнтованих мов підтримують різні комбінації наступних відношень:
- асоціація;
- наслідування;
- агрегація;
- використання;
- інстанціювання;
- метаклас.
Асоціація
Асоціація виражає смисловий зв’язок між класами. В загальному вона не має напрямку і не вказує як класи спілкуються між собою. Асоціація фіксує учасників їх ролі та потужність відношення.
Приклад товарів та продаж.
Під потужністю відношення типу асоціація розуміють кількість учасників відношення. Найчастіше зустрічаються три схеми:
- “один-до-одного”;
- “один-до-багатьох”;
- “бато-до-багатьох”.
В класах асоціації визначаються, через оголошення взаємних вказівників один на одного.
class Product;
class Sale;
class Product {
. . .
protected:
Sale * lastSale;
. . .
};
class Sale {
. . .
protected:
Product ** productSold;
. . .
};
У даному прикладі відображено зв’язок “один-до-багатьох”.
Наслідування
Наслідування – це відношення між класами, коли клас повторює структуру та поведінку іншого класу (одиночне наслідування) або інших класів (множинне наслідування).
Клас, структура та поведінка якого наслідується називається суперкласом (надкласом), а похідний від нього клас називають підкласам. Наслідування встановлює між класами відношення (ієрархію) загального та часткового.
Механізм наслідування відрізняє об’єктно-орієнтовані мови від об’єктних.
Підклас може перевизначити, розширити та звузити (використовуючи механізм інкапсуляції) поведінку базового класу.
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;
};
Класи, екземпляри яких не створюються називаються абстрактними класами. В С++ абстрактний клас визначається через наявність оголошених але не реалізованих віртуальних функцій. Доки така функція не буде реалізована компілятор не дозволить створити екземпляру цього класу.
void TelemetryData::transmit()
{
// передати id
// передати timeStamp
}
void ElectricalData::transmit()
{
TelemetryData::transmit();
// передати напругу
// передати силу струму
}
В С++ класи мають деревоподібну структуру (ліс). В інших мовах програмування (Smalltalk, Java) існує базовий клас від якого походять усі інші класи.
Поведінка підкласу може перевизначитися через зміни в реалізації методів. В Smalltalk перевизначитися може довільний метод. В С++, тільки ті методи, що позначені ключовим словом virtual, а інші ні.
Одиночний поліморфізм. Дозволяє розрізняти оператори та операції в залежності від кількості параметрів та їх типів. Стосовно до операторів така властивість називається перевантаженням оператора. Найбільш гнучко поліморфізм реалізовано в мові С++. Поліморфізм дозволяє обійтися без операторів вибору так як об’єкти самі знають свій тип.
void transmitFreshData(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.
В об’єктно-орієнтованому програмування розрізняють два види агрегації:
- за значенням, фізичне включення;
- за посиланням.
При фізичному включення обидва об’єкти створюються та знищуються одночасно. При включенні за посиланням об’єкти створюються асинхронно і один об’єкт може пережити (в часі) іншого.
Агрегацію за посиланням часто можна переплутати з асоціацією. Тут слід пам’ятати, що асоціація двох-направлений зв’язок, а агрегація одно-направлений і виражає відношення “цілого/частини”. Тобто, один з об’єктів повинен бути фізичною чи логічною (акціонер та акції) частиною іншого.
В практичному програмуванні агрегація може бути альтернативою множинного наслідування, це дозволяє обійти проблеми, що виникають при ньому.

Рис. Агрегація
Використання
У наведеному вище прикладі клас TemperatureController використовує клас TemperatureRamp, що передається його методам як параметр. Це типовий приклад використання.
Відношення використання між класами відповідає рівноправному зв’язку між їх екземплярами. У використання, перетворюється асоціація, якщо об’єкти є у відношенні “клієнт-сервер”.
Використання може також реалізовуватися при оголошенні екземплярів інших класів як локальні змінні методів цього класу.

Рис. Відношення використання
Інстанціонування
Відношення інстанціонування реалізовується через механізм параметризованих класів, що підтримується в С++ та Eiffel.
Наприклад.
Template<class Item>
class Queue {
public:
Queue ();
Queue (const Queue<Item>&);
virtual ~Queue ();
virtual Queue<Item>& operator = (const Queue<Item>&);
virtual int operator == (const Queue<Item>&) const;
int operator != (const Queue<Item>&) const;
virtual void clear();
virtual void append(const Item &);
virtual void pop();
virtual void remove(int at);
virtual int length() const;
virtual int isEmpty() const;
virtual const Item * front() const;
virtual int location(const Item *) const;
protected:
. . .
};
В наведеному прикладі елементи поміщуються та вибираються з черги через клас Item, що оголошений як аргумент шаблону.
Параметризований клас не може мати екземплярів доки не буде інстанційованим. Наприклад.
Queue<int> intQueue;
Queue<DisplayItem*> itemQueue;
Створені об’єкти intQueue та itemQueue – екземпляри зовсім різних класів, вони навіть не мають спільного надкласу. Але вони отримані з одного параметризованого класу Queue.
Інстанціонування безпечне з точки зору контролю типів. При даній реалізації компілятор вже може строго проконтролювати типи елементів, що включаються та вибираються з черги. При попередній реалізації черги через ідіому void* про контроль типів не можна було б навіть вести мови.
Щоб інстанціонувати параметризований клас необхідно використати інший клас. Через це відношення інстанціонування майже завжди приводить до відношення використання. Перед використанням параметризовані класи завжди необхідно інстанціонувати.

Рис. Інстанціонування
Метакласи
Метаклас – це клас, екземпляри якого є класами. Мета класи служать для підтримки змінних класу (які є загальними для екземплярів даного класу), операцій ініціалізації змінних класу та створення одиничного екземпляру класу.
В С++ метакласів нема, але семантика його конструкторів, деструкторів, статичних змінних та методів дозволяє реалізувати поведінку класів до тої, що забезпечується мета класами в чистих об’єктно-орієнтованих мовах (Smalltalk, CLOS).
Відношення між класами та об’єктами
Класи та об’єкти – це окремі, але тісно пов’язані між собою поняття. Кожен об’єкт є екземпляром деякого класу, а з класу може бути породжено декілька об’єктів. Класи, це статичні категорії, використовуються на етапі компіляції, а об’єкти створюються та знищуються в процесі виконання програми.
На етапі аналізу та початкових стадіях проектування вирішуються дві основні задачі:
- виявлення класів та об’єктів, що складають словник предметної області (ключові абстракції, структура класів та об’єктів);
- побудова структур, що забезпечують взаємодію об’єктів, при якій виконуються потрібні задачі (механізми реалізації, сукупність структур).
Якість класів та об’єктів
Метрика якості абстракцій. Процес виділення класів та об’єктів є послідовним та ітеративним. Тому дуже важливо з самого початку за можливістю наблизитися до правильного вирішення задачі, щоб скоротити кількість ітеративних кроків.
Для оцінки якості класів та об’єктів, що виділяються в системі, пропонуються п’ять критеріїв:
- зачеплення – це ступінь глибини зв’язків між окремими модулями, а також класами та об’єктами. Складність системи може бути зменшена за рахунок зменшення зачеплення між відповідними категоріями (модулями, класами та об’єктами). Є певне протиріччя між зачепленням та наслідуванням (зменшення зачеплення – подібність абстракцій).
- зв’язність – степінь взаємодії між елементами окремого модулю, а також класу та об’єкту. Найкращою є функціональна зв’язність, при якій елементи модулю, класу, об’єкту тісно взаємодіють для досягнення визначеної мети.
- достатність – наявність в класі або модулі всього необхідного для реалізації логічної та ефективної поведінки (мінімальний набір вимог, операцій).
- повнота – наявність в інтерфейсі класу всіх характеристик абстракції. Повнотою володіє такий клас чи модуль, що забезпечує всі засоби для ефективної взаємодії з користувачем.
- примітивність – в класі переважають операції, що потребують доступу до внутрішньої реалізації абстракцій.
Вибір операцій. Кількість методів у класі обумовлюється двома компромісами: описання поведінки в одному методі спрощує інтерфейс, але ускладнює та збільшує розміри самого методу; розщеплення методу ускладнює інтерфейс, але робить кожен з методів простішим.
Для прийняття рішення в якому з класів реалізовувати певну поведінку використовуються наступні міркування:
- повторне використання (Чи буде ця поведінка корисна в більш ніж одному контексті?);
- складність (Наскільки важко реалізувати таку поведінку?);
- застосування (Наскільки дана поведінка характерна для класу, в який вона включається?);
- знання реалізації (Чи потрібно для реалізації даної поведінки знати таємниці класу?).
В С++ є можливість деяку поведінку реалізувати як вільні функції, що не відносяться до структури класу (утиліти). Такий підхід задовольняє вимогам примітивності та зменшує зачеплення між класами.
При виборі операцій для паралельної реалізації необхідно оцінити наступні методи синхронізації при передачі повідомлень:
- синхронна (операція виконується тільки при одночасній готовності об’єктів, що його передає та приймає, очікування готовності може бути безмежно довгим);
- з врахуванням затримки (так як і синхронна, але коли об’єкт, що приймає повідомлення не готовий, то операція не виконується);
- з обмеженням за часом (так як і синхронна, але об’єкт, що посилає повідомлення буде чекати готовності іншого об’єкту, тільки певний час);
- асинхронна (операція виконується незалежно від готовності об’єкту, що приймає повідомлення).
Вибір відношень. Одним з основних аспектів при виборі відношень є доступність, здатність однієї абстракції бачити іншу та звертатися до її відкритих ресурсів. Абстракції є доступними одна одній коли перекриваються їх області видимості та надані необхідні права доступу. Таким чином, зачеплення зв’язано з видимістю.
Можна розглянути два діагональні підходи до проектування структури класів, між якими необхідно знайти розумний компроміс. Ліс (багато окремих дерев) – класи можуть вільно змішуватися та вступати у взаємовідносини. Одне дерево з гілками, що виходять від одного предка. Кожен з підходів має свої переваги та недоліки. В лісі неефективно використовується можливості узагальнення/спеціалізації. В дереві важко зрозуміти класи без детального аналізу контексту всіх їх предків.
Вибір між відношеннями наслідування, агрегації та використання базується на семантиці об’єктів предметної області.
Видимість об’єктів забезпечується чотирма основними способами:
- сервер є глобальним;
- сервер передається клієнту як параметр операції;
- сервер є частиною клієнта в змісті класів;
- сервер локально оголошується в області видимості клієнта.
Для підвищення гнучкості взаємодії між клієнтом та сервером ці варіанти можна комбінувати.
Вибір реалізації. Внутрішня будова класів розробляється тільки після уточнення проектування їх зовнішньої поведінки. При цьому вирішуються дві задачі: спосіб представлення класу або об’єкту та спосіб розміщення їх в модулі.
Представлення. Представлення класів та об’єктів майже завжди повинно бути скрито (інкапсульовано). Це дозволяє вносити зміни в реалізацію без порушення інтерфейсу. Наприклад, виділення та перерозподіл пам’яті та тимчасових ресурсів, оптимізація алгоритму обчислень в залежності вимог до бажаної швидкодії виконання певних операцій (вставка/видалення елемента).
Компромісним є також прийняття рішення про збереження параметру як атрибуту класу чи обчислення цього параметру. Перше, приводить до перевитрат пам’яті, а друге до зниження швидкості отримання результату.
У будь-якому випадку вибір представлення не повинен впливати на інтерфейс класу.
Модульна структура. При поділі програми на модулі проектувальник повинен розв’язати компроміс між вимогами видимості класів та об’єктів і ховання інформації. В загальному випадку модулі повинні бути функціонально сильно зв’язані із середини і слабо зв’язані один з одним. При цьому необхідно враховувати такі фактори як: повторне використання; безпека; придатність до документування.