Wzorzec Fasady i Delegacje jako sposób na uporządkowanie chaosu
Gdy projekty rozrastają się, klasy testowe często przekształcają się w monolityczne kolosy pełne powtarzającego się kodu. W tym artykule omówimy strategię refaktoryzacji dużych plików z akcjami testowymi, wykorzystując wzorzec fasady i delegacje, aby utrzymać porządek i skalowalność.
Stan przed refaktoryzacją: anatomia chaosu
Analizując dostarczony kod, widzimy typowy przykład problemu - klasa SaleActions w pliku old-actions.ts liczy ponad 1500 linii kodu. Jest to klasyczny “God Object” zawierający:
- Metody nawigacji interfejsu
- Metody wypełniania formularzy
- Obliczenia cenowe i rabatowe
- Akcje zatwierdzania i weryfikacji
- Manipulacje koszykiem zakupowym
- Obsługę płatności
Problemy takiego podejścia są oczywiste:
- Trudność w utrzymaniu - pojedyncza zmiana wymaga zrozumienia całej klasy
- Ryzyko konfliktów - wielu deweloperów pracujących nad tym samym plikiem
- Trudność w debugowaniu - problemy są trudne do zlokalizowania
- Naruszenie zasad SOLID - szczególnie Single Responsibility Principle
- Bariera wejścia - nowi członkowie zespołu czują się przytłoczeni
Architektura po refaktoryzacji: fasada i delegacje
Refaktoryzacja wprowadza wzorzec fasady, zachowując istniejący interfejs klasy SaleActions, ale delegując konkretne operacje do wyspecjalizowanych klas:
export class SaleActions {
// Moduły z nowej struktury
private basketTableManager: BasketTableManager;
private processActions: ProcessActions.SaleProcessActions;
private paymentActions: ProcessActions.PaymentActions;
private contractorActions: ProcessActions.ContractorActions;
private itemPriceActions: ItemActions.ItemPriceActions;
private itemDiscountActions: ItemActions.ItemDiscountActions;
private itemManagementActions: ItemActions.ItemManagementActions;
private loaderActions: CommonActions.LoaderActions;
private uiActions: CommonActions.UIActions;
constructor(private page: Page) {
// Inicjalizacja wszystkich modułów...
}
// Delegacje do odpowiednich modułów...
async collectBasketData(): Promise<BasketItem[]> {
return this.basketTableManager.collectBasketData();
}
async createNewSale(): Promise<void> {
return this.processActions.createNewSale();
}
// Inne delegacje...
}
Powyższy kod pokazuje główną klasę SaleActions, która działa teraz jako fasada. Zamiast implementować wszystkie metody, deleguje wywołania do wyspecjalizowanych klas:
- BasketTableManager - zarządzanie tabelą koszyka
- ProcessActions - procesy sprzedaży i płatności
- ItemActions - zarządzanie elementami, cenami i rabatami
- CommonActions - wspólne operacje UI i ładowania
Zalety nowego podejścia
1. Łatwiejsze utrzymanie i rozwój
Każda klasa ma teraz jasno określoną odpowiedzialność, co ułatwia znajdowanie i modyfikowanie kodu. Nowe funkcjonalności można dodawać w odpowiednich modułach bez dotykania całego systemu.
2. Lepsza organizacja kodu
Struktura modułowa pozwala łatwiej zrozumieć system:
SaleActions/
├── common/
│ ├── loader-actions.ts
│ └── ui-actions.ts
├── item/
│ ├── item-discount-actions.ts
│ ├── item-management-actions.ts
│ └── item-price-actions.ts
├── process/
│ ├── contractor-actions.ts
│ ├── payment-actions.ts
│ └── sale-process-actions.ts
└── table/
├── basket-action-executor.ts
├── basket-data-extractor.ts
├── basket-table-manager.ts
└── basket-table-navigator.ts
3. Łatwiejsze testowanie
Mniejsze, wyspecjalizowane klasy są łatwiejsze do testowania jednostkowego. Możemy teraz testować działanie BasketDataExtractor niezależnie od reszty systemu.
4. Zgodność z SOLID
- Single Responsibility - każda klasa ma jedną odpowiedzialność
- Open/Closed - rozszerzanie funkcjonalności bez modyfikacji istniejącego kodu
- Liskov Substitution - interfejsy umożliwiają podmiany implementacji
- Interface Segregation - małe, dedykowane interfejsy
- Dependency Inversion - zależności poprzez abstrakcje
5. Ułatwione wdrażanie nowych członków zespołu
Nowi deweloperzy mogą skupić się na zrozumieniu jednego modułu, zamiast całego systemu.
Potencjalne wady i wyzwania
1. Złożoność struktury
Wprowadzenie wielu klas i interfejsów zwiększa złożoność strukturalną projektu. Może to utrudniać zrozumienie przepływu danych dla osób niezaznajomionych z wzorcem.
2. Koszty refaktoryzacji
Przekształcenie istniejącego systemu wymaga czasu i uwagi. Istnieje ryzyko wprowadzenia błędów podczas przenoszenia kodu.
3. Potencjalna nadmiarowość
Wprowadzenie delegacji może prowadzić do nadmiernej warstwy pośredniej:
// Przykład potencjalnej nadmiarowości
async createNewSale(): Promise<void> {
return this.processActions.createNewSale();
}
4. Zarządzanie stanem
Rozproszone klasy mogą utrudniać zarządzanie współdzielonym stanem. Konieczne może być wprowadzenie mechanizmów synchronizacji.
Strategiczne podejście do refaktoryzacji
1. Analiza istniejącego kodu
Zanim zaczniesz, przeanalizuj istniejący kod, identyfikując naturalne klastry funkcjonalności. W naszym przypadku wyodrębniliśmy operacje na tabeli, zarządzanie elementami i procesy sprzedaży.
2. Stopniowa implementacja
Zamiast refaktoryzować wszystko naraz, lepiej pracować iteracyjnie:
- Wyodrębnij jedną grupę funkcjonalności (np. operacje na tabeli)
- Zbuduj nową klasę i przenieś do niej kod
- Zastosuj delegacje w głównej klasie
- Uruchom testy, aby upewnić się, że wszystko działa poprawnie
- Przejdź do kolejnej grupy funkcjonalności
3. Budowanie na interfejsach
Wykorzystaj interfejsy do określenia kontraktów między komponentami:
export interface TableActionExecutor {
openRowMenu(rowIndex: string | number): Promise<void>;
executeAction(actionId: string): Promise<void>;
// inne metody...
}
export class BasketActionExecutor implements TableActionExecutor {
// implementacja...
}
4. Zachowanie kompatybilności
Zasadnicze jest zachowanie istniejącego interfejsu publicznego głównej klasy, aby testy nie wymagały modyfikacji:
// Przed refaktoryzacją
await saleActions.clickChangePriceButton();
// Po refaktoryzacji - taki sam interfejs, inna implementacja
async clickChangePriceButton(): Promise<void> {
return this.itemPriceActions.clickChangePriceButton();
}
DRY vs YAGNI w kontekście refaktoryzacji
Podczas refaktoryzacji często napotykamy napięcie między zasadami DRY (Don’t Repeat Yourself) i YAGNI (You Aren’t Gonna Need It):
DRY: Eliminacja duplikacji kodu prowadzi do tworzenia abstrakcji, co widać w naszych klasach BasketTableNavigator czy BasketDataExtractor.
YAGNI: Nadmierna abstrakcja może prowadzić do niepotrzebnej złożoności. Czasem prosta metoda w jednej klasie jest lepsza niż skomplikowana hierarchia klas.
Rozsądne podejście to:
- Eliminacja oczywistych duplikacji
- Tworzenie abstrakcji tylko tam, gdzie przynoszą wyraźne korzyści
- Opóźnienie tworzenia zaawansowanych abstrakcji do momentu, gdy wzorce staną się jasne
Wnioski praktyczne
-
Zacznij od jasnego planu - mapa refaktoryzacji pomoże utrzymać kierunek zmian
-
Testuj na bieżąco - każda zmiana powinna być weryfikowana przez testy
-
Dokumentuj zmiany - dobrze napisane komentarze i dokumentacja ułatwią zrozumienie nowej struktury
-
Rozważ użycie narzędzi - automatyzacja może pomóc w bezpiecznej refaktoryzacji
-
Komunikuj zmiany zespołowi - wszyscy powinni rozumieć nową architekturę
Podsumowanie
Wzorzec fasady i delegacje stanowią potężne narzędzie w refaktoryzacji dużych, monolitycznych klas testowych. Choć proces ten wymaga starannego planowania i wykonania, korzyści w postaci łatwiejszego utrzymania, testowania i rozszerzania kodu są warte wysiłku.
Pamiętajmy jednak, że refaktoryzacja nie jest celem samym w sobie, ale środkiem do tworzenia lepszego, bardziej utrzymywalnego kodu. Kluczem jest znalezienie równowagi między idealną abstrakcją a praktyczną użytecznością, mając zawsze na uwadze potrzeby zespołu i projektu.