Программа курса← Объектно ориентированное программирование
1История ООП
2Объект и Класс
3Базовые концепции ООП
4Принцип проектирования GRASP
5Принцип проектирования SOLID
6Паттерны GOF
7Методические материалы

ISP – принцип разделения интерфейсов

Клиенты не должны зависеть от интерфейсов, которые они не используют.

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

Как выглядит нарушение

Один интерфейс на все офисные устройства – и простой принтер обязан уметь сканировать и отправлять факсы:

// Толстый интерфейс: реализация обязана уметь то, чего не умеет
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: оба принципа охраняют честность контракта.