Zasada ta mówi o tym że zależności między komponentami nie mogą tworzyć pętli (cykli).

  • Komponent A może zależeć od B.
  • Komponent B może zależeć od C.
  • Ale C (ani żadne inne/kolejne) nie może zależeć od A, bo wtedy A → B → C → A stworzyłyby pętlę.

Czym jest zależność:

Zależność to „wiedza”. Komponent A zależy od komponentu B, jeśli kod w A bezpośrednio „wie” o istnieniu kodu w B.

„Wiedza” w kodzie oznacza jedną z trzech rzeczy:

  1. Import: Klasa w A importuje klasę lub interfejs z B (import com.example.b.SomeClass).
  2. Dziedziczenie: Klasa w A dziedziczy po klasie z B (class A extends B).
  3. Implementacja: Klasa w A implementuje interfejs z B (class A implements B).
graph TD
    UI[UI Controller]
    Service[Account Service]
    Entity[User Entity]
    Repo[User Repository]

    UI --> Service
    Service --> Entity
    Service --> Repo

Przeanalizmy każdą strzałkę, aby zrozumieć, kto „wie” o kim:

  1. UI Controller --> Account Service
    • Kto zależy od kogo? UI Controller zależy od Account Service.
    • Kto wie o kim? UI Controller wie o Account Service. Wywołuje jego metody, aby pobrać dane do wyświetlenia. Bez serwisu kontroler jest bezużyteczny.
  2. Account Service --> User Entity
    • Kto zależy od kogo? Account Service zależy od User Entity.
    • Kto wie o kim? Account Service wie o User Entity. Potrzebuje jej, aby wiedzieć, jak wygląda obiekt użytkownika (jakie ma pola: email, status itp.).
  3. Account Service --> User Repository
    • Kto zależy od kogo? Account Service zależy od User Repository.
    • Kto wie o kim? Account Service wie o User Repository. Potrzebuje go, aby zapisać lub odczytać dane użytkownika z bazy.

Kluczowy Wniosek

Kierunek wiedzy jest jednoznaczny:

  • UI Controller wie o Service.
  • Service wie o Entity i Repository.

Ale User Entity i User Repository nie wiedzą nic o Account Service ani UI Controller.

Czym jest cykl:

Cykl to sytuacja, w której zależności wracają do punktu startowego, tworząc pętlę.

Cykl Bezpośredni:
graph LR
    A["Component A"]
    B["Component B"]

    A --> B
    B --> A

    style A fill:#f99,stroke:#c00,stroke-width:2px
    style B fill:#f99,stroke:#c00,stroke-width:2px
Cykl Pośredni:
graph TD
    A["Component A"]
    B["Component B"]
    C["Component C"]

    A --> B
    B --> C
    C --> A

    style A fill:#f99,stroke:#c00,stroke-width:2px
    style B fill:#f99,stroke:#c00,stroke-width:2px
    style C fill:#f99,stroke:#c00,stroke-width:2px

W obu przypadkach mamy do czynienia z pętlą w zależnościach.

Jak wygląda cykl zależności w kodzie?

// UserService.ts
import { OrderService } from "./OrderService"

// OrderService.ts
import { PaymentService } from "./PaymentService"

// PaymentService.ts
import { UserService } from "./UserService"

Mamy tutaj cykl:

UserService → OrderService → PaymentService → UserService

Co to powoduje?

  • trudniejsze testowanie (żeby przetestować jedno, musisz mieć pół systemu)
  • problemy z inicjalizacją modułów
  • nieprzewidywalne błędy przy refaktoryzacji
  • zmiana w jednym miejscu może wymusić zmiany wszędzie

Konsekwencje istnienia cyklu (pętli):

  1. Problem z budową i wdrażaniem: Jeśli A zależy od B, a B od A, nie możesz zbudować (skompilować) A bez B, ani B bez A. Musisz budować i wdrażać oba komponenty jednocześnie. To niszczy jedną z głównych zalet modularności – możliwość niezależnej pracy nad częściami systemu.
  2. „Efekt domina” przy zmianach: Zmiana w komponencie A może wymusić zmiany w B, które z kolei mogą wymusić zmiany w A… i tak w kółko. System staje się kruchy, a wprowadzanie zmian ryzykowne i nieprzewidywalne.
  3. Trudności w testowaniu: Nie możesz przetestować komponentu A w izolacji, bo do jego uruchomienia potrzebujesz całego „zamkniętego kręgu” zależności.
  4. Brak reużywalności: Komponenty uwikłane w cykl są bardzo trudne do przeniesienia i użycia w innym projekcie.
// W komponencie Orders
public class OrderService {
    private InventoryService inventoryService; // Zależność od Inventory

    public void placeOrder(Long productId) {
        inventoryService.reserveStock(productId);
    }

    public void markAsShipped(Long orderId) { /* ... */ }
}

// W komponencie Inventory
public class InventoryService {
    private OrderService orderService; // Zależność od Orders!

    public void reserveStock(Long productId) { /* ... */ }

    public void shipItem(Long orderId) {
        // ...logika wysyłki...
        orderService.markAsShipped(orderId); // Cykl!
    }
}

Problemy:

  • Nie możesz skompilować Orders bez Inventory i na odwrót. Stracisz modularność.
  • Zmiana w OrderService może złamać InventoryService i odwrotnie. To efekt domina.
  • Test jednostkowy OrderService wymaga podpięcia całego InventoryService, co komplikuje wszystko.

Rozwiązanie: Zasadę Odwrócenia Zależności (Dependency Inversion Principle).

Tworzymy trzeci komponent, np. ApplicationContracts, który przechowuje tylko „umowy”.

// W nowym komponencie ApplicationContracts
public interface IOrderStatusNotifier {
    void markAsShipped(Long orderId);
}

Teraz odwracamy zależność:

// W komponencie Inventory (po zmianie)
public class InventoryService {
    private IOrderStatusNotifier orderNotifier; // Zależy od ABSTRAKCJI

    public void shipItem(Long orderId) {
        // ...logika wysyłki...
        orderNotifier.markAsShipped(orderId); // Wywołanie na interfejsie
    }
}

// W komponencie Orders (po zmianie)
public class OrderService implements IOrderStatusNotifier { // Implementuje umowę
    // ... reszta kodu ...
    
    @Override
    public void markAsShipped(Long orderId) { /* ... */ }
}

Pętla zniknęła. Komponenty są niezależne i można je budować osobno.

Ten wpis należy do serii o architekturze oprogramowania, w której próbuję pokazać, jak powinno się myśleć o programowaniu i architekturze w czasach, gdy kod coraz częściej powstaje przy wsparciu AI.

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *