Клиенты не должны зависеть от интерфейсов, которые они не используют.
Лучше несколько маленьких интерфейсов, чем один огромный, заставляющий реализовывать лишние методы.
Как выглядит нарушение
Один интерфейс на все офисные устройства – и простой принтер обязан уметь сканировать и отправлять факсы:
// Толстый интерфейс: реализация обязана уметь то, чего не умеет
public interface IDevice
{
void Print(string document);
void Scan(string path);
void Fax(string number);
}
public class SimplePrinter : IDevice
{
public void Print(string document) { }
public void Scan(string path) => throw new NotSupportedException();
public void Fax(string number) => throw new NotSupportedException();
}
// Отчётам нужна одна печать, а зависят они от всего устройства целиком
public class ReportSender
{
public ReportSender(IDevice device) { }
}
// Толстый интерфейс: реализация обязана уметь то, чего не умеет
class IDevice {
public:
virtual void Print(const std::string& document) = 0;
virtual void Scan(const std::string& path) = 0;
virtual void Fax(const std::string& number) = 0;
virtual ~IDevice() = default;
};
class SimplePrinter : public IDevice {
public:
void Print(const std::string& document) override {}
void Scan(const std::string&) override { throw std::logic_error("не поддерживается"); }
void Fax(const std::string&) override { throw std::logic_error("не поддерживается"); }
};
// Отчётам нужна одна печать, а зависят они от всего устройства целиком
class ReportSender {
public:
explicit ReportSender(IDevice& device) : device_(device) {}
private:
IDevice& device_;
};
// Толстый интерфейс: реализация обязана уметь то, чего не умеет
type Device interface {
Print(document string) error
Scan(path string) error
Fax(number string) error
}
type SimplePrinter struct{}
func (SimplePrinter) Print(document string) error { return nil }
func (SimplePrinter) Scan(path string) error { return errors.New("не поддерживается") }
func (SimplePrinter) Fax(number string) error { return errors.New("не поддерживается") }
// Отчётам нужна одна печать, а зависят они от всего устройства целиком
type ReportSender struct {
device Device
}
# Толстый интерфейс: реализация обязана уметь то, чего не умеет
class Device(Protocol):
def print(self, document: str) -> None: ...
def scan(self, path: str) -> None: ...
def fax(self, number: str) -> None: ...
class SimplePrinter:
def print(self, document: str) -> None:
pass
def scan(self, path: str) -> None:
raise NotImplementedError("не поддерживается")
def fax(self, number: str) -> None:
raise NotImplementedError("не поддерживается")
# Отчётам нужна одна печать, а зависят они от всего устройства целиком
class ReportSender:
def __init__(self, device: Device) -> None:
self._device = device
// Толстый интерфейс: реализация обязана уметь то, чего не умеет
interface Device {
print(document: string): void;
scan(path: string): void;
fax(number: string): void;
}
class SimplePrinter implements Device {
print(document: string): void {}
scan(): void {
throw new Error('не поддерживается');
}
fax(): void {
throw new Error('не поддерживается');
}
}
// Отчётам нужна одна печать, а зависят они от всего устройства целиком
class ReportSender {
constructor(private readonly device: Device) {}
}
// Толстый интерфейс: реализация обязана уметь то, чего не умеет
public interface Device {
void print(String document);
void scan(String path);
void fax(String number);
}
public class SimplePrinter implements Device {
public void print(String document) { }
public void scan(String path) {
throw new UnsupportedOperationException();
}
public void fax(String number) {
throw new UnsupportedOperationException();
}
}
// Отчётам нужна одна печать, а зависят они от всего устройства целиком
public class ReportSender {
private final Device device;
public ReportSender(Device device) {
this.device = device;
}
}
// Толстый интерфейс: реализация обязана уметь то, чего не умеет
interface Device {
fun print(document: String)
fun scan(path: String)
fun fax(number: String)
}
class SimplePrinter : Device {
override fun print(document: String) = Unit
override fun scan(path: String): Nothing = throw UnsupportedOperationException()
override fun fax(number: String): Nothing = throw UnsupportedOperationException()
}
// Отчётам нужна одна печать, а зависят они от всего устройства целиком
class ReportSender(private val device: Device)
Компилируется, а работает наполовину:
- контракт врёт – тип обещает три операции, реализация выполняет одну, и узнать об этом можно только в рантайме;
- клиент зависит от лишнего –
ReportSenderпечатает отчёты, но пересобирается каждый раз, когда в интерфейс добавляют метод для сканера; - заглушки в тестах разрастаются – чтобы протестировать печать, приходится реализовать и факс, и сканирование;
- добавить метод страшно – новый
ScanDuplex()придётся дописать всем реализациям, включая те, которым он не нужен.
Обратите внимание: throw NotSupportedException – это ровно тот же симптом, что и у
нарушения LSP. Оба принципа охраняют честность контракта, просто
с разных сторон: ISP – со стороны интерфейса, LSP – со стороны реализации.
Как разделить
Режем по клиентам: сколько разных наборов операций просят вызывающие – столько и интерфейсов.
// Каждый интерфейс – про одну возможность
public interface IPrinter { void Print(string document); }
public interface IScanner { void Scan(string path); }
public interface IFax { void Fax(string number); }
// Простое устройство реализует только своё
public class SimplePrinter : IPrinter
{
public void Print(string document) { }
}
// МФУ – всё сразу, и это его добровольный выбор
public class Mfp : IPrinter, IScanner, IFax
{
public void Print(string document) { }
public void Scan(string path) { }
public void Fax(string number) { }
}
// Клиент просит ровно то, чем пользуется
public class ReportSender
{
public ReportSender(IPrinter printer) { }
}
// Каждый интерфейс – про одну возможность
class IPrinter {
public:
virtual void Print(const std::string& document) = 0;
virtual ~IPrinter() = default;
};
class IScanner {
public:
virtual void Scan(const std::string& path) = 0;
virtual ~IScanner() = default;
};
class IFax {
public:
virtual void Fax(const std::string& number) = 0;
virtual ~IFax() = default;
};
// Простое устройство реализует только своё
class SimplePrinter : public IPrinter {
public:
void Print(const std::string& document) override {}
};
// МФУ – всё сразу, и это его добровольный выбор
class Mfp : public IPrinter, public IScanner, public IFax {
public:
void Print(const std::string& document) override {}
void Scan(const std::string& path) override {}
void Fax(const std::string& number) override {}
};
// Клиент просит ровно то, чем пользуется
class ReportSender {
public:
explicit ReportSender(IPrinter& printer) : printer_(printer) {}
private:
IPrinter& printer_;
};
// Каждый интерфейс – про одну возможность
type Printer interface {
Print(document string) error
}
type Scanner interface {
Scan(path string) error
}
type Faxer interface {
Fax(number string) error
}
// Простое устройство реализует только своё
type SimplePrinter struct{}
func (SimplePrinter) Print(document string) error { return nil }
// МФУ – всё сразу, и это его добровольный выбор
type Mfp struct{}
func (Mfp) Print(document string) error { return nil }
func (Mfp) Scan(path string) error { return nil }
func (Mfp) Fax(number string) error { return nil }
// Клиент просит ровно то, чем пользуется
type ReportSender struct {
printer Printer
}
# Каждый интерфейс – про одну возможность
class Printer(Protocol):
def print(self, document: str) -> None: ...
class Scanner(Protocol):
def scan(self, path: str) -> None: ...
class Faxer(Protocol):
def fax(self, number: str) -> None: ...
# Простое устройство реализует только своё
class SimplePrinter:
def print(self, document: str) -> None:
pass
# МФУ – всё сразу, и это его добровольный выбор
class Mfp:
def print(self, document: str) -> None: ...
def scan(self, path: str) -> None: ...
def fax(self, number: str) -> None: ...
# Клиент просит ровно то, чем пользуется
class ReportSender:
def __init__(self, printer: Printer) -> None:
self._printer = printer
// Каждый интерфейс – про одну возможность
interface Printer {
print(document: string): void;
}
interface Scanner {
scan(path: string): void;
}
interface Faxer {
fax(number: string): void;
}
// Простое устройство реализует только своё
class SimplePrinter implements Printer {
print(document: string): void {}
}
// МФУ – всё сразу, и это его добровольный выбор
class Mfp implements Printer, Scanner, Faxer {
print(document: string): void {}
scan(path: string): void {}
fax(number: string): void {}
}
// Клиент просит ровно то, чем пользуется
class ReportSender {
constructor(private readonly printer: Printer) {}
}
// Каждый интерфейс – про одну возможность
public interface Printer {
void print(String document);
}
public interface Scanner {
void scan(String path);
}
public interface Faxer {
void fax(String number);
}
// Простое устройство реализует только своё
public class SimplePrinter implements Printer {
public void print(String document) { }
}
// МФУ – всё сразу, и это его добровольный выбор
public class Mfp implements Printer, Scanner, Faxer {
public void print(String document) { }
public void scan(String path) { }
public void fax(String number) { }
}
// Клиент просит ровно то, чем пользуется
public class ReportSender {
private final Printer printer;
public ReportSender(Printer printer) {
this.printer = printer;
}
}
// Каждый интерфейс – про одну возможность
fun interface Printer {
fun print(document: String)
}
fun interface Scanner {
fun scan(path: String)
}
fun interface Faxer {
fun fax(number: String)
}
// Простое устройство реализует только своё
class SimplePrinter : Printer {
override fun print(document: String) = Unit
}
// МФУ – всё сразу, и это его добровольный выбор
class Mfp : Printer, Scanner, Faxer {
override fun print(document: String) = Unit
override fun scan(path: String) = Unit
override fun fax(number: String) = Unit
}
// Клиент просит ровно то, чем пользуется
class ReportSender(private val printer: Printer)
Ни одного «не поддерживается»: МФУ по-прежнему умеет всё, но теперь это его решение, а не
обязанность простого принтера. А по сигнатуре ReportSender сразу видно, что он только
печатает.
Что даёт соблюдение
- Реализация не тащит лишние зависимости. Простому принтеру не нужны библиотеки для факса только потому, что метод оказался в общем интерфейсе.
- Моки в тестах становятся маленькими. Подменить интерфейс из одного метода – одна строка, из десяти – отдельный класс-заглушка.
- Изменение интерфейса задевает только тех, кто им пользуется. Добавили метод сканеру – принтеры даже не перекомпилируются.
- Интерфейс становится документацией. По списку из одного-двух методов сразу видно, что требуется от реализации.
Признак нарушения
Если реализация интерфейса вынуждена выбрасывать «не поддерживается» или оставлять метод пустым – интерфейс слишком широкий. Заметьте, что это тот же симптом, что и у нарушения LSP: оба принципа охраняют честность контракта.