CodeGym /Kurse /JAVA 25 SELF /Vereinfachung komplexer Systeme mit Abstraktionen

Vereinfachung komplexer Systeme mit Abstraktionen

JAVA 25 SELF
Level 19, Lektion 4
Verfügbar

1. Mehrschichtige Abstraktion

Wenn man erst mit dem Programmieren beginnt, wirkt alles einfach: Klasse geschrieben, Methode aufgerufen, Ergebnis erhalten. In realen Projekten wird es jedoch komplizierter: Dutzende Klassen, Hunderte Methoden, Tausende Zeilen Code … Und wenn ein Team am Projekt arbeitet, wird es noch komplexer! Wie nicht in diesem Chaos untergehen?

Die Antwort lautet: Komplexes in Einfaches zerlegen – und noch besser: in Abstraktionsebenen.

Was sind Abstraktionsebenen?

Stellen Sie sie sich wie Stockwerke in einem großen Gebäude vor: Auf jedem Stockwerk findet eigenes Leben statt, doch alle sind miteinander verbunden. In der Programmierung werden üblicherweise folgende Schichten unterschieden:

  • Benutzeroberfläche (UI) – das, was der Benutzer sieht.
  • Geschäftslogik – Regeln und Prozesse, die das Wesen der Anwendung umsetzen.
  • Datenzugriff (DAO, Repository) – Arbeit mit Datenbank oder Dateien.

Jede Schicht arbeitet mit Abstraktionen, ohne Details anderer Schichten zu kennen. Der Geschäftslogik ist zum Beispiel egal, wie genau die Benutzeroberfläche implementiert ist oder wie Daten gespeichert werden – wichtig ist, dass es Methoden wie saveOrder() oder findUserById() gibt.

Analogie aus dem Alltag

Stellen Sie sich ein Restaurant vor. Gäste (UI) geben die Bestellung über den Kellner (Abstraktion der Schnittstelle) auf, der Koch (Geschäftslogik) bereitet das Gericht zu, und der Lagerist (Datenzugriff) überwacht die Verfügbarkeit der Produkte im Lager. Die Gäste wissen nicht, wie genau der Koch das Gericht zubereitet, und den Koch interessiert nicht, wo die Kartoffeln liegen – Hauptsache, sie sind griffbereit.

2. Beispiel: Mehrschichtige Architektur in der Praxis

Lassen wir unser Übungsprojekt weiterwachsen – z. B. eine Anwendung zur Aufgabenverwaltung (Task Manager). Wir können bereits Klassen für Aufgaben erstellen; nun verkomplizieren wir das Bild und teilen die Anwendung in Schichten.

Abstraktionen herausarbeiten

  • Task – abstrakte Beschreibung einer Aufgabe: Eine Aufgabe hat einen Titel, einen Status und Methoden zur Ausführung.
  • TaskRepository – Abstraktion für die Speicherung von Aufgaben (egal wo – im Speicher, in einer Datei, in der Datenbank).
  • TaskService – Geschäftslogik: Hinzufügen, Suchen, Ausführen von Aufgaben.

Abstrakte Klassen und Interfaces

// Schicht der Geschäftslogik
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; }
}

// Datenschicht (Abstraktion)
public interface TaskRepository {
    void save(Task task);
    Task findByTitle(String title);
    List<Task> findAll();
}

Implementierung der Schichten

Implementierung von 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("Arbeitsaufgabe '" + getTitle() + "' erledigt bis " + deadline);
    }
}

Implementierung von 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);
    }
}

Implementierung von 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("Aufgabe nicht gefunden: " + title);
        }
    }

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

Verwendung in der Main-Klasse

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

        service.addTask(new WorkTask("Bericht erstellen", "2025-07-15"));
        service.addTask(new WorkTask("Präsentation vorbereiten", "2025-07-16"));

        service.showAllTasks();

        service.completeTask("Bericht erstellen");
        service.showAllTasks();
    }
}

