CodeGym /Kursy /JAVA 25 SELF /Interfejsy w architekturze Javy, wzorce projektowe

Interfejsy w architekturze Javy, wzorce projektowe

JAVA 25 SELF
Poziom 20 , Lekcja 4
Dostępny

1. Interfejsy jako kontrakt: fundament architektury

W Javie (i nie tylko) interfejs — to nie tylko zbiór metod. To kontrakt: obietnica, że każda klasa implementująca interfejs zapewnia określone zachowanie. Interfejs definiuje, co ma być zaimplementowane, a nie jak.

Dlaczego to jest ważne?

  • Podział kodu na warstwy. Dzięki interfejsom możemy oddzielić „co robi” od „jak robi”. Na przykład, jeśli masz interfejs PaymentService, różne implementacje mogą obsługiwać płatność kartą, PayPalem lub kryptowalutą, ale kod, który wywołuje pay(), nie musi znać szczegółów.
  • Elastyczność i rozszerzalność. Możesz dodać nową implementację interfejsu bez zmiany reszty kodu. Jest to szczególnie ważne w dużych zespołach i długowiecznych projektach.
  • Testowalność. Dzięki interfejsom łatwo podmieniać implementacje na testowe (mocki), nie dotykając kodu produkcyjnego.

Przykład: warstwa serwisowa i DAO

Rozważmy klasyczny przykład z aplikacji biznesowych. Załóżmy, że mamy interfejs do pracy z użytkownikami:


public interface UserRepository {
    User findById(int id);
    void save(User user);
}

W różnych sytuacjach możemy zaimplementować ten interfejs na różne sposoby:

  • DatabaseUserRepository — przechowuje użytkowników w bazie danych.
  • InMemoryUserRepository — przechowuje użytkowników w pamięci (wygodne do testów).
  • FileUserRepository — zapisuje użytkowników w pliku.

Kod pracujący z użytkownikami zależy tylko od interfejsu:

public class UserService {
    private final UserRepository userRepository;

    // Wstrzykiwanie zależności przez konstruktor
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public void registerUser(User user) {
        userRepository.save(user);
    }
}

Teraz możemy łatwo podmieniać implementację UserRepository bez zmiany kodu serwisu.

2. Dependency Injection (wstrzykiwanie zależności) i rola interfejsów

Dependency Injection (DI, wstrzykiwanie zależności) — to podejście architektoniczne, w którym zależności (np. implementacje interfejsów) są przekazywane obiektowi z zewnątrz, zwykle przez konstruktor lub setter. Pozwala to budować elastyczne, łatwe w testowaniu i rozszerzalne aplikacje.

Dlaczego interfejsy są ważne dla DI?

Gdybyśmy na sztywno wpisywali implementację w kodzie, jej wymiana byłaby trudna. Dzięki interfejsom możemy podstawiać dowolną implementację bez zmiany kodu głównego.

Przykład z wstrzykiwaniem zależności

public interface NotificationSender {
    void send(String message);
}

public class EmailNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Wysyłanie e-maila: " + message);
    }
}

public class SmsNotificationSender implements NotificationSender {
    @Override
    public void send(String message) {
        System.out.println("Wysyłanie SMS-a: " + message);
    }
}

// Klasa, która używa NotificationSender
public class NotificationService {
    private final NotificationSender sender;

    public NotificationService(NotificationSender sender) {
        this.sender = sender;
    }

    public void notifyUser(String message) {
        sender.send(message);
    }
}

Teraz możesz łatwo przetestować NotificationService, przekazując mu np. „atrapę” zamiast rzeczywistego nadawcy wiadomości.

3. Wzorce projektowe i interfejsy

Interfejsy to nie tylko architektura, ale też wzorce projektowe. Wiele wzorców nie da się zrealizować bez interfejsów. Przyjrzyjmy się najpopularniejszym.

Observer (Obserwator)

Observer — wzorzec, który pozwala obiektowi (obserwowanemu) powiadamiać inne obiekty (obserwatorów) o zmianach swojego stanu.

Diagram UML (uproszczony):

+------------------+         +------------------------+
|   Subject        |<------->|   Observer             |
+------------------+         +------------------------+
| +addObserver()   |         | +update()              |
| +removeObserver()|         +------------------------+
| +notifyObservers()|
+------------------+

Przykład kodu:

import java.util.ArrayList;
import java.util.List;

// Interfejs obserwatora
public interface Observer {
    void update(String event);
}

// Interfejs podmiotu
public interface Subject {
    void addObserver(Observer observer);
    void removeObserver(Observer observer);
    void notifyObservers(String event);
}

// Implementacja podmiotu
public class NewsAgency implements Subject {
    private List<Observer> observers = new ArrayList<>();

