1. Побудова ієрархії: від абстракції до деталей
Реалізація абстракцій та ієрархій — це спосіб структурувати код від загальних правил до конкретних деталей. Спочатку ви описуєте, що мають уміти всі обʼєкти (абстракція), а потім уточнюєте, як саме це реалізує кожен конкретний клас.
Як і в житті, у програмуванні все починається з питань. Наприклад: «Що спільного між колом і прямокутником?» Відповідь: обидва — фігури. А що спільного у фігур? Зазвичай у них є площа, і їх можна намалювати.
У Java це реалізується через abstract‑клас:
public abstract class Shape {
public abstract double area();
public abstract void draw();
}
Це означає:
- Будь-яка фігура повинна вміти обчислювати свою площу (area()).
- Будь-яка фігура повинна вміти намалюватися (draw()).
- Як саме вона це робить — поки що неважливо.
Тепер створимо конкретні фігури:
public class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double area() {
return Math.PI * radius * radius;
}
@Override
public void draw() {
System.out.println("Малюємо коло радіусом " + radius);
}
}
public class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
public double area() {
return width * height;
}
@Override
public void draw() {
System.out.println("Малюємо прямокутник " + width + "x" + height);
}
}
Що ми зробили?
- Виділили спільне в абстрактний клас.
- Деталізували поведінку в нащадках.
Схематично:
. Shape
/ \
Circle Rectangle
Таблиця: що реалізовано де
| Клас | area() | draw() | Власні поля |
|---|---|---|---|
| Shape | |
|
- |
| Circle | реалізовано | реалізовано | |
| Rectangle | реалізовано | реалізовано | |
2. Чому це зручно? (і чому це взагалі працює)
Єдиний інтерфейс для роботи з різними обʼєктами
Припустімо, у вас є колекція фігур:
Shape[] shapes = {
new Circle(5),
new Rectangle(3, 4),
new Circle(2.5)
};
Ви можете перебрати їх однаково, не думаючи про тип:
for (Shape shape : shapes) {
shape.draw();
System.out.println("Площа: " + shape.area());
}
Нехай JVM сама розбирається, де там коло, а де прямокутник! Це і є поліморфізм (ви вже знайомі з ним і ще розглянете його детальніше — у наступному блоці).
Легкість розширення
Хочете додати трикутник? Просто створюєте новий тип — старий код не змінюєте: Triangle extends Shape.
public class Triangle extends Shape {
private double base, height;
public Triangle(double base, double height) {
this.base = base;
this.height = height;
}
@Override
public double area() {
return 0.5 * base * height;
}
@Override
public void draw() {
System.out.println("Малюємо трикутник: основа " + base + ", висота " + height);
}
}
Весь інший код (наприклад, перебирання списку фігур) змінювати не потрібно.
Уникнення дублювання коду
Якщо у всіх фігур зʼявиться спільна властивість (наприклад, колір), її зручно винести в абстрактний клас:
public abstract class Shape {
private String color = "чорний";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract double area();
public abstract void draw();
}
Тепер будь‑який нащадок — хоч коло, хоч трикутник — отримає колір «у спадок».
3. Практика: розробляємо міні‑графічний редактор
Спробуймо зібрати усе разом. Уявімо, що ви робите простенький графічний редактор.
Абстрактний клас Figure
public abstract class Figure {
private String color = "black";
public String getColor() { return color; }
public void setColor(String color) { this.color = color; }
public abstract void draw();
public abstract void resize(double factor);
}
Конкретні фігури
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("Малюємо лінію довжиною " + length + " кольором " + getColor());
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("Нова довжина лінії: " + length);
}
}
public class Ellipse extends Figure {
private double a, b;
public Ellipse(double a, double b) {
this.a = a;
this.b = b;
}
@Override
public void draw() {
System.out.println("Малюємо еліпс з осями " + a + " і " + b + " кольором " + getColor());
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("Нові осі еліпса: " + a + ", " + b);
}
}
Хочете додати новий інструмент, наприклад Polygon? Просто створіть новий клас — увесь код редактора працює через абстрактний Figure.
Використання в коді
Figure[] figures = {
new Line(10),
new Ellipse(5, 3)
};
for (Figure figure : figures) {
figure.setColor("red");
figure.draw();
figure.resize(1.5);
}
Виведення:
Малюємо лінію довжиною 10.0 кольором red
Нова довжина лінії: 15.0
Малюємо еліпс з осями 5.0 і 3.0 кольором red
Нові осі еліпса: 7.5, 4.5
Візуалізація ієрархії
. Figure
/ \
Line Ellipse
4. Як уникнути дублювання: спільні поля та методи
Іноді у всіх нащадків є не лише спільні методи, а й спільні поля (наприклад, координати центру). Абстрактний клас — ідеальне місце для цього:
public abstract class Figure {
private double x, y; // координати центру
public Figure(double x, double y) {
this.x = x;
this.y = y;
}
public void moveTo(double newX, double newY) {
x = newX;
y = newY;
System.out.println("Фігуру переміщено в точку (" + x + ", " + y + ")");
}
public abstract void draw();
}
Тепер будь‑які Line або Ellipse можуть переміщуватися, не реалізовуючи цей метод заново.
5. Ще один приклад: платіжні системи
Абстракція — це не лише про фігури! Уявімо, що ви пишете систему обробки платежів.
Абстрактний клас Payment
public abstract class Payment {
public abstract void process();
}
Конкретні реалізації
public class CreditCardPayment extends Payment {
@Override
public void process() {
System.out.println("Обробка платежу з кредитної картки");
}
}
public class PaypalPayment extends Payment {
@Override
public void process() {
System.out.println("Обробка платежу через PayPal");
}
}
Використання
Payment[] payments = {
new CreditCardPayment(),
new PaypalPayment()
};
for (Payment payment : payments) {
payment.process();
}
Виведення:
Обробка платежу з кредитної картки
Обробка платежу через PayPal
6. Переваги такого підходу
- Єдиний інтерфейс: можна працювати з різними обʼєктами однаково.
- Розширюваність: додавання нових видів обʼєктів не потребує переписування старого коду.
- Мінімум дублювання: спільне винесено в базовий абстрактний клас.
- Гнучкість: можна використовувати колекції абстрактних типів, не переймаючись деталями.
7. Приклад із життя: транспорт
Абстракція трапляється не лише в підручниках. Наприклад, якщо ви проєктуєте систему для керування транспортом:
public abstract class Transport {
public abstract void move();
public abstract void fuelUp();
}
Конкретні види транспорту реалізують деталі:
public class Car extends Transport {
@Override
public void move() {
System.out.println("Автомобіль їде дорогою");
}
@Override
public void fuelUp() {
System.out.println("Заправляємо бензином");
}
}
public class Bicycle extends Transport {
@Override
public void move() {
System.out.println("Велосипед рухається завдяки педалям");
}
@Override
public void fuelUp() {
System.out.println("Велосипед не потребує пального, лише бутербродів для велосипедиста!");
}
}
8. Корисна схема: як будувати ієрархію абстракцій
[Абстрактний клас]
|
[Конкретний підклас]
|
[Ще більш конкретний підклас] (якщо потрібно)
- Усе спільне — нагорі!
- Усе унікальне — внизу!
9. Типові помилки під час реалізації абстракцій та ієрархій
Помилка № 1: Дублювання коду в підкласах.
Якщо ви раптом помітили, що в кожному підкласі пишете одні й ті самі поля або методи — це сигнал, що їх варто винести в абстрактний клас. Не бійтеся робити абстракцію «ширшою», якщо це зменшить дублювання.
Помилка № 2: Порушення принципу «від загального до часткового».
Іноді програмісти‑початківці починають будувати ієрархію «від деталей», забуваючи про спільне. У результаті зʼявляються дивні класи на кшталт RedCircleWithShadow, які погано вписуються у загальну структуру. Завжди спочатку виділяйте абстракцію, а потім деталі.
Помилка № 3: Надто глибокі ієрархії.
Якщо у вас ланцюжок наслідування довший за 3–4 рівні, замисліться: чи не час використати композицію або інтерфейси замість наслідування?
Помилка № 4: Примусова реалізація неактуальних методів.
Якщо абстрактний клас містить надто багато абстрактних методів, неактуальних для деяких нащадків, можливо, варто переглянути структуру. Наприклад, не всі транспортні засоби потребують методу fuelUp() (велосипеду він ні до чого).
Помилка № 5: Плутанина між абстрактним класом та інтерфейсом.
Абстрактний клас — коли є спільний стан і/або часткова реалізація. Інтерфейс — коли потрібно лише «пообіцяти» наявність методів, але не зберігати дані й не реалізовувати поведінку. Не змішуйте ці підходи без необхідності.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