JavaRush /Курси /JAVA 25 SELF /Спрощення складних систем за допомогою абстракцій

Спрощення складних систем за допомогою абстракцій

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

1. Багаторівнева абстракція

Коли ви лише починаєте програмувати, усе здається простим: створили клас, викликали метод — і маєте результат. Проте у реальних проєктах усе ускладнюється: десятки класів, сотні методів, тисячі рядків коду… А якщо над проєктом працює команда — завдання стає ще складнішим. Як не потонути у цьому хаосі?

Відповідь — ділити складне на просте, а ще краще — на рівні абстракцій.

Що таке рівні абстракції?

Уявіть це як поверхи у великому будинку: на кожному поверсі своє життя, але всі поверхи повʼязані між собою. У програмуванні прийнято виокремлювати такі шари (рівні):

  • Користувацький інтерфейс (UI) — те, що бачить користувач.
  • Бізнес‑логіка — правила та процеси, які реалізують суть застосунку.
  • Доступ до даних (DAO, Repository) — робота з базою даних або файлами.

Кожен шар працює з абстракціями, не знаючи деталей інших шарів. Наприклад, для бізнес‑логіки неважливо, як саме реалізовано інтерфейс користувача або як зберігаються дані; важливо лише, що існують методи на кшталт saveOrder() і findUserById().

Аналогія з життя

Уявіть ресторан. Гості (UI) роблять замовлення через офіціанта (абстракція інтерфейсу), кухар (бізнес‑логіка) готує страви, а комірник (доступ до даних) стежить за наявністю продуктів на складі. Гості не знають, як саме кухар готує, а кухар не цікавиться, де лежить картопля — головне, щоб вона була під рукою.

2. Приклад: багаторівнева архітектура на практиці

Розвиньмо наш навчальний проєкт — застосунок для обліку завдань (менеджер завдань). Ми вже вміємо створювати класи для завдань, тож ускладнімо картину й поділимо застосунок на шари.

Виокремлюємо абстракції

  • Task — абстрактний опис завдання: назва, статус і методи виконання.
  • TaskRepository — абстракція для зберігання завдань (неважливо, де — у памʼяті, файлі чи базі даних).
  • TaskService — бізнес‑логіка: додавання завдань, пошук, виконання.

Абстрактні класи та інтерфейси

// Шар бізнес‑логіки
public abstract class Task {
    private String title;
    private boolean completed;

    public Task(String title) {
        this.title = title;
        this.completed = false;
    }

    public abstract void complete();

    public String getTitle() { return title; }
    public boolean isCompleted() { return completed; }

    protected void setCompleted(boolean completed) { this.completed = completed; }
}

// Шар зберігання даних (абстракція)
public interface TaskRepository {
    void save(Task task);
    Task findByTitle(String title);
    List<Task> findAll();
}

Реалізація шарів

Реалізація Task

public class WorkTask extends Task {
    private String deadline;

    public WorkTask(String title, String deadline) {
        super(title);
        this.deadline = deadline;
    }

    @Override
    public void complete() {
        setCompleted(true);
        System.out.println("Робоче завдання '" + getTitle() + "' виконано до дедлайну " + deadline);
    }
}

Реалізація TaskRepository

public class InMemoryTaskRepository implements TaskRepository {
    private List<Task> tasks = new ArrayList<>();

    @Override
    public void save(Task task) {
        tasks.add(task);
    }

    @Override
    public Task findByTitle(String title) {
        for (Task task : tasks) {
            if (task.getTitle().equals(title)) {
                return task;
            }
        }
        return null;
    }

    @Override
    public List<Task> findAll() {
        return new ArrayList<>(tasks);
    }
}

Реалізація TaskService

public class TaskService {
    private TaskRepository repository;

    public TaskService(TaskRepository repository) {
        this.repository = repository;
    }

    public void addTask(Task task) {
        repository.save(task);
    }

    public void completeTask(String title) {
        Task task = repository.findByTitle(title);
        if (task != null) {
            task.complete();
        } else {
            System.out.println("Завдання не знайдено: " + title);
        }
    }

    public void showAllTasks() {
        for (Task task : repository.findAll()) {
            System.out.println(task.getTitle() + " — " + (task.isCompleted() ? "виконано" : "не виконано"));
        }
    }
}

Використання в головному класі

public class Main {
    public static void main(String[] args) {
        TaskRepository repo = new InMemoryTaskRepository();
        TaskService service = new TaskService(repo);

        service.addTask(new WorkTask("Зробити звіт", "2025-07-15"));
        service.addTask(new WorkTask("Підготувати презентацію", "2025-07-16"));

        service.showAllTasks();

        service.completeTask("Зробити звіт");
        service.showAllTasks();
    }
}

Що ми отримали?

  • Головний клас (Main) не знає, як влаштоване сховище завдань — він працює з абстракцією TaskRepository.
  • TaskService не знає, які бувають завдання — він працює з абстрактним класом Task.
  • Якщо завтра захочемо зберігати завдання в базі даних, а не у памʼяті, — просто реалізуємо новий клас DatabaseTaskRepository, не переписуючи бізнес‑логіку та UI.
  • Якщо зʼявиться новий тип завдання, наприклад, HomeTask, — просто додамо новий клас‑нащадок.

