Публічний пошук у ICanUp працює з Posts, Categories, News і Tips. У кожного типу свій запит, свої relations і свої правила публічної видимості.
Якби вся ця логіка жила в одному центральному service, кожен новий тип контенту додавав би ще одну умову в код, який уже працює.
Замість цього спільний механізм пошуку працює через один interface, а змінна поведінка винесена в окремі providers.
Open/Closed Principle
Open/Closed Principle: Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
Простіше: систему має бути зручно розширювати новою поведінкою, не переписуючи щоразу стабільний код, який уже виконує свою роботу.
Один contract для різних типів пошуку
interface SearchProviderInterface{ public function type(): SearchTypeEnum; public function count(string $query): int; public function search( string $query, int $perPage = 12, ): LengthAwarePaginator;}SearchProviderInterface визначає тільки те, що потрібно загальному механізму: тип результату, кількість збігів і paginated search.
PostSearchProvider, CategorySearchProvider, NewsSearchProvider і TipSearchProvider реалізують цей contract, але кожен сам відповідає за свій query.
SearchPage залишається стабільним
public function __construct( private readonly string $query, private readonly SearchProviderInterface $activeProvider, private readonly array $providers,) {} public function results(): ?LengthAwarePaginator{ if ($this->query === '') { return null; } if (! $this->resultsResolved) { $this->results = $this->activeProvider->search( $this->query,SearchPage не перевіряє, чи отримав він Post, News або Tip provider. Він викликає search(), count() і type() через спільний interface.
Тому різниця між конкретними запитами не розростається в центральному класі. Стабільний flow залишається однаковим, а нова поведінка з’являється окремо.
Розширення все одно потрібно зареєструвати
$this->app->tag([ PostSearchProvider::class, CategorySearchProvider::class, NewsSearchProvider::class, TipSearchProvider::class,], self::PUBLIC_SEARCH_PROVIDERS_TAG); $this->app->bind( SearchService::class, function ($app) { return new SearchService( $app->tagged( self::PUBLIC_SEARCH_PROVIDERS_TAG, ), ); },Open/Closed Principle не означає, що старі файли більше ніколи не можна змінювати.
У ICanUp provider потрібно зареєструвати в composition root, а новий тип пошуку може вимагати оновлення SearchTypeEnum. Це нормальні явні точки розширення.
Головне, що для нового provider не потрібно переносити його query rules у SearchPage і додавати там новий if або switch.
Другий приклад - кешування Analytics через Decorator
final readonly class CachedAnalyticsAggregationRepository implements AnalyticsAggregationRepositoryInterface{ public function __construct( private AnalyticsAggregationRepositoryInterface $source, private AnalyticsAggregationCache $cache, ) {} public function aggregate( AnalyticsAggregationQuery $query, ): iterable { return $this->cache->remember( $query, fn (): iterable => $this->source->aggregate($query), );MySqlAnalyticsAggregationRepository уже вміє читати aggregates з бази. Коли знадобилося кешування, цю реалізацію не перетворили на repository плюс cache manager.
CachedAnalyticsAggregationRepository реалізує той самий contract, обгортає source repository і додає кешування через композицію.
Це ще один практичний варіант OCP: нова поведінка додається навколо стабільної реалізації, а не всередину неї.
Що Open/Closed Principle не означає
- Не потрібно створювати interface для кожного класу наперед.
- Не потрібно передбачати всі майбутні extension points.
- Існуючий код не стає забороненим для змін.
- Сам факт використання Provider, Strategy або Decorator ще не доводить OCP.
Для мене практичне питання просте: коли з’являється новий варіант поведінки, чи можу я додати його поруч із наявними реалізаціями, не перетворюючи стабільний код на дедалі більший switch?
Практичний висновок
У цьому кейсі Open/Closed Principle добре видно на межі між стабільним механізмом і змінною поведінкою.
У Search стабільним залишається SearchPage, а конкретні запити розширюються providers. В Analytics кешування додається Decorator-ом навколо repository.
Мій простий висновок: новий варіант поведінки краще додавати як нове розширення, а не як ще одну умову всередині коду, який уже працює.



