Что означает буква I в принципах SOLID?

Ответ

I в SOLID — это Interface Segregation Principle (ISP), Принцип разделения интерфейсов.

Формулировка: Клиенты не должны зависеть от методов, которые они не используют. Многочисленные специализированные интерфейсы лучше, чем один универсальный.

Проблема — "толстый" интерфейс:

// Нарушение ISP. Принтер вынужден реализовывать метод Scan, который ему не нужен.
public interface IMultiFunctionDevice
{
    void Print(Document d);
    void Scan(Document d);
    void Fax(Document d);
}

public class OldFashionedPrinter : IMultiFunctionDevice
{
    public void Print(Document d) { /* OK */ }
    public void Scan(Document d)  { throw new NotImplementedException(); } // Ненужный метод!
    public void Fax(Document d)   { throw new NotImplementedException(); } // Ненужный метод!
}

Решение — разделение интерфейсов:

// Создаем несколько узкоспециализированных интерфейсов.
public interface IPrinter { void Print(Document d); }
public interface IScanner { void Scan(Document d); }
public interface IFax { void Fax(Document d); }

// Класс реализует только то, что ему действительно нужно.
public class OldFashionedPrinter : IPrinter
{
    public void Print(Document d) { /* Реализация */ }
}

// Современное устройство может реализовать несколько интерфейсов.
public class Photocopier : IPrinter, IScanner
{
    public void Print(Document d) { /* ... */ }
    public void Scan(Document d)  { /* ... */ }
}

Преимущества ISP:

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

Ответ 18+ 🔞

Слушай, а вот этот принцип «I» — это вообще про то, чтобы не делать из интерфейса швейцарский нож на все случаи жизни. По-русски он звучит как «Принцип разделения интерфейсов», но суть проще: не заставляй класс реализовывать то, что ему нахуй не сдалось.

Вот смотри, представь, что ты проектируешь устройство. Берёшь и пишешь один жирный интерфейс на всё подряд:

public interface IMultiFunctionDevice
{
    void Print(Document d);
    void Scan(Document d);
    void Fax(Document d);
}

И тут ты пытаешься под него сделать старый дедовский принтер, который только печатает. И что получается? Он вынужден реализовывать Scan и Fax! А он нихуя не умеет сканировать и факсить! Приходится либо пустые методы оставлять, либо вообще throw new NotImplementedException() кидать. Это пиздец как неудобно и криво. Класс начинает зависеть от кучи чужих обязательств, которые ему вообще не нужны.

А правильный подход какой? Дробить этот жирный интерфейс на мелкие, узкоспециализированные куски!

public interface IPrinter { void Print(Document d); }
public interface IScanner { void Scan(Document d); }
public interface IFax { void Fax(Document d); }

Теперь твой старый принтер спокойно реализует только IPrinter и ни о чём не парится:

public class OldFashionedPrinter : IPrinter
{
    public void Print(Document d) { /* Реализация */ }
}

А если у тебя есть современная копировалка, которая и печатает, и сканирует — ну так реализуй оба интерфейса и не мучайся:

public class Photocopier : IPrinter, IScanner
{
    public void Print(Document d) { /* ... */ }
    public void Scan(Document d)  { /* ... */ }
}

В чём профит?

  • Связанность падает. Классы перестают тащить на себе груз ненужных методов.
  • Стабильность растёт. Захотел поменять что-то в логике факса — ты трогаешь только IFax и классы, которые его используют. Старые принтеры даже не узнают об этом, им похуй.
  • Код становится чище. Маленькие интерфейсы проще читать, тестировать и понимать. Сразу видно, за что класс отвечает, а за что — нет.

Короче, смысл в том, чтобы не создавать монстров. Лучше десять маленьких и понятных интерфейсов, чем один большой, от которого всем тошно.