1. Abstrakcja w realnych aplikacjach: po co jest potrzebna
W poprzednich wykładach poznaliśmy już pojęcie abstrakcji i przejrzeliśmy proste przykłady. Teraz zobaczymy, jak to podejście działa w bardziej rzeczywistych zadaniach. W prawdziwych projektach niemal zawsze mamy do czynienia z różnymi, lecz podobnymi obiektami. Na przykład różne sposoby płatności, różne środki transportu, różne figury w edytorze graficznym. Jeśli nie stosować abstrakcji, kod szybko zamienia się w zestaw „if-else” i kopiuj‑wklej. Z abstrakcją — wszystko jest uporządkowane, klarowne i, co najważniejsze, wygodne w rozwoju i utrzymaniu.
Abstrakcja pozwala:
- Ukryć szczegóły implementacji: pracować z obiektami przez wspólny interfejs, nie wnikając w to, jak są zbudowane w środku.
- Uniknąć duplikowania kodu: wspólne zachowania i pola są wynoszone do klasy bazowej.
- Łatwo rozszerzać system: dodanie nowych typów obiektów nie wymaga przepisywania starego kodu.
- Uczynić kod elastycznym: można podmienić jedną implementację na inną bez zmiany reszty systemu.
Przeanalizujmy kilka przykładów z różnych obszarów.
2. Przykład 1: Systemy płatności
Zadanie
Załóżmy, że piszesz moduł dla sklepu internetowego. Twoim zadaniem jest obsługa różnych rodzajów płatności: karta płatnicza, PayPal, kryptowaluta. Wszystkie one powinny umieć „przetwarzać płatność”, ale szczegóły są w każdym przypadku inne.
Abstrakcja: klasa Payment
public abstract class Payment {
protected double amount;
public Payment(double amount) {
this.amount = amount;
}
// Metoda abstrakcyjna: to, jak dokładnie przetwarzać płatność, decydują klasy potomne
public abstract void process();
// Wspólna metoda dla wszystkich płatności
public void printAmount() {
System.out.println("Kwota płatności: " + amount + " RUB.");
}
}
Konkretnie implementacje
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("Przetwarzanie płatności kartą: " + cardNumber);
// Tu mogłaby być integracja z bankiem :)
}
}
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("Przetwarzanie płatności PayPal dla konta: " + email);
// A tutaj — wywołanie 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("Przetwarzanie płatności kryptowalutą na portfel: " + walletAddress);
// Tutaj mogłaby być magia blockchaina
}
}
Użycie abstrakcji
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("---");
}
}
}
Wynik działania:
Kwota płatności: 1500.0 RUB.
Przetwarzanie płatności kartą: 1234 5678 9012 3456
---
Kwota płatności: 500.0 RUB.
Przetwarzanie płatności PayPal dla konta: user@example.com
---
Kwota płatności: 0.05 RUB.
Przetwarzanie płatności kryptowalutą na portfel: 0xABCD...
---
Zalety:
- Można dodać nowy sposób płatności, nie zmieniając starego kodu (np. Apple Pay).
- Kod, który pracuje z płatnościami, nie zależy od ich konkretnego typu.
- Wspólna logika (np. drukowanie kwoty przez printAmount()) jest zaimplementowana w jednym miejscu.
3. Przykład 2: Transport
Zadanie
W grze lub symulacji masz różne środki transportu: samochody, rowery, pociągi. Wszystkie one potrafią się „poruszać”, ale robią to na różne sposoby. Niektóre wymagają tankowania, inne — nie.
Abstrakcja: klasa Transport
public abstract class Transport {
protected String name;
public Transport(String name) {
this.name = name;
}
public abstract void move();
// Nie wszystkie środki transportu wymagają tankowania, domyślnie — nie
public void fuelUp() {
System.out.println(name + ": tankowanie nie jest wymagane.");
}
}
Konkretnie implementacje
public class Car extends Transport {
public Car(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " jedzie po drodze.");
}
@Override
public void fuelUp() {
System.out.println(name + ": tankujemy benzyną.");
}
}
public class Bicycle extends Transport {
public Bicycle(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " kręci pedałami.");
}
// nie nadpisujemy fuelUp — rower nie potrzebuje tankowania
}
public class Train extends Transport {
public Train(String name) {
super(name);
}
@Override
public void move() {
System.out.println(name + " pędzi po torach.");
}
@Override
public void fuelUp() {
System.out.println(name + ": tankujemy dieslem lub energią elektryczną.");
}
}
Użycie abstrakcji
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("---");
}
}
}
Wynik działania:
Toyota jedzie po drodze.
Toyota: tankujemy benzyną.
---
Stels kręci pedałami.
Stels: tankowanie nie jest wymagane.
---
Sapsan pędzi po torach.
Sapsan: tankujemy dieslem lub energią elektryczną.
---
Zalety:
- Można obsługiwać każdy środek transportu jednakowo, nie sprawdzając jego typu.
- Łatwo dodać nowy rodzaj transportu (np. hulajnogę elektryczną).
4. Przykład 3: Edytor graficzny
Zadanie
Piszesz mini‑edytor graficzny. Są w nim linie, elipsy, wielokąty — i wszystko to to „figury”, które można narysować i zmienić rozmiar. Jednocześnie każda figura realizuje te działania na swój sposób.
Abstrakcja: klasa 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;
}
}
Konkretnie implementacje
public class Line extends Figure {
private double length;
public Line(double length) {
this.length = length;
}
@Override
public void draw() {
System.out.println("Rysujemy linię o długości " + length + " kolorem " + color);
}
@Override
public void resize(double factor) {
length *= factor;
System.out.println("Nowa długość linii: " + 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("Rysujemy elipsę z osiami " + a + " i " + b + " kolorem " + color);
}
@Override
public void resize(double factor) {
a *= factor;
b *= factor;
System.out.println("Nowe wymiary elipsy: " + 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("Rysujemy wielokąt z " + sides + " bokami kolorem " + color);
}
@Override
public void resize(double factor) {
System.out.println("Zmieniamy rozmiar wielokąta z " + sides + " bokami o " + factor);
}
}
Użycie abstrakcji
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("---");
}
}
}
Wynik działania:
Rysujemy linię o długości 10.0 kolorem green
Nowa długość linii: 20.0
---
Rysujemy elipsę z osiami 5.0 i 3.0 kolorem green
Nowe wymiary elipsy: 10.0 x 6.0
---
Rysujemy wielokąt z 6 bokami kolorem green
Zmieniamy rozmiar wielokąta z 6 bokami o 2.0
---
Zalety:
- Wszystkie figury można przechowywać w jednej liście i obsługiwać jednakowo.
- Łatwo dodać nową figurę (np. gwiazdę lub serce).
- Wspólne metody (np. ustawienie koloru przez setColor()) są zaimplementowane jeden raz.
5. Jak abstrakcja pomaga uprościć kod
W każdym z powyższych przykładów występuje wspólny schemat:
- Bazowa klasa abstrakcyjna definiuje kontrakt (co obiekt potrafi).
- Konkretnie potomkowie implementują szczegóły.
- Kod pracujący z abstrakcją nie zależy od typu obiektu, co czyni system elastycznym i łatwym do rozszerzania.
Porównanie podejść
| Bez abstrakcji (if-else) | Z abstrakcją (OOP) |
|---|---|
| Dużo warunków po typie | Nowy typ — zmieniamy kod |
| Duplikacja logiki | Logika — w jednym miejscu |
| Trudno rozszerzać | Dodawanie — łatwe |
| Trudno testować | Łatwo podmieniać implementacje |
6. Typowe błędy przy projektowaniu abstrakcji
Błąd nr 1: Abstrakcja dla samej abstrakcji.
Jeśli masz tylko jeden typ obiektu i nie planujesz rozszerzeń — klasa abstrakcyjna nie jest potrzebna. Nie warto komplikować kodu bez powodu.
Błąd nr 2: Zbyt ogólna abstrakcja.
Jeśli klasa bazowa jest zbyt „rozmyta”, potomkowie mogą nie mieć nic wspólnego poza nazwą. Na przykład abstrakcja „Thing” do wszystkiego. To utrudnia utrzymanie i zrozumienie kodu.
Błąd nr 3: Duplikowanie kodu w klasach pochodnych.
Jeśli wszyscy potomkowie mają identyczną implementację metody, warto wynieść ją do klasy bazowej (zrobić nieabstrakcyjną).
Błąd nr 4: Naruszenie zasady „od ogółu do szczegółu”.
Jeśli w klasie abstrakcyjnej pojawiają się detale potrzebne tylko jednemu potomkowi — znaczy, że abstrakcja została dobrana niewłaściwie.
Błąd nr 5: Zapomniano zaimplementować metody abstrakcyjne.
Jeśli nie zaimplementujesz wszystkich metod abstrakcyjnych w potomku, kompilator wymusi, by klasa również była abstrakcyjna. Czasem to bywa zaskakujące :)
GO TO FULL VERSION