Что такое инверсия зависимостей (DIP) и разделение интерфейсов (ISP) в SOLID?
Остались последние два принципа SOLID — DIP (Dependency Inversion) и ISP (Interface Segregation). Часто путаю инверсию зависимостей с внедрением зависимостей (DI). В чём разница? И что такое «толстый интерфейс», который запрещает ISP? Хотелось бы примеры на C#.
2 ответа
Разберём оба принципа и заодно развеем путаницу DIP vs DI.
Dependency Inversion Principle (DIP)
Два правила:
- Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба зависят от абстракций.
- Абстракции не зависят от деталей; детали зависят от абстракций.
Нарушение — высокоуровневый сервис жёстко завязан на конкретную реализацию:
// Плохо: OrderService прибит гвоздями к MySqlDatabase
public class MySqlDatabase
{
public void Save(string data) { /* ... */ }
}
public class OrderService
{
private readonly MySqlDatabase _db = new MySqlDatabase(); // жёсткая зависимость
public void Process(string order) => _db.Save(order);
}
Инвертируем зависимость через интерфейс:
public interface IOrderStorage
{
void Save(string data);
}
public class MySqlStorage : IOrderStorage
{
public void Save(string data) { /* ... */ }
}
public class OrderService
{
private readonly IOrderStorage _storage;
// зависим от абстракции, реализацию получаем снаружи
public OrderService(IOrderStorage storage) => _storage = storage;
public void Process(string order) => _storage.Save(order);
}
DIP vs DI: DIP — это принцип (зависим от абстракций). Dependency Injection (DI) — это приём/механизм, как передать конкретную реализацию (через конструктор, свойство, метод или DI-контейнер). DI — один из способов реализовать DIP, но не равен ему.
Interface Segregation Principle (ISP)
«Клиенты не должны зависеть от методов, которые они не используют». Толстый интерфейс заставляет реализовывать лишнее:
// Плохо: толстый интерфейс
public interface IMachine
{
void Print(string doc);
void Scan(string doc);
void Fax(string doc);
}
// Простому принтеру навязали Scan и Fax
public class SimplePrinter : IMachine
{
public void Print(string doc) { /* ок */ }
public void Scan(string doc) => throw new NotSupportedException();
public void Fax(string doc) => throw new NotSupportedException();
}
Разбиваем на узкие интерфейсы:
public interface IPrinter { void Print(string doc); }
public interface IScanner { void Scan(string doc); }
public interface IFax { void Fax(string doc); }
public class SimplePrinter : IPrinter
{
public void Print(string doc) { /* ок */ }
}
public class Mfu : IPrinter, IScanner, IFax
{
public void Print(string doc) { }
public void Scan(string doc) { }
public void Fax(string doc) { }
}
Обратите внимание: появление throw new NotSupportedException() в реализации — частый сигнал нарушения сразу и ISP, и LSP.
По поводу путаницы: запомните формулу «DIP — это цель, DI — это средство».
- DIP отвечает на вопрос «от чего зависеть?» — ответ: от абстракции.
- DI отвечает на вопрос «как получить конкретную зависимость?» — ответ: пусть её передадут снаружи (обычно в конструктор).
Можно соблюдать DIP даже без DI-контейнера — достаточно принимать интерфейс параметром конструктора и собирать граф объектов вручную в Main/composition root.
Про ISP добавлю: на практике хорошим ориентиром служит то, как интерфейс используется клиентом. Если разные потребители используют разные подмножества методов — это кандидат на разделение.