Was haben wir erreicht?

  • Die Main-Klasse (Main) weiß nicht, wie das Aufgabenspeicher-Repository aufgebaut ist – sie arbeitet mit der Abstraktion TaskRepository.
  • TaskService kennt die konkreten Aufgabentypen nicht – er arbeitet mit der abstrakten Klasse Task.
  • Wenn wir morgen Aufgaben in einer Datenbank statt im Speicher ablegen wollen, implementieren wir einfach eine neue Klasse DatabaseTaskRepository, ohne Geschäftslogik und UI umzuschreiben.
  • Falls ein neuer Aufgabentyp entsteht, z. B. HomeTask, fügen wir einfach eine neue Subklasse hinzu.

3. Vorteile für Teamarbeit

In großen Projekten schreibt selten eine Person alles. Üblicherweise teilt sich das Team in „Frontend‑Entwickler“, „Backend‑Entwickler“, „Storage‑Entwickler“ usw. Wie helfen Abstraktionen, damit sie sich nicht gegenseitig behindern?

Trennung der Verantwortlichkeiten

Jeder arbeitet auf seiner eigenen Abstraktionsebene.

  • Eine Person schreibt die Implementierung von TaskRepository für die Arbeit mit der Datenbank.
  • Eine andere kümmert sich um die Geschäftslogik (TaskService).
  • Eine dritte baut die Benutzeroberfläche.

Der Vertrag zwischen den Schichten wird durch Abstraktionen festgelegt.

Solange Einigkeit darüber besteht, dass TaskRepository die Methoden save, findByTitle, findAll hat, sind Implementierungsdetails unwichtig.

Einfaches Testen und Austauschen von Komponenten

  • Man kann eine Implementierung leicht durch eine andere ersetzen (z. B. für Tests InMemoryTaskRepository verwenden, in der Produktion – die Arbeit mit der Datenbank).
  • Der Tester kann die Datenschicht durch ein „Mock“ ersetzen, um die Geschäftslogik isoliert zu testen.

Unabhängige Weiterentwicklung

  • Wenn jemand einen neuen Aufgabentyp hinzufügen möchte, bricht er keinen bestehenden Code – er implementiert einfach eine neue Subklasse von Task.
  • Kommt eine neue Art der Datenspeicherung hinzu, ändert sich nur die Implementierung des Interfaces; der restliche Code bleibt unberührt.

4. Best Practices: Wie man es mit Abstraktionen nicht übertreibt

Abstraktion ist wie Salz im Essen: ohne sie ist es fad, doch zu viel verdirbt alles. Hier ein paar Tipps:

Verwenden Sie Abstraktionen dort, wo sie das System wirklich vereinfachen.
Machen Sie keinen abstrakten Klassenbau nur um seiner selbst willen. Wenn Sie nur einen Aufgabentyp haben, ist die Abstraktion möglicherweise überflüssig.

Dokumentieren Sie abstrakte Klassen und Methoden.
Gute Dokumentation hilft zu verstehen, was genau der Nachfolger implementieren soll – und warum.

Sorgen Sie dafür, dass Abstraktionen sinnvoll sind.
Eine abstrakte Klasse sollte wirklich gemeinsames Verhalten und/oder gemeinsamen Zustand ausdrücken.

Mischen Sie keine Verantwortlichkeiten.
Fügen Sie in eine abstrakte Klasse keine Methoden ein, die nur ein einzelner Nachfolger benötigt.

5. Abstraktion in großen Systemen: Beispiel aus der Praxis

Betrachten wir, wie Abstraktion in einem wirklich großen Projekt funktioniert – z. B. in einem Online‑Shop.

Schichten des Systems

  • Controller (UI): nehmen Benutzeranfragen entgegen (z. B. „Bestellung aufgeben“).
  • Services (Geschäftslogik): prüfen die Verfügbarkeit, berechnen Rabatte, führen die Bestellung aus.
  • Repositories (Datenzugriff): speichern Bestellungen, Produkte, Benutzer in der Datenbank.

Beispiel für Abstraktionen

