CodeGym /Kursy /JAVA 25 SELF /Uproszczanie złożonych systemów dzięki abstrakcjom

Uproszczanie złożonych systemów dzięki abstrakcjom

JAVA 25 SELF
Poziom 19 , Lekcja 4
Dostępny

1. Wielowarstwowa abstrakcja

Gdy dopiero zaczynasz programować, wszystko wydaje się proste: piszesz klasę, wywołujesz metodę, dostajesz wynik. Jednak w prawdziwych projektach wszystko się komplikuje: dziesiątki klas, setki metod, tysiące linii kodu... A jeśli nad projektem pracuje zespół – robi się jeszcze trudniej! Jak nie utonąć w tym chaosie?

Odpowiedź – dzielić złożone na proste, a jeszcze lepiej – na poziomy abstrakcji.

Czym są poziomy abstrakcji?

Wyobraź sobie to jak piętra w dużym domu: na każdym piętrze toczy się własne życie, ale wszystkie piętra są ze sobą połączone. W programowaniu zwykle wyróżnia się takie warstwy (poziomy):

  • Interfejs użytkownika (UI) – to, co widzi użytkownik.
  • Logika biznesowa – zasady i procesy, które oddają istotę aplikacji.
  • Dostęp do danych (DAO, Repository) – praca z bazą danych lub plikami.

Każda warstwa pracuje na abstrakcjach, nie znając szczegółów pozostałych warstw. Na przykład dla logiki biznesowej nie ma znaczenia, jak dokładnie zrealizowano interfejs użytkownika ani jak przechowywane są dane – ważne, że istnieją metody typu saveOrder() lub findUserById().

Analogia z życia

Wyobraź sobie restaurację. Goście (UI) składają zamówienie przez kelnera (abstrakcja interfejsu), kucharz (logika biznesowa) przygotowuje danie, a magazynier (dostęp do danych) dba o dostępność produktów w magazynie. Goście nie wiedzą, jak dokładnie kucharz przyrządza potrawę, a kucharz nie interesuje się, gdzie leżą ziemniaki – najważniejsze, żeby były pod ręką.

2. Przykład: architektura wielowarstwowa w praktyce

Rozwińmy nasz projekt edukacyjny – na przykład aplikację do ewidencji zadań (task manager). Umiemy już tworzyć klasy reprezentujące zadania, teraz skomplikujmy obraz i podzielmy aplikację na warstwy.

Wydzielenie abstrakcji

  • Task – abstrakcyjny opis zadania: zadanie ma nazwę, status i metody do wykonania.
  • TaskRepository – abstrakcja przechowywania zadań (nieważne gdzie – w pamięci, pliku, bazie danych).
  • TaskService – logika biznesowa: dodawanie zadań, wyszukiwanie, wykonywanie.

Klasy abstrakcyjne i interfejsy

// Warstwa logiki biznesowej
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; }
}

// Warstwa dostępu do danych (abstrakcja)
public interface TaskRepository {
    void save(Task task);
    Task findByTitle(String title);
    List<Task> findAll();
}

Implementacja warstw

Implementacja 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("Zadanie służbowe '" + getTitle() + "' ukończone w terminie " + deadline);
    }
}

Implementacja 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);
    }
}

Implementacja 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("Nie znaleziono zadania: " + title);
        }
    }

    public void showAllTasks() {
        for (Task task : repository.findAll()) {
            System.out.println(task.getTitle() + " — " + (task.isCompleted() ? "wykonane" : "niewykonane"));
        }
    }
}

Użycie w głównej klasie

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

        service.addTask(new WorkTask("Sporządzić raport", "2025-07-15"));
        service.addTask(new WorkTask("Przygotować prezentację", "2025-07-16"));

        service.showAllTasks();

        service.completeTask("Sporządzić raport");
        service.showAllTasks();
    }
}

Co uzyskaliśmy?

  • Główna klasa (Main) nie wie, jak zbudowane jest repozytorium zadań – pracuje z abstrakcją TaskRepository.
  • TaskService nie wie, jakie są rodzaje zadań – pracuje z abstrakcyjną klasą Task.
  • Jeśli jutro zechcemy przechowywać zadania w bazie danych, a nie w pamięci – po prostu zaimplementujemy nową klasę DatabaseTaskRepository, bez przepisywania logiki biznesowej i UI.
  • Jeśli pojawi się nowy typ zadania, na przykład HomeTask, – po prostu dodamy nową klasę‑potomka.

3. Korzyści dla pracy zespołowej

W dużych projektach rzadko bywa tak, że jedna osoba pisze wszystko. Zwykle zespół dzieli się na „frontendowców”, „backendowców”, „specjalistów od warstwy danych” itd. Jak abstrakcje pomagają im nie wchodzić sobie w drogę?

Podział odpowiedzialności

Każdy pracuje na swoim poziomie abstrakcji.

  • Jeden deweloper pisze implementację TaskRepository do pracy z bazą.
  • Drugi zajmuje się logiką biznesową (TaskService).
  • Trzeci tworzy interfejs użytkownika.

Kontrakt między warstwami jest określony przez abstrakcje.

Dopóki wszyscy zgadzają się, że TaskRepository ma metody save, findByTitle, findAll – szczegóły implementacji nie mają znaczenia.

Łatwość testowania i wymiany komponentów

  • Można łatwo wymienić jedną implementację na inną (np. w testach używać InMemoryTaskRepository, a na produkcji – pracy z bazą).
  • Tester może podmienić warstwę danych „zaślepką” (mockiem), aby testować logikę biznesową w izolacji.

