handbookНаписать

principles/solid/interface-segregation/README.md · тема · 4 языка · 19 тестов

Принцип разделения интерфейсов (Interface Segregation Principle) ​

Клиент не должен зависеть от методов, которыми не пользуется: печать и сканирование - отдельные узкие интерфейсы, а не одно "устройство".

Проблема ​

В офисе есть МФУ, и под него написали один интерфейс на всё:

php
interface Device
{
    public function print(Document $document): string;
    public function scan(): Document;
    public function fax(string $number, Document $document): void;
}

final class OfficePrinter implements Device
{
    public function print(Document $document): string { /* ... */ }

    public function scan(): Document
    {
        throw new LogicException('this printer cannot scan');
    }

    public function fax(string $number, Document $document): void
    {
        throw new LogicException('this printer cannot fax');
    }
}

Обычный принтер вынужден реализовать сканирование и факс заглушками с исключением. Код, которому нужна только печать, всё равно зависит от Device целиком: тестовый двойник должен реализовать три метода вместо одного, а добавление в Device метода staple() заставит править каждый класс, даже те, что к сшиванию отношения не имеют. Заодно нарушается и подстановка: OfficePrinter формально Device, но падает на половине методов.

Решение ​

Разбить толстый интерфейс по ролям, которые нужны клиентам: Printer с одним методом print и Scanner с одним методом scan. Устройство реализует столько ролей, сколько умеет: OfficePrinter - только Printer, MultiFunctionDevice - обе. Клиент просит ровно те роли, которыми пользуется: Copier получает отдельно Scanner и Printer, и ему всё равно, одно это устройство или два.

Интерфейс проектируется от клиента, а не от устройства: вопрос не "что умеет МФУ", а "что нужно копиру".

Структура ​

«interface»

Printer

+print(document) : string

«interface»

Scanner

+scan() : Document

OfficePrinter

MultiFunctionDevice

+place(paper)

Copier

+copy() : string

Document

+title: string

+pages: int

place - метод самого МФУ, а не роли: положить бумагу на стекло - дело человека у аппарата, копир этого не делает и о методе не знает. Пустое стекло - отдельное исключение GlassEmpty, а не пустой документ.

Как это собирается ​

Устройства берутся из инвентаря офиса и собираются в DI-контейнере или main. В примере это OfficePrinter на этаже и MultiFunctionDevice на ресепшене. Счёт печатается на офисном принтере напрямую. Посетитель кладёт паспорт на стекло МФУ, и два копира собираются из ролей: один берёт сканер и принтер у самого МФУ, другой сканирует на МФУ, а печатает на этаже. Copier в обоих случаях один и тот же класс, меняется только то, что ему передали.

Сценарий целиком с выводом: пример на PHP, он же на Go, Python и TypeScript в панели кода.

Реализации ​

ЯзыкОсобенность
PHPДва интерфейса по одному методу, MultiFunctionDevice implements Printer, Scanner; двойник в тесте - анонимный класс с одним методом
GoМаленькие интерфейсы и их композиция PrintScanner, как io.ReadWriter; Scan возвращает error
PythonProtocol с @runtime_checkable: устройства ни от чего не наследуются, роли проверяются по форме
TypeScriptСтруктурные интерфейсы; двойник сканера в тесте - литерал { scan: () => ... }

Запуск: make test T=principles/solid/interface-segregation

Плюсы и минусы ​

Тесты показывают выгоду для клиента: двойник сканера в тесте - один метод, без заглушек печати и факса, и Copier с ним работает. Тот же Copier работает с одним МФУ в обеих ролях. Ещё один тест фиксирует, что OfficePrinter реализует только Printer и метода scan у него нет вовсе.

  • ✅ Классы реализуют только то, что умеют, без заглушек с исключениями.
  • ✅ Тестовые двойники крошечные: один метод вместо всего устройства.
  • ✅ Изменение одной роли не задевает классы, которые её не реализуют.
  • ❌ Больше интерфейсов, и их нужно правильно назвать.
  • ❌ Если раздробить слишком мелко, клиенту придётся принимать пять параметров вместо одного.

Когда не применять ​

  • У интерфейса один клиент и одна реализация - делить его не на что.
  • Методы всегда используются вместе (open и close у соединения) - разрезать их значит разрезать одну ответственность.
  • Интерфейс задан внешним протоколом или библиотекой: его не переделать, можно только обернуть узким адаптером.

Отличия между языками ​

  • PHP - класс может реализовать несколько интерфейсов, и объявлять их нужно явно. Узкие интерфейсы делают анонимные классы в тестах короткими.
  • Go - принцип встроен в культуру языка: интерфейсы маленькие (io.Reader, io.Writer), объявляются у потребителя, а широкие собираются композицией (PrintScanner). Реализация не знает об интерфейсах, поэтому разделить роль можно позже, не трогая устройство.
  • Python - Protocol описывает роль у потребителя, как в Go. isinstance с @runtime_checkable проверяет только наличие методов, не сигнатуры.
  • TypeScript - структурная типизация: двойник - объектный литерал с одним методом, а лишние методы у реального устройства клиенту не видны.

Связанные темы ​

  • SOLID - все пять принципов
  • Принцип подстановки Лисков - заглушка с исключением ломает и подстановку
  • Адаптер - как дать клиенту узкий интерфейс поверх чужого широкого
  • Фасад - обратная задача: собрать много ролей за одной простой точкой входа
© 2014-2026 Тимур ТурдыевСайт и справочник работают на VPS от AdminVPS