У ICanUp різні ContentBlocks мають різні можливості. Paragraph потребує базових даних і правил валідації. Form або Hero мають додаткові параметри відображення. Tips Explorer ще й дозволений тільки для певного типу сторінки.
Якби всі ці можливості зібрати в один великий interface, кожен block був би змушений реалізувати методи, які йому не потрібні.
Interface Segregation Principle
Interface Segregation Principle: Clients should not be forced to depend on methods they do not use.
Простіше: код не повинен залежати від методів, якими він не користується. Спільні можливості краще залишити в базовому interface, а додаткові винести в окремі interfaces.
Приклад 1 - базовий interface і окрема можливість
interface ContentBlockDefinitionInterface{ public function type(): ContentBlockTypeEnum; public function label(): string; public function defaultData(): array; public function rules(): array; public function attributes(): array;} interface ScopedContentBlockDefinitionInterface{ public function supportsBlockable(ContentBlockDefinitionInterface містить тільки те, що потрібне кожному визначенню блока: type, label, default data, rules та attributes.
TipsExplorerBlockDefinition має додаткову особливість - він працює тільки для page. Тому supportsBlockable() знаходиться не в базовому interface, а в окремому ScopedContentBlockDefinitionInterface.
ContentBlockRegistry перевіряє цю додаткову можливість лише тоді, коли вона є. Paragraph, Image або Quote взагалі не повинні знати про це обмеження.
Приклад 2 - додаткові методи відображення потрібні не кожному block
interface PresentationBlockDefinitionInterface extends ContentBlockDefinitionInterface{ public function contentRules(): array; public function configurationRules(): array; public function variants(): array; public function options(): array; public function makeData( array $data, ): BlockDataInterface;} Blocks для складнішого відображення мають додаткові методи: variants(), options(), окремі content/configuration rules та makeData().
Але звичайному ParagraphBlockDefinition вони не потрібні. Він реалізує тільки базовий interface і не повертає порожні variants або fake configuration тільки тому, що цього потребують інші blocks.
У цьому і є практичний сенс ISP: проста реалізація може залишатися простою.
Приклад 3 - Analytics розділяє запис і читання
interface AnalyticsHistoryRepositoryInterface{ public function record( AnalyticsMeasurement $measurement, ): void;} interface AnalyticsAggregationRepositoryInterface{ public function aggregate( AnalyticsAggregationQuery $query, ): iterable;} final readonly class AnalyticsTracker{В Analytics теж немає одного великого repository interface для всіх операцій.
AnalyticsTracker записує вимірювання, тому залежить від AnalyticsHistoryRepositoryInterface з методом record(). Йому не потрібен aggregate().
ContentAnalyticsQuery читає агреговані дані, тому залежить від AnalyticsAggregationRepositoryInterface. Йому не потрібен record().
Кожна частина коду бачить тільки ту частину API, яку реально використовує.
ISP - не просто про маленькі interfaces
Interface з одним методом не автоматично хороший, а interface з п’ятьма методами не автоматично поганий.
Головне питання: чи весь код, який залежить від цього interface, справді потребує цих методів?
Якщо клас постійно повертає порожні значення, кидає NotSupportedException або реалізує метод тільки тому, що interface цього вимагає, межу interface варто переглянути.
Практичний висновок
Моє найпростіше формулювання Interface Segregation Principle: код не повинен залежати від методів і можливостей, які йому не потрібні.
У ContentBlocks базовий interface містить тільки спільне, а додаткові можливості винесені окремо. В Analytics запис і читання мають окремі interfaces.
У результаті interfaces описують реальні потреби коду, а не намагаються одразу охопити всі можливості системи.



