→ Пошук по сайту       Увійти / Зареєструватися
Знання Програмування на Java Навчальний курс по JAVA

Основи програмування

 Java

Методологія процедурно-орієнтованого програмування

Поява перших електронних обчислювальних машин, або комп'ютерів, ознаменувало новий етап у розвитку техніки обчислень. Здавалося, достатньо розробити послідовність елементарних дій, кожну з яких можна перетворити на зрозумілі комп'ютеру інструкції, і будь-яка обчислювальна задача буде вирішена. Ця ідея виявилася настільки життєздатною, що довгий час домінувала над усім процесом розробки програм. З'явилися спеціалізовані мови програмування, створені для розробки програм, призначених для вирішення обчислювальних завдань. Прикладами таких мов можуть служити FOCAL (FOrmula CALculator) і FORTRAN (FORmula TRANslator).

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

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

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

 Procedure printGreeting (name: String)
Begin
   Write ("Hello,");
   WriteLn (name);
End;

Призначення цієї процедури - вивести на екран вітання Hello, Name, де Name передається в процедуру в якості вхідного параметра.

З часом обчислювальні завдання ставали все складнішими, а значить, і вирішальні їх програми збільшувалися в розмірах. Їх розробка перетворилася на серйозну проблему. Коли програма стає все більшою, її доводиться розділяти на все більш дрібні фрагменти. Основою для такого розбиття якраз і стала процедурна декомпозиція, при якій окремі частини програми, або модулі, представляли собою сукупність процедур для вирішення однієї або кількох завдань. Одна з основних особливостей процедурного програмування полягає в тому, що воно дозволило створювати бібліотеки підпрограм (процедур), які можна було б використовувати повторно в різних проектах або в рамках одного проекту. При процедурному підході для візуального представлення алгоритму виконання програми застосовується так звана блок-схема. Відповідна система графічних позначень була зафіксована в ГОСТ 19.701-90. Приклад блок-схеми зображений на малюнку (мал. 2.1)

рис.2 1. Приклад блок-схеми

Поява і інтенсивне використання умовних операторів і оператора безумовного переходу стало предметом гострих дискусій серед фахівців з програмування. Справа в тому, що безконтрольне застосування в програмі оператора безумовного переходу goto може помітно ускладнити розуміння коду. Такі заплутані програми порівнювали з порцією спагетті (bowl of spaghetti), маючи на увазі численні переходи від одного фрагмента програми до іншого, або, що ще гірше, повернення від кінцевих операторів програми до початкових. Ситуація здавалася настільки драматичною, що багато пропонували виключити оператор goto з мов програмування. Саме з цього часу відсутність безумовних переходів стали вважати хорошим стилем програмування.

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

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

Методологія об'єктно-орієнтованого програмування

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

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

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

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

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

