Перейти к содержанию

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

Принцип разделения интерфейсов (Interface Segregation Principle) гласит что: «Ни один клиент не должен зависеть от методов, которые он не использует». Принцип разделения интерфейсов был предложен Робертом К. Мартином, когда он консультировал компанию Xerox. Принцип разделения интерфейсов предполагает создание небольших интерфейсов, известных как «ролевые интерфейсы», вместо большого интерфейса, состоящего из нескольких методов. Разделяя методы по ролям на более мелкие интерфейсы, клиенты будут зависеть только от методов, которые имеют к ним отношение. Допустим, мы разрабатываем приложение для различных коммуникационных устройств. Мы говорим, что устройство связи – это устройство, которое будет иметь одну или несколько из следующих функций: совершать звонки, отправлять SMS или искать в Интернете. Итак, мы создаем интерфейс с именем CommunicationDevice и добавляем соответствующие абстрактные методы для каждой из этих функций, чтобы любой создаваемый класс смог реализовать эти методы. Затем мы создаем класс SmartPhone с помощью интерфейса CommunicationDevice и реализуем функционал абстрактных методов. До сих пор все было в порядке. Теперь предположим, что нам нужно создать стационарный телефон. Он тоже является устройством связи, поэтому мы создаем новый класс LandlinePhone через тот же интерфейс CommunicationDevice. Именно здесь мы сталкиваемся с проблемой из-за объемного интерфейса CommunicationDevice. В классе LandlinePhone мы реализовываем метод make_calls(), но поскольку мы также наследуем абстрактные методы send_sms() и browse_internet(), мы должны предоставить реализацию и этих двух абстрактных методов в классе LandlinePhone, даже если они в принципе неприменимы к этому виду телефонов. Мы можем либо создать исключение, либо оставить pass вместо реализации, но нам все равно нужно ее предоставить.

from abc import abstractmethod

class CommunicationDevice():
    @abstractmethod
    def make_calls():
        pass

    @abstractmethod
    def send_sms():
        pass

    @abstractmethod
    def browse_internet():
        pass

class SmartPhone(CommunicationDevice):
    def make_calls():
        #implementation
        pass

    def send_sms():
        #implementation
        pass

    def browse_internet():
        #implementation
        pass

class LandlinePhone(CommunicationDevice):
    def make_calls():
        #implementation
        pass

    def send_sms():
        #just pass or raise exception as this feature is not supported
        pass

    def browse_internet():
        #just pass or raise exception as this feature is not supported
        pass
<?php
interface CommunicationDevice {
    public function make_calls();
    public function send_sms();
    public function browse_internet();
}

class SmartPhone implements CommunicationDevice {
    public function make_calls() {
        // TO DO
    }

    public function send_sms() {
        // TO DO
    }

    public function browse_internet() {
        // TO DO
    }
}

class LandlinePhone implements CommunicationDevice {
    public function make_calls() {
        // TO DO
    }

    public function send_sms() {
        // just pass or raise exception as this feature is not supported
    }

    public function browse_internet() {
        // just pass or raise exception as this feature is not supported
    }
}

Все можно исправить, следуя принципу разделения интерфейсов, как в примере ниже. Вместо создания большого интерфейса мы создаем более маленькие ролевые интерфейсы для каждого метода. Соответствующие классы будут использовать только связанные интерфейсы.

from abc import abstractmethod

class CallingDevice():
    @abstractmethod
    def make_calls():
        pass

class MessagingDevice():
    @abstractmethod
    def send_sms():
        pass

class InternetbrowsingDevice():
    @abstractmethod
    def browse_internet():
        pass

class SmartPhone(CallingDevice, MessagingDevice, InternetbrowsingDevice):
    def make_calls():
        #implementation
        pass

    def send_sms():
        #implementation
        pass

    def browse_internet():
        #implementation
        pass

class LandlinePhone(CallingDevice):
    def make_calls():
        #implementation
        pass
