Wzorzec projektowy Strategia to jedna z kluczowych technik programistycznych i jednocześnie jeden z moich ulubionych, który wiele razy okazał się pomocny w sytuacjach, gdzie napotykałem powtarzający się kod lub gdy chciałem stworzyć funkcjonalność, która nie wymagała od przyszłych programistów zgadywania, co kryje się pod spodem metody. Wzorzec ten pozwala na odpowiednie wywołanie różnych operacji, wykorzystując dostarczony typ interfejsu.
Krótka i formalna informacja co to jest Wzorzec Strategii
Wzorzec strategii w programowaniu Java jest behawioralnym wzorcem projektowym, który umożliwia zdefiniowanie rodziny algorytmów lub strategii, umożliwiając zmianę ich implementacji niezależnie od kodu klienta, który ich używa. Wzorzec ten pomaga w unikaniu nadmiernego kodu powtarzającego się oraz pozwala na elastyczną zmianę zachowania komponentów w trakcie działania programu.
Zalety Wykorzystania Wzorca Strategii
- Rozdzielenie Odpowiedzialności: Wzorzec Strategii pomaga oddzielić różne algorytmy od głównej logiki aplikacji, co ułatwia zarządzanie kodem i jego rozwijanie.
- Łatwa Rozszerzalność: Dodawanie nowych strategii płatności lub algorytmów jest prostsze, ponieważ nie ma konieczności modyfikowania istniejącego kodu.
- Czytelność i Utrzymanie: Kod staje się bardziej zrozumiały, ponieważ logika związana z różnymi strategiami jest izolowana w osobnych klasach.
- Testowanie: Możliwość testowania poszczególnych strategii płatności niezależnie od siebie ułatwia weryfikację poprawności kodu.
Let's code strategy
Zakładając, że w ramach naszej dziedziny biznesowej zajmujemy się obsługą przypadków, naszym zadaniem jest odczytanie plików CSV pochodzących z różnych banków, a następnie przetworzenie ich do formy zgodnej z naszą dziedziną biznesową. Ten proces przygotowuje dane do dalszej obróbki lub zapisu w bazie danych.
Na samym początku stwórzmy sobie DocReaderStrategyFactory
, jest to fabryka która umożliwi nam wybór odpowiedniej strategii zależnie od typu który wskażemy.
@Component
public class DocReaderStrategyFactory {
private final Set<DocReaderStrategy> strategies;
public DocReaderStrategyFactory(Set<DocReaderStrategy> strategySet) {
this.strategies = strategySet;
}
public Optional<DocReaderStrategy> findStrategyFor(DocBankType type) {
return strategies.stream()
.filter(strategy -> strategy.isSuitableFor(type))
.findFirst();
}
}
Wewnątrz klasy mamy kolekcję strategii DocReaderStrategy
, a konstruktor spełnia rolę wzbogacania tej kolekcji poprzez dodawanie obiektów (beany), które implementują dany interfejs. Ponadto, klasa posiada metodę findStrategy
, która umożliwia wybór odpowiedniej strategii na podstawie określonego typu. Zastosowanie obiektu Optional
pozwala na elastyczną obsługę przypadku, gdy nie ma dostępnej odpowiedniej strategii w serwisie, który będzie realizował fabrykę.
Przykładowa implementacja interface
public interface DocReaderStrategy {
List<TransactionDTO> readTransactions(InputStream file);
boolean isSuitableFor(DocBankType type);
}
Przedstawiam prosty interfejs, który zawiera dwie metody. Pierwsza z nich, isSuitableFor
, służy do weryfikacji, czy dany typ jest dla nas interesujący. Druga metoda, readTransactions
, odpowiada za odczyt transakcji z pliku w formacie CSV.
Ten interfejs zapewnia elastyczność poprzez możliwość implementacji różnych strategii do przetwarzania transakcji w zależności od typu pliku oraz dostarcza metody do odczytu transakcji z pliku CSV.
Docelowa klasa imlementacyjna wzorca strategii
@Service
class PKOReader implements DocReaderStrategy {
@Override
public boolean isSuitableFor(DocBankType type) {
return type == DocBankType.PKO;
}
@Override
public List<TransactionDTO> readTransactions(InputStream file) {
var workbook = new HSSFWorkbook();
try {
workbook = new HSSFWorkbook(file);
Sheet sheet = workbook.getSheetAt(0);
for (Row row : sheet) {
for (Cell cell : row) {
//code
}
}
workbook.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
//code
}
}
Powyższy przykład to już klasa, która docelowo będzie posiadała logiczną strukturę opartą na wzorcu stratergii i zawierającą logike biznesową odpowiedzialną za przetwarzanie konkretnego pliku CSV.
Pierwsza metoda, isSuitableForreadTransactions
, to właściwa część procesu, gdzie następuje obróbka danych oraz zwracana jest konkretne implementacje obiektów.
Przykładowe zastosowanie
@Override
public List<Transaction> readDoc(InputStream file, DocBankType type) {
return docReaderStrategyFactory.findStrategyFor(type)
.orElseThrow(() -> new RuntimeException("No suitable strategy found for " + type))
.readTransactions(file)
.stream()
.map(transactionMapper::toDomain)
.collect(Collectors.toCollection(List::of));
}
Powyższa implementacja potrzebuje jedynie pliku wejściowego oraz określenia oczekiwanego typu obsługi, co czyni ją niezwykle prostą, a jednocześnie zaawansowaną pod względem funkcjonalności biznesowych.
Podsumowanie
Wzorzec Strategii to wartościowa technika programistyczna, która pozwala na elastyczne i efektywne zarządzanie różnymi algorytmami w aplikacji. W Javie można wykorzystać ten wzorzec do oddzielenia i wymiany różnych strategii w przejrzysty sposób. Dzięki temu kod staje się bardziej zorganizowany, łatwiejszy w utrzymaniu i rozszerzalny, co przyczynia się do wydajnego i skalowalnego projektowania oprogramowania.