Zasada ta mówi o tym że zależności między komponentami nie mogą tworzyć pętli (cykli).
- Komponent
Amoże zależeć odB. - Komponent
Bmoże zależeć odC. - Ale
C(ani żadne inne/kolejne) nie może zależeć odA, bo wtedyA → B → C → Astworzył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:
- Import: Klasa w A importuje klasę lub interfejs z B (
import com.example.b.SomeClass). - Dziedziczenie: Klasa w A dziedziczy po klasie z B (
class A extends B). - 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 --> RepoPrzeanalizmy każdą strzałkę, aby zrozumieć, kto „wie” o kim:
UI Controller --> Account Service- Kto zależy od kogo?
UI Controllerzależy odAccount Service. - Kto wie o kim?
UI Controllerwie oAccount Service. Wywołuje jego metody, aby pobrać dane do wyświetlenia. Bez serwisu kontroler jest bezużyteczny.
- Kto zależy od kogo?
Account Service --> User Entity- Kto zależy od kogo?
Account Servicezależy odUser Entity. - Kto wie o kim?
Account Servicewie oUser Entity. Potrzebuje jej, aby wiedzieć, jak wygląda obiekt użytkownika (jakie ma pola:email,statusitp.).
- Kto zależy od kogo?
Account Service --> User Repository- Kto zależy od kogo?
Account Servicezależy odUser Repository. - Kto wie o kim?
Account Servicewie oUser Repository. Potrzebuje go, aby zapisać lub odczytać dane użytkownika z bazy.
- Kto zależy od kogo?
Kluczowy Wniosek
Kierunek wiedzy jest jednoznaczny:
UI Controllerwie oService.Servicewie oEntityiRepository.
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:2pxCykl 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:2pxW 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):
- 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.
- „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.
- 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.
- 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ć
OrdersbezInventoryi na odwrót. Stracisz modularność. - Zmiana w
OrderServicemoże złamaćInventoryServicei odwrotnie. To efekt domina. - Test jednostkowy
OrderServicewymaga podpięcia całegoInventoryService, 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.