<?php
interface CallingDevice {
    public function make_calls();
}

interface MessagingDevice {
    public function send_sms();
}

interface InternetbrowsingDevice {
    public function browse_internet();
}

class SmartPhone implements CallingDevice, MessagingDevice, InternetbrowsingDevice {
    public function make_calls() {
        // TO DO
    }

    public function send_sms() {
        // TO DO
    }

    public function browse_internet() {
        // TO DO
    }
}

class LandlinePhone implementsCallingDevice {
    public function make_calls() {
        // TO DO
    }
}

Задачи

Задача 1. Принтер и сканер

Интерфейс MultifunctionDevice содержит методы print(), scan() и fax(). Класс SimplePrinter реализует его, но не поддерживает сканирование и факс. Нарушает ли это ISP? Разбейте интерфейс.

from abc import abstractmethod

# Нарушение ISP — толстый интерфейс
class MultifunctionDevice:
    @abstractmethod
    def print_doc(self, document: str): pass

    @abstractmethod
    def scan_doc(self, document: str): pass

    @abstractmethod
    def send_fax(self, document: str): pass

class AdvancedPrinter(MultifunctionDevice):
    def print_doc(self, document: str):
        print(f"Печать: {document}")

    def scan_doc(self, document: str):
        print(f"Сканирование: {document}")

    def send_fax(self, document: str):
        print(f"Отправка факса: {document}")

class SimplePrinter(MultifunctionDevice):
    def print_doc(self, document: str):
        print(f"Печать: {document}")

    def scan_doc(self, document: str):
        raise NotImplementedError("Сканер не поддерживается")

    def send_fax(self, document: str):
        raise NotImplementedError("Факс не поддерживается")

# Вызов
printer = SimplePrinter()
printer.print_doc("Договор.pdf")
printer.scan_doc("Договор.pdf")  # NotImplementedError
<?php
// Нарушение ISP — толстый интерфейс
interface MultifunctionDevice {
    public function print_doc(string $document): void;
    public function scan_doc(string $document): void;
    public function send_fax(string $document): void;
}

class AdvancedPrinter implements MultifunctionDevice {
    public function print_doc(string $document): void {
        echo "Печать: {$document}\n";
    }
    public function scan_doc(string $document): void {
        echo "Сканирование: {$document}\n";
    }
    public function send_fax(string $document): void {
        echo "Отправка факса: {$document}\n";
    }
}

class SimplePrinter implements MultifunctionDevice {
    public function print_doc(string $document): void {
        echo "Печать: {$document}\n";
    }
    public function scan_doc(string $document): void {
        throw new \RuntimeException("Сканер не поддерживается");
    }
    public function send_fax(string $document): void {
        throw new \RuntimeException("Факс не поддерживается");
    }
}

// Вызов
$printer = new SimplePrinter();
$printer->print_doc("Договор.pdf");
$printer->scan_doc("Договор.pdf"); // RuntimeException

Задача 2. Работник и робот

Интерфейс Worker содержит методы work() и eat(). Класс Robot реализует его, но роботы не едят. Устраните нарушение ISP.

from abc import abstractmethod

# Нарушение ISP
class Worker:
    @abstractmethod
    def work(self): pass

    @abstractmethod
    def eat(self): pass

class HumanWorker(Worker):
    def work(self):
        print("Человек работает")

    def eat(self):
        print("Человек обедает")

class RobotWorker(Worker):
    def work(self):
        print("Робот работает")

    def eat(self):
        raise NotImplementedError("Роботы не едят")

def manage(workers: list[Worker]):
    for w in workers:
        w.work()
        w.eat()

# Вызов
manage([HumanWorker(), RobotWorker()])  # упадёт на RobotWorker
<?php
// Нарушение ISP
interface Worker {
    public function work(): void;
    public function eat(): void;
}

class HumanWorker implements Worker {
    public function work(): void { echo "Человек работает\n"; }
    public function eat(): void  { echo "Человек обедает\n"; }
}

class RobotWorker implements Worker {
    public function work(): void { echo "Робот работает\n"; }
    public function eat(): void  {
        throw new \RuntimeException("Роботы не едят");
    }
}

function manage(array $workers): void {
    foreach ($workers as $w) {
        $w->work();
        $w->eat();
    }
}

// Вызов
manage([new HumanWorker(), new RobotWorker()]); // упадёт на RobotWorker

Задача 3. Репозиторий данных

Интерфейс Repository содержит методы find(), save(), delete() и export_csv(). Класс ReadOnlyRepository реализует только find(), а остальные методы бросают исключения. Разбейте интерфейс по ролям.

from abc import abstractmethod

# Нарушение ISP
class Repository:
    @abstractmethod
    def find(self, id: int): pass

    @abstractmethod
    def save(self, entity: dict): pass

    @abstractmethod
    def delete(self, id: int): pass

    @abstractmethod
    def export_csv(self) -> str: pass

class UserRepository(Repository):
    def find(self, id: int):
        return {'id': id, 'name': 'Анна'}

    def save(self, entity: dict):
        print(f"Сохранение: {entity}")

    def delete(self, id: int):
        print(f"Удаление id={id}")

    def export_csv(self) -> str:
        return "id,name\n1,Анна"

class ReadOnlyRepository(Repository):
    def find(self, id: int):
        return {'id': id, 'name': 'Иван'}

    def save(self, entity: dict):
        raise PermissionError("Запись запрещена")

    def delete(self, id: int):
        raise PermissionError("Удаление запрещено")

    def export_csv(self) -> str:
        raise NotImplementedError("Экспорт не поддерживается")

# Вызов
repo = ReadOnlyRepository()
print(repo.find(1))
repo.save({'name': 'Новый'})  # PermissionError
<?php
// Нарушение ISP
interface Repository {
    public function find(int $id): array;
    public function save(array $entity): void;
    public function delete(int $id): void;
    public function export_csv(): string;
}

class UserRepository implements Repository {
    public function find(int $id): array { return ['id' => $id, 'name' => 'Анна']; }
    public function save(array $entity): void { echo "Сохранение\n"; }
    public function delete(int $id): void { echo "Удаление id={$id}\n"; }
    public function export_csv(): string { return "id,name\n1,Анна"; }
}

class ReadOnlyRepository implements Repository {
    public function find(int $id): array { return ['id' => $id, 'name' => 'Иван']; }
    public function save(array $entity): void {
        throw new \RuntimeException("Запись запрещена");
    }
    public function delete(int $id): void {
        throw new \RuntimeException("Удаление запрещено");
    }
    public function export_csv(): string {
        throw new \RuntimeException("Экспорт не поддерживается");
    }
}

// Вызов
$repo = new ReadOnlyRepository();
print_r($repo->find(1));
$repo->save(['name' => 'Новый']); // RuntimeException

Задача 4. Медиаплеер

Интерфейс MediaPlayer содержит методы play_audio(), play_video() и show_subtitles(). Класс AudioPlayer поддерживает только аудио. Устраните нарушение ISP.

from abc import abstractmethod

# Нарушение ISP
class MediaPlayer:
    @abstractmethod
    def play_audio(self, file: str): pass

    @abstractmethod
    def play_video(self, file: str): pass

    @abstractmethod
    def show_subtitles(self, file: str): pass

class VideoPlayer(MediaPlayer):
    def play_audio(self, file: str):
        print(f"Воспроизведение аудио: {file}")

    def play_video(self, file: str):
        print(f"Воспроизведение видео: {file}")

    def show_subtitles(self, file: str):
        print(f"Субтитры: {file}")

class AudioPlayer(MediaPlayer):
    def play_audio(self, file: str):
        print(f"Воспроизведение аудио: {file}")

    def play_video(self, file: str):
        raise NotImplementedError("Видео не поддерживается")

    def show_subtitles(self, file: str):
        raise NotImplementedError("Субтитры не поддерживаются")

# Вызов
player = AudioPlayer()
player.play_audio("song.mp3")
player.play_video("movie.mp4")  # NotImplementedError
<?php
// Нарушение ISP
interface MediaPlayer {
    public function play_audio(string $file): void;
    public function play_video(string $file): void;
    public function show_subtitles(string $file): void;
}

class VideoPlayer implements MediaPlayer {
    public function play_audio(string $file): void { echo "Аудио: {$file}\n"; }
    public function play_video(string $file): void { echo "Видео: {$file}\n"; }
    public function show_subtitles(string $file): void { echo "Субтитры: {$file}\n"; }
}

class AudioPlayer implements MediaPlayer {
    public function play_audio(string $file): void { echo "Аудио: {$file}\n"; }
    public function play_video(string $file): void {
        throw new \RuntimeException("Видео не поддерживается");
    }
    public function show_subtitles(string $file): void {
        throw new \RuntimeException("Субтитры не поддерживаются");
    }
}

// Вызов
$player = new AudioPlayer();
$player->play_audio("song.mp3");
$player->play_video("movie.mp4"); // RuntimeException

Задача 5. Платёжный шлюз

Интерфейс PaymentGateway содержит методы charge(), refund(), get_transaction_history() и generate_report(). Некоторые шлюзы не поддерживают возвраты или отчёты. Разбейте интерфейс по ролям и создайте класс BasicGateway, реализующий только оплату.

from abc import abstractmethod

# Нарушение ISP
class PaymentGateway:
    @abstractmethod
    def charge(self, amount: float): pass

    @abstractmethod
    def refund(self, transaction_id: str): pass

    @abstractmethod
    def get_transaction_history(self) -> list: pass

    @abstractmethod
    def generate_report(self) -> str: pass

class FullGateway(PaymentGateway):
    def charge(self, amount: float):
        print(f"Списание: {amount} руб.")

    def refund(self, transaction_id: str):
        print(f"Возврат по транзакции {transaction_id}")

    def get_transaction_history(self) -> list:
        return [{'id': 'tx1', 'amount': 500}]

    def generate_report(self) -> str:
        return "Отчёт: транзакций — 1"

class BasicGateway(PaymentGateway):
    def charge(self, amount: float):
        print(f"Списание: {amount} руб.")

    def refund(self, transaction_id: str):
        raise NotImplementedError("Возвраты не поддерживаются")

    def get_transaction_history(self) -> list:
        raise NotImplementedError("История недоступна")

    def generate_report(self) -> str:
        raise NotImplementedError("Отчёты недоступны")

# Вызов
gw = BasicGateway()
gw.charge(1500.0)
gw.refund("tx123")  # NotImplementedError
<?php
// Нарушение ISP
interface PaymentGateway {
    public function charge(float $amount): void;
    public function refund(string $transaction_id): void;
    public function get_transaction_history(): array;
    public function generate_report(): string;
}

class FullGateway implements PaymentGateway {
    public function charge(float $amount): void { echo "Списание: {$amount} руб.\n"; }
    public function refund(string $transaction_id): void { echo "Возврат: {$transaction_id}\n"; }
    public function get_transaction_history(): array { return [['id' => 'tx1', 'amount' => 500]]; }
    public function generate_report(): string { return "Отчёт: транзакций — 1"; }
}

class BasicGateway implements PaymentGateway {
    public function charge(float $amount): void { echo "Списание: {$amount} руб.\n"; }
    public function refund(string $transaction_id): void {
        throw new \RuntimeException("Возвраты не поддерживаются");
    }
    public function get_transaction_history(): array {
        throw new \RuntimeException("История недоступна");
    }
    public function generate_report(): string {
        throw new \RuntimeException("Отчёты недоступны");
    }
}

// Вызов
$gw = new BasicGateway();
$gw->charge(1500.0);
$gw->refund("tx123"); // RuntimeException