JavaRush /Курси /JAVA 25 SELF /Приклади побудови абстракцій у реальних завданнях

Приклади побудови абстракцій у реальних завданнях

JAVA 25 SELF
Рівень 19 , Лекція 3
Відкрита

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: Забули реалізувати абстрактні методи.
Якщо не реалізувати всі абстрактні методи в нащадку, компілятор змусить зробити клас теж абстрактним. Іноді це несподівано :)

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