Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методичка КР.doc
Скачиваний:
0
Добавлен:
01.04.2025
Размер:
185.34 Кб
Скачать

5.1.1. Розширення класу

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

Якщо підклас не перевизначає (override) поведінку базового, то він успадковує всі властивості базового, оскільки, як уже говорилося, розширений клас успадковує поля і методи базового.

Class Point {

public double x, y;

public void clear() {

this.x = 0;

this.y = 0;

}

}

class Pixel extends Point {

Color color;

public void clear() {

super.clear();

color = null;

}

}

5.1.2. Створення об'єктів (екземплярів класу)

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

Аналогічно змінним примітивних типів, змінні посилальних типів також перед своїм використанням повинні отримати певні значення. Для оголошеної змінної типу класу необхідно створити конкретний об'єкт, екземпляр (instance) описаного класу. У мові Java об'єкти створюються за допомогою виразів, в яких використовується ключове слово new. Створені на основі визначення об'єкти часто називають екземплярами даного класу.

При створенні об'єкта змінні в різних об'єктах класу мають однакове ім'я, але можуть мати різні значення, причому зміна на нове значення змінної в одному екземплярі ніяк не впливає на значення тієї ж змінної в іншому екземплярі, оскільки в кожному об'єкті для цих змінних виділяється своя область пам'яті. Тобто, кожен об'єкт класу звертається до методів класу незалежно від інших об'єктів. Тому такі змінні і методи називаються змінними і методами екземпляра класу (instance variables and methods) або змінними і методами об'єкта.

5.1.3. Взаємодія програмних об'єктів

Загалом, об'єкт може бути розділений на дві частини: зовнішню і внутрішню. Зовнішню частину, яку часто називають відкритий інтерфейс (не плутати з класом інтерфейс), складають методи, які здійснюють взаємодію з іншою частиною програми. Внутрішню частину складають дані і методи, доступні тільки всередині об'єкта. Таке приховування («затінення») даних і методів всередині об'єкта називається інкапсуляцією. Тобто інкапсуляція - це процес упаковки даних об'єкта разом з його методами. Результатом інкапсуляції є запобігання небажаного доступу ззовні до даних і методів всередині об'єкта і можливість зміни внутрішньої реалізації об'єкта без зміни інших частин програми. Саме ця можливість і є одним з найважливіших переваг об'єктно-орієнтованого підходу в цілому, і мови програмування Java зокрема.

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

public class Human {

public int age;

}

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

Через якийсь час може виникнути ситуація, що цілочисельного типу даних вже недостатньо, і хотілося б змінити тип поля на дробовий. Однак якщо просто змінити int на double, то незабаром всі розробники, які користувалися класом Human і його полем age, виявлять, що в їхньому коді з'явилися помилки, тому що поле раптом стало дробовим, і в рядках, подібної цієї:

Human h = getHuman();

int i=h.age; // Помилка!!

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

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

Навпаки, якби поле було оголошено як private (іншими словами - це поле було «сховано» від доступу ззовні), а для читання і зміни його значення були б введені додаткові методи, то ситуація змінилася б в корені:

public class Human {

private int age;

// метод, який повертає значення age

public int getAge() {

return age;

}

// метод, який встановлює значення age

public void setAge(int a) {

age=a;

}

}

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

Human h = getHuman();

int i=h.getAge(); // Звернення через метод

Розглянемо, як виглядає процес зміни типу поля age:

public class Human {

// поле отримує новий тип double

private /*int*/ double age;

// старі методи працюють с округленням значення

public int getAge() {

return (int)Math.round(age);

}

public void setAge(int a) {

age=a;

}

// додаються нові методи для роботы з типом double

public double getExactAge() {

return age;

}

public void setExactAge(double a) {

age=a;

}

}

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

Приклад використання:

Human h = getHuman();

int i=h.getAge(); // Коректно

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

Human h = getHuman();

double d=h.getExactAge(); // Точне значення віку

Отже, в клас була додана нова можливість, але вона не вимагала ніяких змін вже написаного коду в інших класах.

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

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

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

public void setAge(int a) {

if a>=0) {

age=a;

}

}

У цьому прикладі поле age ніколи не прийме некоректне негативне значення. (Недоліком наведеного прикладу є те, що у разі неправильних вхідних даних, вони просто ігнорується, немає ніяких повідомлень, які дозволяють дізнатися, що зміни поля віку насправді не відбулося; для повноцінної реалізації методу необхідно використовувати роботу з помилками в Java).

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

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