"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.