Niezależny rozwój

  • Jeśli ktoś doda nowy typ zadania, nie psuje istniejącego kodu – po prostu implementuje nową podklasę Task.
  • Jeśli pojawi się nowy sposób przechowywania danych, zmienia się tylko implementacja interfejsu, a reszty kodu nie trzeba dotykać.

4. Dobre praktyki: jak nie przesadzić z abstrakcjami

Abstrakcja jest jak sól w potrawie: bez niej mdło, a jeśli przesadzisz – zepsujesz wszystko. Oto kilka wskazówek:

Używaj abstrakcji tam, gdzie realnie upraszczają system.
Nie rób abstrakcji dla samej abstrakcji. Jeśli masz tylko jeden typ zadania, być może abstrakcja nie jest potrzebna.

Dokumentuj klasy abstrakcyjne i metody.
Dobra dokumentacja pomaga zrozumieć, co dokładnie ma zaimplementować potomek i po co.

Dbaj, aby abstrakcje były sensowne.
Klasa abstrakcyjna powinna wyrażać naprawdę wspólne zachowanie i/lub stan.

Nie mieszaj odpowiedzialności.
Nie dodawaj do klasy abstrakcyjnej metod potrzebnych tylko jednemu z potomków.

5. Abstrakcja w dużych systemach: przykład z życia

Przyjrzyjmy się, jak abstrakcja działa w naprawdę dużym projekcie – na przykład w sklepie internetowym.

Warstwy systemu

  • Kontrolery (UI): przyjmują żądania użytkownika (np. „złożyć zamówienie”).
  • Serwisy (logika biznesowa): sprawdzają dostępność towaru, obliczają rabaty, realizują zamówienie.
  • Repozytoria (dostęp do danych): zapisują zamówienia, produkty i użytkowników do bazy danych.

Przykład abstrakcji

// Abstrakcja dla serwisu obsługi zamówień
public interface OrderService {
    void createOrder(Order order);
    Order findOrderById(String id);
}

// Abstrakcja dla repozytorium zamówień
public interface OrderRepository {
    void save(Order order);
    Order findById(String id);
}

Każda warstwa zna tylko własną abstrakcję. Jeśli jutro postanowimy przechowywać zamówienia w chmurze – zmieni się tylko implementacja OrderRepository.

Współpraca warstw – schemat

[UI/Controller] <--> [OrderService (abstrakcja)] <--> [OrderRepository (abstrakcja)] <--> [Baza danych]
  • Każda warstwa pracuje z abstrakcją, nie znając szczegółów warstwy niższej.
  • Pozwala to rozwijać, testować i ulepszać każdą warstwę niezależnie.

Abstrakcja a utrzymanie kodu

  • Łatwo dodawać nowe możliwości (nowe typy zadań, płatności, transportu).
  • Łatwo naprawiać błędy (naprawisz błąd w jednym miejscu – wszyscy potomkowie dostaną aktualizację).
  • Łatwo testować (można podmieniać warstwy na „mocki” do testów jednostkowych).

6. Typowe błędy przy projektowaniu abstrakcji

Błąd nr 1: Nadmierna abstrakcja. Czasem kusi, by tworzyć klasę abstrakcyjną na każdy drobiazg. Ale jeśli masz tylko jeden rodzaj encji, nie twórz abstrakcji dla mody – tylko skomplikuje kod.

Błąd nr 2: Zbyt rozmyta abstrakcja. Jeśli klasa abstrakcyjna opisuje zbyt wiele i nie ma wyraźnego zakresu odpowiedzialności, jej potomkowie będą zmuszeni implementować niepotrzebne metody albo przechowywać „martwe” pola.

Błąd nr 3: Naruszenie zasady jednej odpowiedzialności. Klasa abstrakcyjna powinna odpowiadać tylko za jeden obszar zachowania. Nie mieszaj, na przykład, metod przechowywania i metod logiki biznesowej w jednej klasie abstrakcyjnej.

Błąd nr 4: Ścisłe powiązanie między warstwami. Jeśli warstwa logiki biznesowej bezpośrednio zależy od konkretnej implementacji repozytorium (np. używa new InMemoryTaskRepository() w swoim wnętrzu), to przy wymianie repozytorium trzeba będzie przepisać cały kod. Używaj abstrakcji (interfejsów, klas abstrakcyjnych) do luzowania powiązań.

Błąd nr 5: Niewystarczająca dokumentacja. Abstrakcja to kontrakt i trzeba go jasno opisać. Jeśli nie napiszesz, co ma robić potomek, łatwo o nieoczekiwane błędy lub „twórczość” kolegów.

1
Zadanie
JAVA 25 SELF, poziom 19, lekcja 4
Niedostępne
Tworzymy podstawę menedżera zadań ✍️
Tworzymy podstawę menedżera zadań ✍️
1
Zadanie
JAVA 25 SELF, poziom 19, lekcja 4
Niedostępne
Inteligentny serwis zadań 🧠
Inteligentny serwis zadań 🧠
1
Zadanie
JAVA 25 SELF, poziom 19, lekcja 4
Niedostępne
Budujemy modułowy menedżer zadań 🏗️
Budujemy modułowy menedżer zadań 🏗️
1
Zadanie
JAVA 25 SELF, poziom 19, lekcja 4
Niedostępne
Rozszerzamy menedżera zadań o różne typy zadań 🚀
Rozszerzamy menedżera zadań o różne typy zadań 🚀
1
Ankieta/quiz
Klasy abstrakcyjne, poziom 19, lekcja 4
Niedostępny
Klasy abstrakcyjne
Abstrakcja i klasy abstrakcyjne
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION