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

Описание
Проблема ООП может быть, когда при наследовании класс-потомок получает вместе с нужной функциональностью кучу неиспользуемой и ненужной.

Однако, проблема возникает не столько из-за ООП как такового, сколько из-за неправильной модели системы.

Принцип разделения интерфейса содержит правила и ограничения, которые помогают с этой проблемой справляться.

Принцип гласит: Сущности не должны зависеть от интерфейсов, которые они не используют.

Чаще всего это заставляет классы дробить (разделять) на несколько более узких классов.

ISP предлагает два подхода к решению этой проблемы с помощью разделения интерфейсов: через делегирование и через множественное наследование.
Пример перегруженного класса Printer

from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class Printer(ABC):

    @abstractmethod
    def print(self, document):
        pass

    @abstractmethod
    def scan(self, document):
        pass


@dataclass
class MultiPrinter(Printer):

    def print(self, document):
        '''Реализация печати'''
        pass

    def scan(self, document):
        '''Реализация сканирования'''
        pass


@dataclass
class OldPrinter(Printer):

    def print(self, document):
        '''Реализация печати'''
        pass

    def scan(self, document):
        '''Отсутствия функции сканирования'''
        raise NotImplementedError('Принтер не умеет сканировать,'
                                  'но функцию необходимо описать')

Разделение через делегирование
Этот подход подразумевает использование шаблона проектирования под названием Адаптер.

Мы не будем связывать печать и сканирование непосредственно друг с другом. Мы будем связывать их через дополнительный слой (адаптер), который будет транслировать сообщения от одной сущности другой.

Данный пример рассматривается в разделе паттерны
Разделение через множественное наследование
Второй вариант предполагает, что класс будет наследоваться одновременно от классов Print и Scan.

Это позволит отвязать родительские методы друг от друга и использовать в классе Printer только нужную функциональность.
Пример разделения методов по классам

from abc import ABC, abstractmethod
from dataclasses import dataclass


@dataclass
class Print(ABC):

    @abstractmethod
    def print(self, document):
        pass

class Scan(ABC):

    @abstractmethod
    def scan(self, document):
        pass


@dataclass
class MultiPrinter(Print, Scan):

    def print(self, document):
        '''Реализация печати'''
        pass

    def scan(self, document):
        '''Реализация сканирования'''
        pass


@dataclass
class OldPrinter(Print):

    def print(self, document):
        '''Реализация печати'''
        pass

Вывод
Принцип разделения интерфейса:

  • помогает бороться с наследованием или реализацией ненужной функциональности;
  • даёт возможность спроектировать модули так, чтобы их затрагивали изменения только тех интерфейсов, которые они действительно реализуют;
  • снижает зацепление модулей;
  • уничтожает наследование ради наследования, поощряет использование композиции;
  • позволяет выявлять более высокие абстракции и находить неочевидные связи между сущностями.