рис.2 2. Семантика (сенс програми з точки зору виконання її комп'ютером) і прагматика (сенс програми з точки зору її користувачів)

Модель містить не всі ознаки і властивості репрезентованої нею предмета чи поняття, а тільки ті, які істотні для розроблювальної програмної системи. Таким чином, модель "бідніша", а отже, простіше представленого нею предмета чи поняття.

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

Об'єктно-орієнтований підхід має такі переваги, як:

  •  зменшення складності програмного забезпечення;
  •  підвищення надійності програмного забезпечення;
  •  забезпечення можливості модифікації окремих компонентів програмного забезпечення без зміни інших його компонентів;
  • забезпечення можливості повторного використання окремих компонентів програмного забезпечення.

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

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

Об'єкти

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

  • розуміння прикладної задачі (проблеми);
  • введення основи для реалізації на комп'ютері.

Приклади об'єктів: кватирка, Банк "Імперіал", Петро Сидоров, справа № 7461, ощадкнижка і т.д.

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

Граді Буч дає наступне визначення об'єкту:

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

Кожен об'єкт має стан, володіє чітко певною поведінкою та унікальною ідентичністю.

Стан

Розглянемо приклад. Будь-яка людина може знаходитися в деякому положенні (стані): стояти, сидіти, лежати, і - в той же час здійснювати будь які дії.

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

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

Для розглянутих вище прикладів такими характеристиками, або атрибутами, об'єкта "Людина" є:

  • поточне становище людини (стоїть, сидить, лежить);
  • наявність вудки (є чи ні).

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

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

Поведінка

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

  • створити;
  • відкрити;
  • читати з файлу;
  • писати у файл;
  • закрити;
  • видалити.

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

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

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

Поведінка (behavior) - дії та реакції об'єкта, виражені в термінах передачі повідомлень і зміни стану; видима ззовні і відтворна активність об'єкту.

Унікальність

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

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

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

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

Класи

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

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

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

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

рис.2 3

Відповідно до UML (Unified Modelling Language - уніфікована мова моделювання), клас має таке графічне представлення.

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

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

  • конструктор (constructor) - виконується при створенні об'єктів;
  • деструктор (destructor) - виконується при знищенні об'єктів.

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

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

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

Усередині об'єкту дані і методи можуть володіти різним ступенем відкритості (або доступності). Ступені доступності, прийняті в мові Java, докладно будуть розглянуті в лекції 6. Вони дозволяють більш тонко керувати властивістю інкапсуляції.

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

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

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

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

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

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

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

Як приклад можна розглянути задачу, в якій необхідно реалізувати класи "Легковий автомобіль" і "Вантажний автомобіль". Очевидно, ці два класи мають загальну функціональність. Так, обидва вони мають 4 колеса, двигун, можуть переміщатися і т.д. Всіма цими властивостями володіє будь-який автомобіль, незалежно від того, вантажний він чи легковий, 5 - або 12-місний. Розумно винести ці загальні властивості і функціональність в окремий клас, наприклад, "Автомобіль" і успадковувати від нього класи "Легковий автомобіль" і "Вантажний автомобіль", щоб уникнути повторного написання одного й того ж коду в різних класах.

рис.2 4

Відношення узагальнення позначається суцільною лінією з трикутною стрілкою на кінці. Стрілка вказує на більш загальний клас (клас-предок або суперклас), а її відсутність - на більш спеціальний клас (клас-нащадок або підклас).

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

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

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

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

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

У мові Java множинне успадкування має обмежену підтримку через інтерфейси і буде розглянуто в лекції 8.

Поліморфізм

Поліморфізм є одним з фундаментальних понять в об'єктно-орієнтованому програмуванні поряд зі спадкоємством та інкапсуляцією. Слово "поліморфізм" грецького походження і означає "має багато форм". Щоб зрозуміти, що воно означає стосовно об'єктно-орієнтованого програмування, розглянемо приклад.

Припустимо, ми хочемо створити векторний графічний редактор, в якому нам потрібно описати у вигляді класів набір графічних примітивів - Point, Line, Circle, Box і т.д. У кожного з цих класів визначимо метод draw для відображення відповідного примітиву на екрані.

Очевидно, доведеться написати код, який при необхідності відобразити малюнок буде послідовно перебирати всі примітиви, на момент відтворення знаходяться на екрані, і викликати метод draw у кожного з них. Людина, незнайомий з поліморфізмом, найімовірніше, створить кілька масивів (окремий масив для кожного типу примітивів) і напише код, який послідовно перебере елементи з кожного масиву і викличе у кожного елемента метод draw. У результаті вийде приблизно наступний код:

 // Створення порожнього масиву, який може
// Містити об'єкти Point з максимальним
// Об'ємом 1000
Point [] p = new Point [1000];
Line [] l = new Line [1000];
Circle [] c = new Circle [1000];
Box [] b = new Box [1000];
// Припустимо, в цьому місці відбувається
// Заповнення усіх масивів відповідними
// Об'єктами
for (int i = 0; i <p.length; i + +) {
// Цикл з перебором всіх осередків масиву.
// Виклик методу draw () у випадку,
// Якщо комірка не порожня.
   if (p [i]! = null) p [i]. draw ();
}
for (int i = 0; i <l.length; i + +) {
   if (l [i]! = null) l [i]. draw ();
}
for (int i = 0; i <c.length; i + +) {
   if (c [i]! = null) c [i]. draw ();
}
for (int i = 0; i <b.length; i + +) {
   if (b [i]! = null) b [i]. draw ();
}

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

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

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

Для описаної вище ієрархії класів, використовуючи поліморфізм, можна написати наступний код:

 ...
Point p [] = new Point [1000];
p [0] = new Circle ();
p [1] = new Point ();
p [2] = new Box ();
p [3] = new Line ();
for (int i = 0; i <p.length; i + +) {
   if (p [i]! = null) p [i]. draw ();
}

В описаному вище прикладі масив p [] може містити будь-які об'єкти, породжені від спадкоємців класу Point. При виклику будь-якого методу у будь-якого з елементів цього масиву буде виконаний метод того об'єкта, який міститься у клітинці масиву. Наприклад, якщо в комірці p [0] знаходиться об'єкт Circle, то при виклику методу draw наступним чином:

 p [0]. draw ()

вималюється коло, а не крапка.

На закінчення наведемо формальне визначення поліморфізму.

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

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

рис.2 5 Приклад ієрархії класів

Прикладом використання перевантаження методів у мові Java може служити клас PrintWriter, який застосовується, зокрема, для виведення повідомлень на консоль. Цей клас має безліч методів println, які різняться типами та / або кількості вхідних параметрів. Ось лише декілька з них:

 void println ()
   // Перехід на новий рядок
void println (boolean x)
   // Виводить значення булевою
   // Змінної (true або false)
void println (String x)
   // Виводить рядок - значення
   // Текстового параметра.

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

Типи відносин між класами

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

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

  • агрегація (Aggregation);
  • асоціація (Association);
  • успадкування (Inheritance);
  • метаклассов (Metaclass).

Агрегація

Відношення між класами типу "містить" (contain) або "складається з" називається агрегацією, або включенням. Наприклад, якщо акваріум наповнений водою і в ньому плавають рибки, то можна сказати, що акваріум агрегує в собі воду і рибок.

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

У нашому прикладі ставлення contain є двонаправленим. Об'єкт класу Aquarium містить кілька об'єктів Fish. У той же час кожна рибка "знає", в якому саме акваріумі вона живе. Кожен клас має свою роль в агрегації, яка вказує, яке місце займає клас у даному відношенні. Ім'я ролі не є обов'язковим елементом позначень і може бути відсутнім на діаграмі. У прикладі можна бачити роль home класу Aquarium (акваріум є домом для рибок), а також роль inhabitants класу Fish (рибки є мешканцями акваріума). Назва ролі звичайно збігається з назвою відповідного поля в класі. Зображення такого поля на діаграмі зайве, якщо вже вказано ім'я ролі. Тобто в даному випадку клас Aquarium матиме властивість (поле) inhabitants, а клас Fish - властивість home.

Кількість об'єктів, які беруть участь у відношенні, записується поряд з ім'ям ролі. Запис "0 .. n" означає "від нуля до нескінченності". Прийнято також позначення:

"1 .. n" - від одиниці до нескінченності;

"0" - нуль;

"1" - один;

"N" - фіксована кількість;

"0 .. 1" - нуль чи один.

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

 // Визначення класу Fish
public class Fish {
   // Визначення поля home
   // (Посилання на об'єкт Aquarium)
   private Aquarium home;
   public Fish () {
   }
}
// Визначення класу Aquarium
public class Aquarium {
   // Визначення поля inhabitants
   // (Масив посилань на об'єкти Fish)
   private Fish inhabitants [];
   public Aquarium () {
   }
}

Асоціація

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

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

У даному випадку між екземплярами класів Programmer і Computer в обидві сторони використовується відношення "0 .. n", тому що програміст, в принципі, може не працювати з комп'ютером (якщо він теоретик або на пенсії). У свою чергу, комп'ютер може ніким не використовуватися (якщо він новий і ще не встановлений).

Код, відповідний розглянутому наприклад, буде, наприклад, таким:

 public class Programmer {
   private Computer computers [];
   public Programmer () {
   }
}
public class Computer {
   private Programmer programmers [];
   public Computer () {
   }
}

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

Спадкування є важливим випадком відносин між двома або більше класами. Докладно вона розглядалася вище.

Метакласи

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

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

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

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

Отже, об'єкти породжуються від класів, а класи - від метаклассов. Він, як правило, в системі тільки один. Але існують мови програмування, в яких можна створювати і використовувати власні метакласи, наприклад мова Python. Зокрема, функціональність метаклассов може бути наступна: при формуванні класу він буде переглядати список усіх методів в класі і, якщо ім'я методу має вид set_XXX або get_XXX, автоматично створювати поле з ім'ям XXX, якщо такого не існує.

Оскільки метаклас сам є класом, то немає ніякого сенсу у створенні "мета-мета-класів".

У мові Java також є метакласи. Це клас, який так і називається - Class (описує класи), він розташовується в основний бібліотеці java.lang. Віртуальна машина використовує його за прямим призначенням. Коли завантажується черговий. Class-файл, що містить опис нового класу, JVM породжує об'єкт класу Class, який буде зберігати його структуру. Таким чином, Java використовує концепцію метаклассов в самих практичних цілях. За допомогою Class реалізована підтримка статичних (static) полів і методів. Нарешті, цей клас містить ряд методів, корисних для розробників. Вони будуть розглянуті в наступних лекціях.

Переваги ООП

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

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

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

Поліморфізм виявляється корисним переважно в наступних ситуаціях.

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

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

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

Недоліки ООП

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

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

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

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

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

  1. Неефективність на етапі виконання. У мовах типу Smalltalk повідомлення інтерпретуються під час виконання програми шляхом здійснення їх пошуку в одній або кількох таблицях і за рахунок вибору відповідного методу. Звичайно, це повільний процес. І навіть при використанні найкращих методів оптимізації Smalltalk-програми в десять разів повільніше оптимізованих C-програм.У гібридних мовах типу Oberon-2, Object Pascal і C + + відправка повідомлення призводить лише до виклику через покажчик процедурної змінної. На деяких машинах повідомлення виконуються лише на 10% повільніше, ніж звичайні процедурні виклики. І оскільки повідомлення зустрічаються в програмі набагато рідше за інших операцій, їх вплив на час виконання впливу практично не робить.Однак існує інший фактор, який впливає на час виконання: інкапсуляція даних. Рекомендується не надавати прямий доступ до полів класу, а виконувати кожну операцію над даними через методи. Така схема приводить до необхідності виконання процедурного виклику кожен раз при доступі до даних. Однак якщо інкапсуляція використовується тільки там, де вона необхідна (тобто в тих випадках, коли це стає перевагою), то уповільнення цілком прийнятне.
  2. Неефективність в сенсі розподілу пам'яті. Динамічне зв'язування і перевірка типу на етапі виконання вимагають по ходу роботи інформації про тип об'єкта. Така інформація зберігається в дескрипторі типу і він виділяється один на клас. Кожен об'єкт має невидимий покажчик на дескриптор типу для свого класу. Таким чином, в об'єктно-орієнтованих програмах необхідна додаткова пам'ять виявляється в одному покажчику для об'єкта і в одному дескрипторі типу для класу.
  3. Зайва універсальність. Неефективність також може означати, що в програмі реалізовані надлишкові можливості. У бібліотечному класі часто міститься більше методів, ніж це реально необхідно. А оскільки зайві методи не можуть бути вилучені, вони стають мертвим вантажем. Це не впливає на час виконання, але позначається на розмірі коду.

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

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

Висновок

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

загрузка...
Теми розділу
Сторінки, близькі за змістом