    @Override
    public void addObserver(Observer observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(Observer observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers(String event) {
        for (Observer observer : observers) {
            observer.update(event);
        }
    }
}

// Implementacja obserwatora
public class NewsReader implements Observer {
    private String name;

    public NewsReader(String name) {
        this.name = name;
    }

    @Override
    public void update(String event) {
        System.out.println(name + " otrzymał wiadomość: " + event);
    }
}

// Główna klasa do uruchomienia przykładu
public class ObserverExample {
    public static void main(String[] args) {
        // Tworzymy "agencję informacyjną" (podmiot)
        NewsAgency agency = new NewsAgency();

        // Tworzymy obserwatorów
        Observer alice = new NewsReader("Alisa");
        Observer bob = new NewsReader("Bob");

        // Dodajemy obserwatorów
        agency.addObserver(alice);
        agency.addObserver(bob);

        // Wysyłamy wiadomość
        agency.notifyObservers("Wyszła nowa wersja Javy!");

        // Usuwamy jednego obserwatora i wysyłamy jeszcze jedną wiadomość
        agency.removeObserver(bob);
        agency.notifyObservers("Kolejna wiadomość dla subskrybentów");
    }
}

Wynik:

Alisa otrzymał wiadomość: Wyszła nowa wersja Javy!
Bob otrzymał wiadomość: Wyszła nowa wersja Javy!

Strategy (Strategia)

Strategy — wzorzec pozwalający wybrać algorytm zachowania w czasie wykonywania, bez zmiany kodu klienta.

Diagram UML (uproszczony):

+------------------+
|   Context        |
+------------------+
| -strategy: Strat.|
| +setStrategy()   |
| +execute()       |
+------------------+
         |
         v
+------------------+
|   Strategy       |<-------------------------+
+------------------+                          |
| +execute()       |                          |
+------------------+                          |
         ^                                    |
         |                                    |
+------------------+   +------------------+   |
|   ConcreteA      |   |   ConcreteB      |---+
+------------------+   +------------------+
| +execute()       |   | +execute()       |
+------------------+   +------------------+

Przykład kodu:

public interface PaymentStrategy {
    void pay(int amount);
}

public class CreditCardPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Płatność " + amount + " rub. kartą kredytową");
    }
}

public class PaypalPayment implements PaymentStrategy {
    @Override
    public void pay(int amount) {
        System.out.println("Płatność " + amount + " rub. przez PayPal");
    }
}

public class OnlineStore {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy strategy) {
        this.paymentStrategy = strategy;
    }

    public void checkout(int amount) {
        paymentStrategy.pay(amount);
    }
}

// Użycie:
OnlineStore store = new OnlineStore();
store.setPaymentStrategy(new CreditCardPayment());
store.checkout(1000);

store.setPaymentStrategy(new PaypalPayment());
store.checkout(500);

Wynik:

Płatność 1000 rub. kartą kredytową
Płatność 500 rub. przez PayPal

Command (Polecenie)

Command — wzorzec enkapsulujący żądanie jako obiekt, umożliwiający przekazywanie działań jako parametrów.

Przykład kodu:

public interface Command {
    void execute();
}

public class LightOnCommand implements Command {
    @Override
    public void execute() {
        System.out.println("Światło włączone!");
    }
}

public class LightOffCommand implements Command {
    @Override
    public void execute() {
        System.out.println("Światło wyłączone!");
    }
}

public class RemoteControl {
    private Command command;

    public void setCommand(Command command) {
        this.command = command;
    }

    public void pressButton() {
        command.execute();
    }
}

// Użycie:
RemoteControl remote = new RemoteControl();
remote.setCommand(new LightOnCommand());
remote.pressButton(); // Światło włączone!

remote.setCommand(new LightOffCommand());
remote.pressButton(); // Światło wyłączone!

4. Zalety używania interfejsów w architekturze

  • Słabe powiązanie (Low coupling). Kod zależy tylko od interfejsu, a nie od konkretnej implementacji. To ułatwia podmianę, testowanie i rozszerzanie.
  • Testowalność. Łatwo podmienić prawdziwą implementację na testową (mock/stub) podczas pisania testów jednostkowych.
  • Rozszerzalność. Można dodawać nowe implementacje interfejsu bez zmiany istniejącego kodu — zasada otwartości/zamkniętości (OCP).
  • Równoległy rozwój. Kilka zespołów może niezależnie implementować różne części systemu, jeśli mają wspólny interfejs.
  • Elastyczność architektury. Łatwo wdrażać nowe wzorce i podejścia.

5. Typowe błędy przy używaniu interfejsów w architekturze

Błąd nr 1: Sztywne powiązanie z implementacją.
Jeśli wszędzie używasz konkretnych klas zamiast interfejsów, każda zmiana implementacji będzie wymagała przeróbek w wielu miejscach. Zawsze staraj się programować „na poziomie interfejsów”.

Błąd nr 2: Zbyt duże interfejsy (God Interface).
Interfejs powinien być zwięzły i odpowiadać za jeden obszar odpowiedzialności. Nie warto upychać do jednego interfejsu wszystkiego — inaczej implementacja stanie się ciężka i zawiła.

Błąd nr 3: Ignorowanie zalet testowalności.
Jeśli nie używasz interfejsów do podmiany zależności w testach, testy mogą być wolne i zawodne, zwłaszcza gdy pracują na prawdziwych bazach danych lub sieciach.

Błąd nr 4: Wiele implementacji, ale brak DI.
Jeśli stworzyłeś kilka implementacji interfejsu, ale na sztywno wpisałeś jedną z nich w kodzie, tracisz całą elastyczność architektury. Używaj wstrzykiwania zależności (DI)!

1
Ankieta/quiz
Interfejsy, poziom 20, lekcja 4
Niedostępny
Interfejsy
Pojęcie interfejsu
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION