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

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

This lesson is not translated yet – the Russian original is below.

TODO: отвалидировать – текст собран автоматически из материалов курса на Google Drive, проверьте формулировки и код.

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

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

// Толстый интерфейс: принтер обязан уметь то, чего не умеет
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 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) { }
}
// Толстый интерфейс: принтер обязан уметь то, чего не умеет
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& path) override {
        throw std::logic_error("не поддерживается");
    }

    void Fax(const std::string& number) override {
        throw std::logic_error("не поддерживается");
    }
};

// Разделённые интерфейсы: каждый реализует только то, что умеет
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 SimplePrinter : public IPrinter {
public:
    void Print(const std::string& document) override {}
};
// Толстый интерфейс: принтер обязан уметь то, чего не умеет
type Device interface {
	Print(document string)
	Scan(path string)
	Fax(number string)
}

// Разделённые интерфейсы. В Go это естественный стиль:
// интерфейсы объявляет тот, кто их потребляет, и они получаются мелкими.
type Printer interface{ Print(document string) }
type Scanner interface{ Scan(path string) }
type Faxer interface{ Fax(number string) }

type SimplePrinter struct{}

func (SimplePrinter) Print(document string) {}
from typing import Protocol

# Толстый протокол: принтер обязан уметь то, чего не умеет
class Device(Protocol):
    def print(self, document: str) -> None: ...
    def scan(self, path: str) -> None: ...
    def fax(self, number: str) -> None: ...

# Разделённые протоколы: каждый реализует только то, что умеет
class Printer(Protocol):
    def print(self, document: str) -> None: ...

class Scanner(Protocol):
    def scan(self, path: str) -> None: ...

class SimplePrinter:
    def print(self, document: str) -> None:
        pass
// Толстый интерфейс: принтер обязан уметь то, чего не умеет
interface Device {
    print(document: string): void;
    scan(path: string): void;
    fax(number: string): void;
}

// Разделённые интерфейсы: каждый реализует только то, что умеет
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 {}
}
// Толстый интерфейс: принтер обязан уметь то, чего не умеет
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 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) { }
}
// Толстый интерфейс: принтер обязан уметь то, чего не умеет
interface Device {
    fun print(document: String)
    fun scan(path: String)
    fun fax(number: String)
}

// Разделённые интерфейсы: каждый реализует только то, что умеет
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
}

Что даёт соблюдение

  • Реализация не тащит лишние зависимости. Простому принтеру не нужны библиотеки для факса только потому, что метод оказался в общем интерфейсе.
  • Моки в тестах становятся маленькими. Подменить интерфейс из одного метода – одна строка, из десяти – отдельный класс-заглушка.
  • Изменение интерфейса задевает только тех, кто им пользуется. Добавили метод сканеру – принтеры даже не перекомпилируются.
  • Интерфейс становится документацией. По списку из одного-двух методов сразу видно, что требуется от реализации.

Признак нарушения

Если реализация интерфейса вынуждена выбрасывать «не поддерживается» или оставлять метод пустым – интерфейс слишком широкий. Заметьте, что это тот же симптом, что и у нарушения LSP: оба принципа охраняют честность контракта.