0% прочитано

SOLID. Принцип 2 - Open/Closed Principle на практиці в реальному Laravel-проєкті

На реальному пошуку ICanUp показую Open/Closed Principle: нові типи пошуку додаються окремими providers, а спільний SearchPage продовжує працювати через один contract без type-specific умов.

22 вересня 2026 р. 8 хв читанняЛаравель

Публічний пошук у 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 для різних типів пошуку

PHP - SearchProviderInterface
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 залишається стабільним

PHP - SearchPage excerpt
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 залишається однаковим, а нова поведінка з’являється окремо.

Розширення все одно потрібно зареєструвати

PHP - SearchServiceProvider registration
$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

PHP - CachedAnalyticsAggregationRepository
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.

Мій простий висновок: новий варіант поведінки краще додавати як нове розширення, а не як ще одну умову всередині коду, який уже працює.