// Abstraktion für den Bestellservice
public interface OrderService {
    void createOrder(Order order);
    Order findOrderById(String id);
}

// Abstraktion für das Bestell-Repository
public interface OrderRepository {
    void save(Order order);
    Order findById(String id);
}

Jede Schicht kennt nur ihre Abstraktion. Wenn morgen entschieden wird, Bestellungen in der Cloud zu speichern, ändert sich nur die Implementierung von OrderRepository.

Zusammenspiel der Schichten – Schema

[UI/Controller] <--> [OrderService (Abstraktion)] <--> [OrderRepository (Abstraktion)] <--> [Datenbank]
  • Jede Schicht arbeitet mit der Abstraktion, ohne die Details der darunterliegenden Schicht zu kennen.
  • Das ermöglicht es, jede Schicht unabhängig zu entwickeln, zu testen und weiterzuentwickeln.

Abstraktion und Wartbarkeit des Codes

  • Leicht neue Funktionen hinzufügen (neue Aufgabentypen, Zahlungen, Transportarten).
  • Leicht Fehler beheben (Bug an einer Stelle behoben – alle Nachfolger bekommen das Update).
  • Leicht testen (man kann Schichten für Unit‑Tests durch „Stubs“ ersetzen).

6. Typische Fehler beim Entwurf von Abstraktionen

Fehler Nr. 1: Übermäßige Abstraktion. Manchmal möchte man für jeden Kleinkram eine abstrakte Klasse bauen. Wenn es jedoch nur eine Entitätsart gibt, sollte man nicht aus modischen Gründen abstrahieren – das verkompliziert den Code nur.

Fehler Nr. 2: Zu vage Abstraktion. Wenn Ihre abstrakte Klasse zu viel beschreibt und keinen klaren Verantwortungsbereich hat, werden Nachfolger gezwungen, unnötige Methoden zu implementieren oder „tote“ Felder zu halten.

Fehler Nr. 3: Verletzung des Single-Responsibility-Prinzips. Eine abstrakte Klasse sollte nur für einen Verantwortungsbereich stehen. Mischen Sie z. B. keine Speicher-Methoden und Geschäftslogik in einer abstrakten Klasse.

Fehler Nr. 4: Starre Kopplung zwischen den Schichten. Wenn die Geschäftslogik direkt von einer konkreten Speicher-Implementierung abhängt (z. B. verwendet new InMemoryTaskRepository() intern), muss beim Austausch des Speichers der gesamte Code umgeschrieben werden. Verwenden Sie Abstraktionen (Interfaces, abstrakte Klassen), um Kopplungen zu lockern.

Fehler Nr. 5: Unzureichende Dokumentation. Abstraktion ist ein Vertrag, und der sollte klar beschrieben sein. Wenn nicht festgehalten ist, was ein Nachfolger leisten muss, entstehen leicht unerwartete Fehler oder „kreative“ Abweichungen im Team.

1
Aufgabe
JAVA 25 SELF, Level 19, Lektion 4
Gesperrt
Grundlage für einen Aufgabenmanager erstellen ✍️
Grundlage für einen Aufgabenmanager erstellen ✍️
1
Aufgabe
JAVA 25 SELF, Level 19, Lektion 4
Gesperrt
Intelligenter Aufgabenservice 🧠
Intelligenter Aufgabenservice 🧠
1
Aufgabe
JAVA 25 SELF, Level 19, Lektion 4
Gesperrt
Wir bauen einen modularen Aufgabenmanager 🏗️
Wir bauen einen modularen Aufgabenmanager 🏗️
1
Aufgabe
JAVA 25 SELF, Level 19, Lektion 4
Gesperrt
Den Aufgabenmanager für verschiedene Aufgabentypen erweitern 🚀
Den Aufgabenmanager für verschiedene Aufgabentypen erweitern 🚀
1
Umfrage/Quiz
Abstrakte Klassen, Level 19, Lektion 4
Nicht verfügbar
Abstrakte Klassen
Abstraktion und abstrakte Klassen
Kommentare
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION