
Лекція 1. Повторення основних принципів ооп Класи. Ролі класу
Розробка об’єктно-орієнтованої парадигми була викликана труднощами, що мали місце в практиці розробки стандартного структурного програмування.
Метод функціональної декомпозиції – це основний підхід розробки структурного програмного забезпечення (ПЗ). Цей метод передбачає, що розроблювач розбиває (піддає декомпозиції) проблему на кілька функціональних етапів, що забезпечують її рішення. Розроблювач діє так тому, що простіше мати справу з невеликими частинами, чим із усією проблемою в цілому. Основна проблема методу функціональної декомпозиції полягає в тому, що він не дозволяє будь-яким чином підготувати текст програми до наступних змін, що з'являються в міру її поступової еволюції. Найчастіше подібні зміни пов'язані з необхідністю врахувати новий варіант поводження програми у вже існуючій системі. Якщо всю логіку реалізації обраної поетапної схеми помістити в одну велику функцію або єдиний модуль, то кожна зміна кожного з етапів буде потребувати внесення змін у цю функцію або модуль. З іншого боку, внесення змін зазвичай підвищує небезпеку появи помилок або інших небажаних наслідків. Інакше це можна сформулювати в такий спосіб.
1. Безліч помилок з'являються при внесенні в текст програми змін. Усякий раз, коли в текст програми необхідно внести зміни, виникає побоювання, що зміна тексту програми в одному місці може привести до збою в іншому. Чому ж це відбувається? Чи повинен програміст аналізувати всі наявні функції і звертати увагу на спосіб їхнього використання? Чи необхідно йому враховувати, як функції можуть взаємодіяти одна з одною? Чи зобов'язаний він перевіряти всю безліч різних подробиць, зв'язаних з кожною функцією, реалізовану цією функцією логіку, компоненти з якими ця функція взаємодіє, дані, що вона використовує.
2. Проблема формулювання вимог до створюваного програмного забезпечення. Не має значення, скільки зусиль було прикладено, наскільки ретельно був проведений аналіз - користувач просто не може сформулювати всі необхідні вимоги відразу. Занадто багато невідомого несе в собі майбутнє. Часи міняються. І так було завжди...Ніщо не може запобігти настанню змін. Однак це не значить, що до їхнього приходу не можна підготуватися.
Будь-яке опитування серед розроблювачів програмного забезпечення про якість наданих замовником вимог до створюваного програмного продукту навряд чи дасть велику розмаїтість відповідей. Швидше за все вони будуть наступними.
- Вимоги є неповними.
- Вимоги по більшій частині помилкові.
- Вимоги (і користувачі) суперечливі
- Вимоги не описують поставлену задачу докладно.
Практично немає шансів одержати відповідь, подібну наступний: "надані вимоги не тільки вичерпно повні, ясні і зрозумілі, але також включають опис усіх функціональних можливостей, що будуть потрібні замовникові протягом п'яти наступного років!"
Вимоги до програмного забезпечення завжди змінюються, більшість розроблювачів знають цю обставину і вважають її досить неприємною. Але лише деякі з них дійсно враховують можливість зміни існуючих вимог при написанні програм. Зміна вимог до програмного забезпечення відбувається внаслідок ряду простих причин:
- Погляди користувачів на їхні потреби змінюються як у результаті обговорень з розроблювачами, так і при ознайомленні з новими можливостями в області програмного забезпечення.
- Представлення розроблювачів про предметну область міняється в міру створення ними програмного забезпечення, призначеного для її автоматизації, оскільки їм стають відомі додаткові особливості цієї області.
- З'являються все нові обчислювальні середовища, призначені для розробки програмного забезпечення.
Все це зовсім не значить, що можна опустити руки і відмовитися від збору повноцінних вимог користувачів до створюваної системи. Навпроти, це значить, що необхідно навчитися пристосовувати створюваний програмний код до неминучого внесення змін. Це також виходить, що необхідно припинити обвинувачувати себе (або замовників) у тому, що є зовсім закономірним.
Щоб цілком зрозуміти зміст усього вищесказаного, уведемо деяку спеціальну термінологію. Мартін Фулер (Martіn Fowler) у книзі UML Dіstіlled описує три різних підходи до процесу розробки програмного забезпечення. Вони представлені в табл.
Таблиця 1.1. Різні підходи до процесу розробки програмного забезпечення
На концептуальному рівні |
При цьому підході виявляються основні концепції досліджуваної предметної області, концептуальна модель може незначно або навіть узагалі не стосуватися питань її реалізації в створюваному ПЗ |
На рівні спецификацій |
У цьому випадку проектується саме програмне забезпечення, але лише з погляду його інтерфейсної частини, а не повної реалізації |
На рівні реалізації |
На цьому етапі створюється текст програм. У більшості випадків саме цей підхід сприймається як основний, але найчастіше виявляється виправданим попереднє звертання до підходів попередніх типів. |
Об’єктно - орієнтована розробка програмної системи заснована на стилі, називаному проектуванням від даних. Проектування системи зводиться до пошуку абстракцій даних, що підходять для конкретної задачі. Кожна з таких абстракцій реалізується у вигляді класу, що і стає модулем - архітектурною одиницею побудови системи. В основі класу лежить абстрактний тип даних.
Об’єктно - орієнтована розробка програмної системи зосереджена на об'єктах, тому текст створюваних програм також будується на використанні об'єктів, а не функцій. Що ж таке об'єкт? Традиційно об'єкт розглядається як сукупність даних і методів (об’єктно-орієнтований термін для функцій). У класу дві різні ролі: модуля і типу даних. Клас - це модуль, архітектурна одиниця побудови програмної системи. Клас - це тип даних, що задає реалізацію деякої абстракції даних, характерної для проблемної області, в інтересах якої створюється програмна система.
Перевага використання об'єктів полягає в тому, що на них покладається відповідальність за їхню власну поведінку. Спочатку об'єкти відносять до визначеного типу. Дані об'єкта дозволяють йому визначати свій стан, а програмний код об'єкта забезпечує його коректне функціонування (тобто виконання тих дій, для яких він, власне, призначений).
На концептуальному рівні об'єкти виступають як сукупність зобов'язань.
На рівні визначення специфікацій об'єкти розглядаються як набір методів, що можуть викликатися іншими об'єктами або цим же об'єктом.
На рівні реалізації об'єкти розглядаються як сукупність програмного коду і даних.
На жаль, вивчення й обговорення методів об’єктно-орієнтованого проектування найчастіше ведеться тільки з погляду рівня реалізації - у термінах програмного коду і даних, а не з позицій концептуального рівня або рівня визначення специфікацій.
Оскільки об'єкти мають обов'язки і несуть повну відповідальність за свої дії, обов'язково повинний існувати спосіб керувати ними - тобто вказувати, що необхідно зробити. Не забувайте, що об'єкт містить дані, що описують його поточний стан, і методи, що реалізують його функціональність. Деякі методи об'єкта ідентифікуються як методи, доступні для інших об'єктів. Набір цих методів визначає відкритий інтерфейс об'єкта (publіc іnterface).
Клас є визначенням лінії поведінки об'єкта. Він містить повний опис наступних елементів:
- усіх елементів даних, що входять до складу об'єкта;
- усіх методів, що об'єкт здатний виконувати;
- необхідних способів доступу до наявних елементів даних і методам.
У класу дві різні ролі: модуля і типи даних. Клас - це модуль, архітектурна одиниця побудови програмної системи. Клас - це тип даних, що задає реалізацію деякої абстракції даних, характерною для проблемної області, в інтересах якої створюється програмна система.
Об'єктно-орієнтована розробка програмної системи заснована на стилі, званому проектуванням від даних. Проектування системи зводиться до пошуку абстракцій даних, придатних для конкретного завдання. Кожна з таких абстракцій реалізується у вигляді класу, який і стає модулем - архітектурною одиницею побудови нашої системи. В основі класу лежить абстрактний тип даних.
Оскільки елементам даних, що утримується в об'єкті, можна привласнювати різні значення, об'єкти того самого класу будуть містити різні набори конкретних значень даних, але, у той же час, володіти однієї і тією же функціональністю (реалізованої методами). Щоб одержати доступ до об'єкта, попередньо варто повідомити програму, що необхідний новий об'єкт визначеного типу (тобто того класу, до якого відноситься цей об'єкт). Створюваний новий об'єкт одержує назву екземпляра класу. Створення екземпляра класу називається його реалізацією. Абстрактні класи визначають поведінку інших, родинних їм класів. Ці "інші" класи представляють специфічний тип родинного поводження. Такий клас зазвичай називають конкретним класом, оскільки він представляє особливу визначену, фіксовану реалізацію загальної концепції.
Зі спадкуванням тісно зв'язаний ще один важливий механізм проектування сімейства класів - механізм абстрактних класів.
Визначення. Клас називається абстрактним, якщо він має хоча б один абстрактний метод.
Визначення. Метод називається абстрактним, якщо при визначенні методу задана його сигнатура, але не задана реалізація методу.
Оголошення абстрактних методів і абстрактних класів має супроводжуватися модифікатором abstract. Оскільки абстрактні класи не є повністю визначеними класами, не можна створювати об'єкти абстрактних класів. Абстрактні класи можуть мати нащадків, які частково або повністю реалізують абстрактні методи батьківського класу. Абстрактний метод є віртуальним методом, перевизначеним нащадком, тому до них застосовується стратегія динамічного зв'язування.
На концептуальному рівні абстрактні класи - це просто шаблони для створення інших класів. Отже, за допомогою абстрактного класу ми просто привласнюємо власну назву деякій множині родинних класів. Це дозволить нам надалі розглядати дану множину як єдину концепцію.
Оскільки об'єкти несуть повну відповідальність за власну поведінку, існує безліч елементів, доступ до яких з боку інших об'єктів вони дозволяти не зацікавлені. Раніше вже згадувалося поняття відкритий інтерфейс (publіc іnterface) - воно охоплює всі методи об'єкта, доступні для будь-яких інших об'єктів. В об’єктно-орієнтованих системах існує кілька рівнів доступу до елементів об'єктів.
Можливими модифікаторами в оголошенні класу можуть бути модифікатори abstract, virtual і чотири модифікатора доступу, два з яких - private і protected - можуть бути задані тільки для вкладених класів. За замовчуванням клас має атрибут доступу internal. Щоб зробити клас доступним не тільки класам одного проекту, його явно потрібно оголосити з атрибутом public. Так що в простих випадках оголошення класу може виглядати так С#:
public class Rational { тіло_класу }
У тілі класу можуть бути оголошені: константи; поля; конструктори і деструктори; методи; події; класи (структури, делегати, інтерфейси, перерахування).
Поля класу
Основу будь-якого класу, що представляє тип даних, складають його конструктори, поля і методи. Поля класу синтаксично є звичайними змінними (об'єктами) мови програмування. Їх опис задовольняє звичайні правила оголошення змінних.
Модифікатор private
Модифікатор private є атрибутом доступу. Він закриває поля від усіх інших класів, дозволяючи прямий доступ до них (читання і запис) тільки методам самого класу.
Модифікатор protected
Цей модифікатор відкриває поля класам спадкоємцям. Якщо клас A оголосив деяке поле з модифікатором protected, то методи класу B, який є спадкоємцем класу A і, таким чином, успадковує поля класу A, можуть безпосередньо працювати з успадкованими полями.
Механізми зміни доступу до полів. Методи-властивості
Методи, називані властивостями (Propertіes), представляють спеціальну синтаксичну конструкцію, призначену для забезпечення ефективної роботи з властивостями. Правильною стратегією є закриття полів від клієнта – поля з'являються з модифікатором protected або prіvate. Клієнти класу не повинні використовувати інформацію про те, як улаштовані поля. Це полегшує можливу модифікацію класу в майбутньому. Клас зможе змінити представлення даних, зберігши інтерфейс, наданий клієнтам. У цьому випадку зміни в полях не відіб'ються на клієнтах.
Закриття полів не означає, що клієнти класу не можуть працювати з даними, що зберігаються в полях класу. Можливі різні стратегії доступу клієнта до закритих полів класу: п'ять найбільш уживаних стратегій:
-читання, запис ( Read, Wrіte );
-читання, запис при першому звертанні ( Read, Wrіte-once );
-тільки читання ( Read-only );
-тільки запис ( Wrіte-only );
-ні читання, ні запису ( Not Read, Not Wrіte ).
Забезпечуючи інкапсуляцію, все-таки таки необхідно мати можливість використовувати значення полів класу, для цього можна застосувати механізм використання методів-властивостей. Розглянемо загальний синтаксис методів-властивостей. Нехай name - це закрита властивість. Тоді для неї можна визначити відкритий метод-властивість (функцію), що повертає той же тип, що і поле name. Ім'я методу зазвичай близько до імені поля, відрізняючися від нього, наприклад, тільки заголовною буквою ( Name ). Тіло властивості містить два методи - get і set, один із яких може бути опущений. Метод get повертає значення закритого поля, метод set установлює значення, використовуючи значення, передане йому в момент виклику і збережене в службовій перемінній зі стандартним ім'ям value. Оскільки get і set - це звичайні процедури мови, програмно можна реалізувати як завгодно складні стратегії доступу.
Практичний приклад створення методів-властивостей:
Розглянемо клас Person, у якого п'ять полів: fam, status, salary, age, health, що характеризують, відповідно, прізвище, статус, зарплату, вік і здоров'я персони. Усі поля закриті для клієнта, так що клієнт не може безпосередньо читати або записувати дані в поля класу. Для кожного з цих полів може бути створено своя стратегія доступу. При проектуванні класу будемо припускати, що вік доступний для читання і запису, прізвище можна задати тільки один раз, статус можна тільки читати, зарплата недоступна для читання, а здоров'я закрите для доступу і тільки спеціальні методи класу можуть повідомляти деяку інформацію про здоров'я персони. От як на C# можна забезпечити ці стратегії доступу до закритих полів класу (Лістинг 4.1):
Лістинг 4.1:
public class Person
{
public enum Status
{ ребенок, школьник, студент, работник, пенсионер
}
//поля (все закрыты)
string fam = "", health = "";
int age = 0, salary = 0;
Status status = Status.работник;
//методы - свойства
/// <summary>
///стратегия: Read,Write-once (Чтение, запись при первом обращении)
/// </summary>
public string Fam
{
set { if (fam == "") fam = value; }
get { return (fam); }
}
/// <summary>
///стратегия: Read-only(Только чтение)
/// </summary>
public Status GetStatus
{
get { return (status); }
}
/// <summary>
///стратегия: Read,Write (Чтение, запись)
/// </summary>
public int Age
{
set
{
age = value;
//Изменение статуса
if (age < 7) status = Status.ребенок;
else if (age < 17) status = Status.школьник;
else if (age < 22) status = Status.студент;
else if (age < 65) status = Status.работник;
else status = Status.пенсионер;
}
get { return (age); }
}
/// <summary>
///стратегия: Write-only (Только запись)
/// </summary>
public int Salary
{
set { salary = value; }
}
}
Методи класу
Всі процедури і функції, оголошені в класі, є методами класу. Їх опис задовольняють звичайні правила синтаксису. Методи містять описи операцій, доступні над об'єктами класу, визначаючи, тим самим, поведінку об'єктів. Всі об'єкти одного класу мають один і той же набір методів.