12.1.4 — четвёртая буква SOLID12.1.4 — the fourth letter of SOLID

Interface Segregation Principle (ISP)

Клиенты не должны зависеть от методов, которые они не используют. Много узких, специализированных интерфейсов лучше одного «толстого» — зависимость от лишних методов создаёт связанность там, где её не должно быть.Clients should not be forced to depend on methods they don't use. Many small, specialized interfaces beat one "fat" interface — depending on methods you don't need creates coupling where none should exist.

частый вопрос на собеседованииcommon interview question SOLID · 4/5

Что этоWhat it is

Сформулирован Робертом Мартином на основе опыта работы с Xerox: класс с «толстым» интерфейсом заставляет каждый его клиент неявно зависеть от всех методов этого интерфейса, даже тех, которые клиент никогда не вызывает. Любое изменение метода, которым клиент не пользуется, всё равно требует перекомпиляции и, в динамических языках, повторного тестирования клиента — потому что формально клиент зависит от всего интерфейса целиком.Formulated by Robert Martin based on his experience at Xerox: a class with a "fat" interface forces every one of its clients to implicitly depend on every method of that interface, even ones the client never calls. Any change to a method the client doesn't use still forces a recompile, and in dynamic languages, re-testing the client — because formally, the client depends on the entire interface.

До и послеBefore and after

толстый интерфейс — принтер, который умеет всёa fat interface — a printer that does everything
struct IMultiFunctionDevice {
    virtual void print(Doc&) = 0;
    virtual void scan(Doc&) = 0;
    virtual void fax(Doc&) = 0;
};

// SimplePrinter обязан реализовать scan()/fax(), которые не умеет// SimplePrinter is forced to implement scan()/fax(), which it can't do
struct SimplePrinter : IMultiFunctionDevice {
    void print(Doc& d) override { /* ok */ }
    void scan(Doc&) override { throw std::logic_error("not supported"); }
    void fax(Doc&) override { throw std::logic_error("not supported"); }
};
узкие интерфейсы — каждый клиент зависит только от нужногоnarrow interfaces — each client depends only on what it needs
struct IPrinter { virtual void print(Doc&) = 0; };
struct IScanner { virtual void scan(Doc&) = 0; };
struct IFax     { virtual void fax(Doc&) = 0; };

struct SimplePrinter : IPrinter {
    void print(Doc& d) override { /* ok */ }
};
// MultiFunctionDevice может реализовать все три — но никто не обязан// MultiFunctionDevice can implement all three — but nothing forces it to
struct MultiFunctionDevice : IPrinter, IScanner, IFax { /* ... */ };

ISP на практикеISP in practice

01

Rust traits — узкая композиция по умолчаниюRust traits — narrow composition by default

Идиоматичный Rust почти всегда предпочитает несколько маленьких трейтов (Read, Write, Seek из std::io) одному большому — функция принимает impl Read, если ей нужно только читать, и не тянет за собой обязательства писать или искать.Idiomatic Rust almost always prefers several small traits (Read, Write, Seek from std::io) over one large one — a function takes impl Read if it only needs to read, and doesn't drag in an obligation to write or seek.

fn count_lines(r: impl std::io::Read) -> usize { /* ... */ 0 }
// принимает File, TcpStream, Cursor<Vec<u8>> — что угодно с Read,
// не требуя от них Write или Seek// accepts a File, TcpStream, Cursor<Vec<u8>> — anything with Read,
// without requiring Write or Seek from them
02

Role Interfaces (Мартин Фаулер)Role interfaces (Martin Fowler)

Интерфейс проектируется не «от класса» (все методы, которые есть у реализации), а «от роли клиента» (что конкретно этому клиенту нужно вызывать) — один и тот же класс может реализовывать несколько независимых ролевых интерфейсов одновременно.The interface is designed not "from the class" (every method the implementation happens to have) but "from the client's role" (specifically what that client needs to call) — the same class can implement several independent role interfaces at once.

ГраблиGotchas

Реализация пустой заглушкой — не решение, а маскировка нарушенияImplementing an empty stub isn't a fix — it's a violation in disguise

void scan() override {} вместо исключения формально «компилируется», но не решает проблему: клиент, приводящий объект к IScanner, получит объект, который тихо ничего не делает — ISP не про то, чтобы код скомпилировался, а про то, чтобы зависимость была осмысленной.void scan() override {} instead of an exception formally "compiles," but it doesn't solve the problem: a client that casts the object to IScanner gets an object that silently does nothing — ISP isn't about making the code compile, it's about the dependency being meaningful.

Чрезмерная сегрегация плодит interface soupOver-segregation creates interface soup

Дробление интерфейса на один метод каждый ради формального ISP превращает простую композицию в необходимость реализовывать и приводить десятки крошечных интерфейсов — баланс важен так же, как и при SRP: границы должны отражать реальные, различающиеся роли клиентов, а не быть максимально мелкими.Splitting an interface down to one method each purely for formal ISP compliance turns simple composition into a need to implement and cast dozens of tiny interfaces — the balance matters just as it does with SRP: boundaries should reflect real, distinct client roles, not be maximally fine-grained for its own sake.

ISP актуален и без явных интерфейсов — в Python/duck typing тожеISP matters even without explicit interfaces — in Python/duck-typed code too

В языках без явных interface (Python, JS) нарушение ISP проявляется как функция, принимающая «толстый» объект и использующая от него один метод — зависимость от остального интерфейса неявная, но всё равно реальная: рефакторинг непроверенного поля где-то ещё в объекте может внезапно сломать эту функцию.In languages with no explicit interface construct (Python, JS), an ISP violation shows up as a function that accepts a "fat" object and uses just one method from it — the dependency on the rest of the interface is implicit, but still real: refactoring an unrelated field elsewhere in the object can unexpectedly break that function.