"Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification"
Analiza dotyczyła będzie rozbudowania kodu o nową funkcjonalność bez modyfikowania istniejących klas. Spotkałem się z sytuacją gdzie klasa budująca model domenowy na podstawie danych wyciągniętych z bazy danych powinna też tworzyć całe kolekcje takich modeli. Można by:
zmodyfikować klasę budowniczego dodając nową metodę buildCollection(int ...$ids): array,
istniejącą metodę budowniczego build($id): Model wywołać w pętli w klasie klienta,
Wiele zapytań do bazy
Drugie rozwiązanie z perspektywy zasady Open/Closed wydaje się lepsze bo nie wymaga od nas modyfikacji istniejącego kodu, ale wiąże się z nim pewien problem. W ciele metody Builder::build wykonywane jest zapytanie do bazy danych (RepositoryInterface::featchById) przez co im więcej obiektów do utworzenia tym gorzej dla optymalności rozwiązania.
Jedno zapytanie do bazy
Można zaimplementować jednak rozwiązanie nie ingerujące w istniejący kod wykonujące tylko jedno zapytanie do bazy. Dzięki temu, że klasa Builder posiada wiedzę tylko o interfejsie repozytorium możemy utworzyć kolejną implementację RepositoryInterface - InMemoryRepository.
InMemoryRepository posiada naprawdę prosta implementację i to na jej podstawie będzie tworzony obiekt Builder w klasie klienta zwracającego kolekcje modeli.
final class InMemoryRepository implements RepositoryInterface { /** * @param Row[] $rows */ public function __construct(private array $rows) {} public function featchById($id): ?Row { return $this->rows[$id] ?? null; } /** * @return Row[] */ public function featchByIds(int ...$ids): array { return array_filter( $this->rows, static fn (Row $row): bool => in_array($row->id(), $ids); ); } }
Pozostaje tylko napełnić instancję InMemoryRepository danymi z już istniejącego PDORepository.
Query: GetModel (dotychczasowe)
Istniejąca funkcjonalność pobierania tylko jednego modelu zamknięta jest w klasie GetModel która realizuje tą funkcjonalność na podstawie obiektu Builder pobranego z kontenera zależności.
Odbywa się tu tylko jedna istotna czynność:
$this->builder->build($id);
Query: GetModelCollection (nowe)
W przeciwieństwie do poprzedniej klasy GetModel tutaj będziemy bezpośrednio operować na PDORepository (pod przykrywką RepositoryInterface). Dzieje się tutaj o wiele więcej niż w poprzednim przypadku:
- pobranie danych z bazy RepositoryInterface::featchByIds,
- utworzenie na ich podstawie instancji InMemoryRepository,
- utworzenie instancji Builder na podstawie InMemoryRepository,
- wywołanie metody Builder::build w pętli.
/** * @return Model[] */ public function __invoke(int ...$ids): array { $builder = new Builder( new InMemoryRepository( $this->repository->featchByIds(...$ids) ) ); return array_map([$builder, 'build'], $ids); }
Podsumowanie
- ✅ Nie zmodyfikowaliśmy żadnej istniejącej klasy,
- ✅ nowa funkcjonalność bazuje na działających już wcześniej klasach,
- ❌ kolejne użycie słowa kluczowego "new" dla klasy Builder,
- ❌ Repository::fetchByIds musi być idealnie zgrane z wywołaniem Builder::build() w pętli. Repository musi dawać gwarancję, że zawsze zwracany jest komplet danych, w przeciwnym razie powinien wyrzucać wyjątek @throws NotFoundAllIds
Dzięki takiej implementacji nie ingerowaliśmy w działający koda, a do nowej funkcjonalności wykorzystaliśmy sprawdzone/przetestowane klasy. Dodatkowo rozwiązanie jest całkowicie odseparowane (nowa klasa Query) dzięki temu w całości zachowaliśmy funkcjonalność pobierania pojedynczego modelu.
Takie podejście uwydatnia swoje zalety gdy proces budowania modelu w budowniczym jest skomplikowany a sam Builder posiada wiele zależności.