3. Переваги для командної роботи

У великих проєктах рідко буває, що одна людина пише все підряд. Зазвичай команда ділиться на фронтенд‑ і бекенд‑розробників, розробників сховища тощо. Як абстракції допомагають їм не наступати одне одному на пʼяти?

Розподіл відповідальності

Кожен працює на своєму рівні абстракції.

  • Один розробник пише реалізацію TaskRepository для роботи з базою даних.
  • Інший займається бізнес‑логікою (TaskService).
  • Третій розробляє користувацький інтерфейс.

Контракт між шарами фіксується абстракціями.

Допоки всі домовилися, що в TaskRepository є методи save, findByTitle, findAll, — деталі реалізації неважливі.

Легкість тестування та заміни компонентів

  • Можна легко замінити одну реалізацію на іншу (наприклад, для тестів використовувати InMemoryTaskRepository, а у продакшні — реальну роботу з базою даних).
  • Тестувальник може підмінити шар даних «заглушкою» (mock), щоб тестувати бізнес‑логіку ізольовано.

Незалежність розвитку

  • Якщо хтось вирішить додати новий тип завдання, він не ламає наявний код — просто реалізує новий підклас Task.
  • Якщо зʼявиться новий спосіб зберігання даних, змінюється лише реалізація інтерфейсу, а решту коду не чіпаємо.

4. Найкращі практики: як не переборщити з абстракціями

Абстракція — як сіль у страві: без неї прісно, а якщо переборщити — зіпсуєш усе. Ось кілька порад:

Використовуйте абстракції там, де це справді спрощує систему.
Не варто робити абстрактний клас заради абстрактного класу. Якщо у вас є лише один тип завдання, можливо, абстракція не потрібна.

Документуйте абстрактні класи та методи.
Якісна документація допомагає зрозуміти, що саме має реалізувати нащадок і навіщо це потрібно.

Намагайтеся, щоб абстракції були осмисленими.
Абстрактний клас має виражати справді спільну поведінку та/або стан.

Не змішуйте відповідальність.
Не варто додавати в абстрактний клас методи, які потрібні лише одному з нащадків.

5. Абстракція у великих системах: приклад із життя

Розгляньмо, як абстракція працює у справді великому проєкті — наприклад, в інтернет‑магазині.

Шари системи

  • Контролери (UI): приймають запити користувача (наприклад, «оформити замовлення»).
  • Сервіси (бізнес‑логіка): перевіряють наявність товару, розраховують знижки, оформляють замовлення.
  • Репозиторії (доступ до даних): зберігають замовлення, товари, користувачів до бази даних.

Приклад абстракцій

// Абстракція для сервісу обробки замовлень
public interface OrderService {
    void createOrder(Order order);
    Order findOrderById(String id);
}

// Абстракція для сховища замовлень
public interface OrderRepository {
    void save(Order order);
    Order findById(String id);
}

Кожен шар знає лише про свою абстракцію. Якщо завтра вирішать зберігати замовлення в хмарі, змінюється тільки реалізація OrderRepository.

Взаємодія шарів — схема

[UI/Controller] <--> [OrderService (абстракція)] <--> [OrderRepository (абстракція)] <--> [База даних]
  • Кожен шар працює з абстракцією, не знаючи деталей нижчого шару.
  • Це дозволяє розробляти, тестувати та доопрацьовувати кожен шар незалежно.

Абстракція та підтримка коду

  • Легко додавати нові можливості (нові типи завдань, платежів, транспорту).
  • Легко виправляти помилки (виправили помилку в одному місці — усі нащадки отримали оновлення).
  • Легко тестувати (можна підмінювати шари на «заглушки» для юніт‑тестів).

6. Типові помилки під час проєктування абстракцій

Помилка № 1: Надмірна абстракція. Іноді хочеться зробити абстрактний клас із будь‑якого приводу. Але якщо у вас лише один тип сутності, не варто заради моди робити абстракцію — це лише ускладнить код.

Помилка № 2: Надто розмита абстракція. Якщо ваш абстрактний клас охоплює надто багато і не має чіткої сфери відповідальності, його нащадки будуть змушені реалізовувати зайві методи або зберігати «мертві» поля.

Помилка № 3: Порушення принципу єдиної відповідальності. Абстрактний клас має відповідати лише за одну область поведінки. Не варто змішувати, наприклад, методи для зберігання та методи для бізнес‑логіки в одному абстрактному класі.

Помилка № 4: Жорсткий звʼязок між шарами. Якщо шар бізнес‑логіки безпосередньо залежить від конкретної реалізації сховища (наприклад, використовує new InMemoryTaskRepository() у своєму коді), то під час заміни сховища доведеться переписувати весь код. Використовуйте абстракції (інтерфейси, абстрактні класи) для послаблення звʼязків.

Помилка № 5: Недостатня документація. Абстракція — це контракт, і його потрібно чітко описувати. Якщо не описати, що має робити нащадок, легко отримати неочікувані помилки або «творчість» колег.

1
Опитування
Абстрактні класи, рівень 19, лекція 4
Недоступний
Абстрактні класи
Абстракція і абстрактні класи
Коментарі (1)
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ
Grimnir Рівень 31
21 січня 2026
Нарешті задачки, які справді заставили подумати і попрацювати