1. Abstraktion in realen Anwendungen: warum sie nötig ist
In den vorherigen Vorlesungen haben wir bereits den Begriff der Abstraktion kennengelernt und einfache Beispiele betrachtet. Jetzt schauen wir, wie dieser Ansatz in realeren Aufgaben funktioniert. In realen Projekten hat man fast immer mit unterschiedlichen, aber ähnlichen Objekten zu tun. Zum Beispiel verschiedene Zahlungsarten, verschiedene Verkehrsmittel, verschiedene Formen in einem Grafikeditor. Ohne Abstraktion wird der Code schnell zu einer Sammlung aus „if-else“ und Copy-and-Paste. Mit Abstraktion hingegen – alles ist streng, schön und vor allem bequem für Weiterentwicklung und Wartung.
Abstraktion ermöglicht:
- Implementierungsdetails zu verbergen: mit Objekten über eine gemeinsame Schnittstelle arbeiten, ohne sich dafür zu interessieren, wie sie innen aufgebaut sind.
- Code-Duplikate zu vermeiden: gemeinsames Verhalten und Felder werden in die Basisklasse ausgelagert.
- Das System leicht zu erweitern: das Hinzufügen neuer Objektarten erfordert kein Umschreiben des alten Codes.
- Den Code flexibel zu machen: man kann eine Implementierung durch eine andere ersetzen, ohne den Rest des Systems zu ändern.
Schauen wir uns einige Beispiele aus verschiedenen Bereichen an.
2. Beispiel 1: Zahlungssysteme
Aufgabenstellung
Angenommen, Sie schreiben ein Modul für einen Online-Shop. Ihre Aufgabe ist es, verschiedene Zahlungsarten zu verarbeiten: Kreditkarte, PayPal, Kryptowährung. Alle sollen „eine Zahlung verarbeiten“ können, aber die Details sind jeweils unterschiedlich.
Abstraktion: Klasse Payment
public abstract class Payment {
protected double amount;
public Payment(double amount) {
this.amount = amount;
}
// Abstrakte Methode: Wie die Zahlung konkret verarbeitet wird – entscheiden die Subklassen
public abstract void process();
// Allgemeine Methode für alle Zahlungen
public void printAmount() {
System.out.println("Zahlungsbetrag: " + amount + " Rubel.");
}
}
Konkrete Implementierungen
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("Verarbeitung der Kartenzahlung: " + cardNumber);
// Hier könnte eine Bankintegration stehen :)
}
}
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("Verarbeitung der PayPal-Zahlung für das Konto: " + email);
// Und hier – Aufruf des 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("Verarbeitung der Krypto-Zahlung an die Wallet: " + walletAddress);
// Hier könnte Blockchain-Magie stehen
}
}
Nutzung der Abstraktion
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("---");
}
}
}
Ausgabe:
Zahlungsbetrag: 1500.0 Rubel.
Verarbeitung der Kartenzahlung: 1234 5678 9012 3456
---
Zahlungsbetrag: 500.0 Rubel.
Verarbeitung der PayPal-Zahlung für das Konto: user@example.com
---
Zahlungsbetrag: 0.05 Rubel.
Verarbeitung der Krypto-Zahlung an die Wallet: 0xABCD...
---
Vorteile:
- Man kann eine neue Zahlungsart hinzufügen, ohne den alten Code zu ändern (z. B. Apple Pay).
- Der Code, der mit Zahlungen arbeitet, hängt nicht von deren konkretem Typ ab.
- Gemeinsame Logik (z. B. das Ausgeben des Betrags über printAmount()) ist an einer Stelle implementiert.
3. Beispiel 2: Transport
Aufgabenstellung
In einem Spiel oder einer Simulation gibt es verschiedene Arten von Verkehrsmitteln: Autos, Fahrräder, Züge. Alle können sich „bewegen“, tun dies aber auf unterschiedliche Weise. Manche müssen betankt werden, andere nicht.
Abstraktion: Klasse Transport
public abstract class Transport {
protected String name;
public Transport(String name) {
this.name = name;
}
public abstract void move();
// Nicht alle Verkehrsmittel müssen betankt werden, standardmäßig: nein
public void fuelUp() {
System.out.println(name + ": Tanken ist nicht erforderlich.");
}
}
Konkrete Implementierungen
public class Car extends Transport {
public Car(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " fährt auf der Straße.");
}
@Override
public void fuelUp() {
System.out.println(name + ": wir tanken Benzin.");
}
}
public class Bicycle extends Transport {
public Bicycle(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " tritt in die Pedale.");
}
// fuelUp wird nicht überschrieben – ein Fahrrad muss nicht betankt werden
}
public class Train extends Transport {
public Train(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " rast auf den Schienen.");
}
@Override
public void fuelUp() {
System.out.println(name + ": wir tanken Diesel oder Strom.");
}
}
Nutzung der Abstraktion
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("Sapsan")
);
for (Transport t : vehicles) {
t.move();
t.fuelUp();
System.out.println("---");
}
}
}
Ausgabe:
Toyota fährt auf der Straße.
Toyota: wir tanken Benzin.
---
Stels tritt in die Pedale.
Stels: Tanken ist nicht erforderlich.
---
Sapsan rast auf den Schienen.
Sapsan: wir tanken Diesel oder Strom.
---
Vorteile:
- Man kann jedes Verkehrsmittel identisch verarbeiten, ohne seinen Typ zu prüfen.
- Es ist einfach, eine neue Art von Transport hinzuzufügen (z. B. E-Scooter).
4. Beispiel 3: Grafikeditor
Aufgabenstellung
Sie schreiben einen Mini-Grafikeditor. Darin gibt es Linien, Ellipsen, Polygone – und all das sind „Formen“, die man zeichnen und in der Größe verändern kann. Dabei implementiert jede Form diese Aktionen auf ihre Weise.
Abstraktion: Klasse 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;
}
}
Konkrete Implementierungen
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("Wir zeichnen eine Linie mit der Länge " + length + " in der Farbe " + color);
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("Neue Linienlänge: " + 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("Wir zeichnen eine Ellipse mit den Achsen " + a + " und " + b + " in der Farbe " + color);
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("Neue Ellipsenmaße: " + 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("Wir zeichnen ein Polygon mit " + sides + " Seiten in der Farbe " + color);
}
@Override
public void resize(double factor) {
System.out.println("Wir ändern die Größe des Polygons mit " + sides + " Seiten um " + factor);
}
}
Nutzung der Abstraktion
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("---");
}
}
}
Ausgabe:
Wir zeichnen eine Linie mit der Länge 10.0 in der Farbe green
Neue Linienlänge: 20.0
---
Wir zeichnen eine Ellipse mit den Achsen 5.0 und 3.0 in der Farbe green
Neue Ellipsenmaße: 10.0 x 6.0
---
Wir zeichnen ein Polygon mit 6 Seiten in der Farbe green
Wir ändern die Größe des Polygons mit 6 Seiten um 2.0
---
Vorteile:
- Alle Formen können in einer Liste gespeichert und identisch verarbeitet werden.
- Es ist einfach, eine neue Form hinzuzufügen (z. B. einen Stern oder ein Herz).
- Gemeinsame Methoden (z. B. das Setzen der Farbe über setColor()) sind einmal implementiert.
5. Wie Abstraktion hilft, Code zu vereinfachen
In jedem der obigen Beispiele gibt es ein gemeinsames Schema:
- Die abstrakte Basisklasse definiert den Vertrag (was ein Objekt kann).
- Konkrete Unterklassen implementieren die Details.
- Der Code, der mit der Abstraktion arbeitet, hängt nicht vom Objekttyp ab, wodurch das System flexibel und erweiterbar wird.
Vergleichstabelle der Ansätze
| Ohne Abstraktion (if-else) | Mit Abstraktion (OOP) |
|---|---|
| Viele Typprüfungen | Neuer Typ – wir ändern den Code |
| Duplizierte Logik | Logik – an einem Ort |
| Schwer zu erweitern | Hinzufügen – leicht |
| Schwer zu testen | Implementierungen leicht austauschbar |
6. Typische Fehler beim Entwerfen von Abstraktionen
Fehler Nr. 1: Abstraktion um der Abstraktion willen.
Wenn Sie nur einen Objekttyp haben und keine Erweiterung geplant ist, ist eine abstrakte Klasse nicht nötig. Man sollte den Code nicht ohne Grund verkomplizieren.
Fehler Nr. 2: Zu allgemeine Abstraktion.
Wenn die Basisklasse zu „verwässert“ ist, haben die Unterklassen nichts gemeinsam außer dem Namen. Zum Beispiel die Abstraktion „Thing“ für alles Mögliche. Das erschwert die Wartung und das Verständnis des Codes.
Fehler Nr. 3: Code-Duplikation in Unterklassen.
Wenn alle Unterklassen dieselbe Implementierung einer Methode haben, sollte man sie in die Basisklasse verschieben (nicht abstrakt machen).
Fehler Nr. 4: Verstoß gegen das Prinzip „vom Allgemeinen zum Speziellen“.
Wenn in der abstrakten Klasse Details auftauchen, die nur eine Unterklasse braucht, ist die Abstraktion falsch gewählt.
Fehler Nr. 5: Abstrakte Methoden zu implementieren vergessen.
Wenn man nicht alle abstrakten Methoden in der Unterklasse implementiert, zwingt der Compiler dazu, die Klasse ebenfalls abstrakt zu machen. Manchmal ist das überraschend :)
GO TO FULL VERSION