CodeGym /Kursy /JAVA 25 SELF /Przykłady tworzenia abstrakcji w rzeczywistych zadaniach

Przykłady tworzenia abstrakcji w rzeczywistych zadaniach

JAVA 25 SELF
Poziom 19, Lekcja 3
Dostępny

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 :)

1
Zadanie
JAVA 25 SELF, poziom 19, lekcja 3
Niedostępne
Obliczanie pola w aplikacji graficznej 🎨
Obliczanie pola w aplikacji graficznej 🎨
1
Zadanie
JAVA 25 SELF, poziom 19, lekcja 3
Niedostępne
Uniwersalny system wysyłania wiadomości 📧💬
Uniwersalny system wysyłania wiadomości 📧💬
1
Zadanie
JAVA 25 SELF, poziom 19, lekcja 3
Niedostępne
Zarządzanie flotą firmy 🚚🚲
Zarządzanie flotą firmy 🚚🚲
1
Zadanie
JAVA 25 SELF, poziom 19, lekcja 3
Niedostępne
Moduł obsługi zamówień w sklepie detalicznym 🛒
Moduł obsługi zamówień w sklepie detalicznym 🛒
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION