1. Абстракція в реальних застосунках: навіщо вона потрібна
У попередніх лекціях ми вже познайомилися з поняттям абстракції та розглянули прості приклади. Тепер подивімося, як цей підхід працює у більш реалістичних завданнях. У реальних проєктах майже завжди доводиться мати справу з різними, але схожими об’єктами. Наприклад, різні способи оплати, різні види транспорту, різні фігури в графічному редакторі. Якщо не застосовувати абстракцію, код швидко перетворюється на набір конструкцій «if-else» та суцільне дублювання. З абстракцією ж — усе структуровано, акуратно й, головне, зручно для розвитку та підтримки.
Абстракція дає змогу:
- Приховати деталі реалізації: працювати з об’єктами через спільний інтерфейс, не цікавлячись, як вони влаштовані всередині.
- Уникнути дублювання коду: спільну поведінку та поля винесено в базовий клас.
- Легко розширювати систему: додавання нових видів об’єктів не вимагає переписування старого коду.
- Зробити код гнучким: можна замінити одну реалізацію на іншу без зміни решти системи.
Розгляньмо кілька прикладів із різних сфер.
2. Приклад 1: Платіжні системи
Постановка завдання
Припустімо, ви пишете модуль для інтернет-магазину. Ваше завдання — реалізувати обробку різних видів платежів: банківська картка, PayPal, криптовалюта. Усі вони повинні вміти «обробляти платіж», але деталі в кожного — свої.
Абстракція: клас Payment
public abstract class Payment {
protected double amount;
public Payment(double amount) {
this.amount = amount;
}
// Абстрактний метод: як саме обробляти платіж — вирішують нащадки
public abstract void process();
// Спільний метод для всіх платежів
public void printAmount() {
System.out.println("Сума платежу: " + amount + " ₴");
}
}
Конкретні реалізації
public class CreditCardPayment extends Payment {
private String cardNumber;
public CreditCardPayment(double amount, String cardNumber) {
super(amount);
this.cardNumber = cardNumber;
}
@Override
public void process() {
System.out.println("Обробка платежу карткою: " + cardNumber);
// Тут могла б бути інтеграція з банком :)
}
}
public class PaypalPayment extends Payment {
private String email;
public PaypalPayment(double amount, String email) {
super(amount);
this.email = email;
}
@Override
public void process() {
System.out.println("Обробка платежу PayPal для облікового запису: " + email);
// А тут — виклик PayPal API
}
}
public class CryptoPayment extends Payment {
private String walletAddress;
public CryptoPayment(double amount, String walletAddress) {
super(amount);
this.walletAddress = walletAddress;
}
@Override
public void process() {
System.out.println("Обробка криптоплатежу на гаманець: " + walletAddress);
// Тут могла б бути магія блокчейну
}
}
Використання абстракції
import java.util.*;
public class PaymentDemo {
public static void main(String[] args) {
List<Payment> payments = new ArrayList<>();
payments.add(new CreditCardPayment(1500.0, "1234 5678 9012 3456"));
payments.add(new PaypalPayment(500.0, "user@example.com"));
payments.add(new CryptoPayment(0.05, "0xABCD..."));
for (Payment payment : payments) {
payment.printAmount();
payment.process();
System.out.println("---");
}
}
}
Результат роботи:
Сума платежу: 1500.0 ₴
Обробка платежу карткою: 1234 5678 9012 3456
---
Сума платежу: 500.0 ₴
Обробка платежу PayPal для облікового запису: user@example.com
---
Сума платежу: 0.05 ₴
Обробка криптоплатежу на гаманець: 0xABCD...
---
Переваги:
- Можна додати новий спосіб оплати, не змінюючи старий код (наприклад, Apple Pay).
- Код, який працює з платежами, не залежить від їхнього конкретного типу.
- Спільну логіку (наприклад, друк суми через printAmount()) реалізовано в одному місці.
3. Приклад 2: Транспорт
Постановка завдання
У грі або симуляторі є різні види транспорту: автомобілі, велосипеди, потяги. Усі вони можуть рухатися, але роблять це по-різному. Деякі потребують заправки, інші — ні.
Абстракція: клас Transport
public abstract class Transport {
protected String name;
public Transport(String name) {
this.name = name;
}
public abstract void move();
// Не всі види транспорту потребують заправки, тому за замовчуванням — ні
public void fuelUp() {
System.out.println(name + ": заправка не потрібна.");
}
}
Конкретні реалізації
public class Car extends Transport {
public Car(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " їде дорогою.");
}
@Override
public void fuelUp() {
System.out.println(name + ": заправляємо бензином.");
}
}
public class Bicycle extends Transport {
public Bicycle(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " крутить педалі.");
}
// fuelUp не перевизначаємо — велосипеду не потрібна заправка
}
public class Train extends Transport {
public Train(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " мчить колією.");
}
@Override
public void fuelUp() {
System.out.println(name + ": заправляємо дизелем або електрикою.");
}
}
Використання абстракції
import java.util.*;
public class TransportDemo {
public static void main(String[] args) {
List<Transport> vehicles = Arrays.asList(
new Car("Toyota"),
new Bicycle("Stels"),
new Train("Інтерсіті")
);
for (Transport t : vehicles) {
t.move();
t.fuelUp();
System.out.println("---");
}
}
}
Результат роботи:
Toyota їде дорогою.
Toyota: заправляємо бензином.
---
Stels крутить педалі.
Stels: заправка не потрібна.
---
Інтерсіті мчить колією.
Інтерсіті: заправляємо дизелем або електрикою.
---
Переваги:
- Можна обробляти будь-який транспорт однаково, не перевіряючи його тип.
- Легко додати новий вид транспорту (наприклад, електросамокат).
4. Приклад 3: Графічний редактор
Постановка завдання
Ви пишете мініграфічний редактор. У ньому є лінії, еліпси, багатокутники — і все це фігури, які можна намалювати й змінити їхній розмір. Водночас кожна фігура реалізує ці дії по-своєму.
Абстракція: клас Figure
public abstract class Figure {
protected String color = "black";
public abstract void draw();
public abstract void resize(double factor);
public void setColor(String color) {
this.color = color;
}
}
Конкретні реалізації
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("Малюємо лінію довжиною " + length + " кольором " + color);
}
@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 + " кольором " + color);
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("Нові розміри еліпса: " + a + " x " + b);
}
}
public class Polygon extends Figure {
private int sides;
public Polygon(int sides) {
this.sides = sides;
}
@Override
public void draw() {
System.out.println("Малюємо багатокутник із " + sides + " сторонами кольором " + color);
}
@Override
public void resize(double factor) {
System.out.println("Змінюємо розмір багатокутника з " + sides + " сторонами на " + factor);
}
}
Використання абстракції
import java.util.*;
public class EditorDemo {
public static void main(String[] args) {
List<Figure> figures = new ArrayList<>();
figures.add(new Line(10));
figures.add(new Ellipse(5, 3));
figures.add(new Polygon(6));
for (Figure f : figures) {
f.setColor("green");
f.draw();
f.resize(2);
System.out.println("---");
}
}
}
Результат роботи:
Малюємо лінію довжиною 10.0 кольором green
Нова довжина лінії: 20.0
---
Малюємо еліпс з осями 5.0 і 3.0 кольором green
Нові розміри еліпса: 10.0 x 6.0
---
Малюємо багатокутник із 6 сторонами кольором green
Змінюємо розмір багатокутника з 6 сторонами на 2.0
---
Переваги:
- Усі фігури можна зберігати в одному списку й обробляти однаково.
- Легко додати нову фігуру (наприклад, зірку або серце).
- Спільні методи (наприклад, встановлення кольору через setColor()) реалізовано один раз.
5. Як абстракція допомагає спростити код
У кожному прикладі вище є спільна схема:
- Базовий абстрактний клас задає контракт (що вміє об’єкт).
- Конкретні нащадки реалізують деталі.
- Код, що працює з абстракцією, не залежить від типу об’єкта, що робить систему гнучкою та розширюваною.
Таблиця порівняння підходів
| Без абстракції (if-else) | З абстракцією (ООП) |
|---|---|
| Багато умов за типом | Поліморфізм замість умов |
| Дублювання логіки | Логіка — в одному місці |
| Важко розширювати | Додавати — легко |
| Важко тестувати | Легко підмінювати реалізації |
6. Типові помилки під час проєктування абстракцій
Помилка № 1: Абстракція заради абстракції.
Якщо у вас лише один тип об’єкта і не планується розширення — абстрактний клас не потрібен. Не варто ускладнювати код без причини.
Помилка № 2: Надто загальна абстракція.
Якщо базовий клас надто «розмитий», нащадки можуть не мати нічого спільного, окрім назви. Наприклад, абстракція «Thing» для всього підряд. Це ускладнює підтримку й розуміння коду.
Помилка № 3: Дублювання коду в нащадках.
Якщо у всіх нащадків однакова реалізація методу, її варто винести в базовий клас (зробити неабстрактною).
Помилка № 4: Порушення принципу «від загального до конкретного».
Якщо в абстрактному класі з’являються деталі, які потрібні лише одному нащадку, — отже, абстракцію вибрано неправильно.
Помилка № 5: Забули реалізувати абстрактні методи.
Якщо не реалізувати всі абстрактні методи в нащадку, компілятор змусить зробити клас теж абстрактним. Іноді це несподівано :)
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