Урок 04

SOLID: ISP — разделение интерфейсов

ISP

Interface Segregation Principle — клиент не должен зависеть от методов, которые он не использует.

Проще: много маленьких интерфейсов лучше, чем один большой.

Проблема

Большой интерфейс:

interface Worker
{
    public function work(): void;
    public function eat(): void;
    public function sleep(): void;
    public function takeBreak(): void;
}

Класс-робот:

class Robot implements Worker
{
    public function work(): void { /* работает */ }

    public function eat(): void
    {
        throw new RuntimeException('Робот не ест');
    }

    public function sleep(): void
    {
        throw new RuntimeException('Робот не спит');
    }

    public function takeBreak(): void
    {
        throw new RuntimeException('Робот не устаёт');
    }
}

Проблемы:

  • Робот вынужден реализовать ненужные методы.
  • Ошибки в рантайме.
  • Клиент зависит от методов, которые не использует.

Решение

Маленькие интерфейсы:

interface Workable
{
    public function work(): void;
}

interface Eatable
{
    public function eat(): void;
}

interface Sleepable
{
    public function sleep(): void;
}

Классы реализуют только нужное:

final class Human implements Workable, Eatable, Sleepable
{
    public function work(): void { /* ... */ }
    public function eat(): void { }
    public function sleep(): void { }
}

final class Robot implements Workable
{
    public function work(): void { }
}

Никаких лишних методов.

Практика

Плохо:

interface Repository
{
    public function find(int $id): object;
    public function findAll(): array;
    public function save(object $entity): void;
    public function delete(int $id): void;
    public function paginate(int $page, int $perPage): array;
    public function search(string $query): array;
    public function count(): int;
}

Read-only репозиторий — вынужден реализовать save/delete.

Хорошо:

interface ReadableRepository
{
    public function find(int $id): object;
    public function findAll(): array;
    public function count(): int;
}

interface WritableRepository
{
    public function save(object $entity): void;
    public function delete(int $id): void;
}

interface SearchableRepository
{
    public function search(string $query): array;
}

Клиент использует только нужное.

Клиентские интерфейсы

Интерфейс создаётся под клиента:

// Клиенту нужен только приветствие
interface Greeter
{
    public function greet(): string;
}

// А класс умеет больше
final class User implements Greeter
{
    public function greet(): string { return 'Привет'; }
    public function getEmail(): string { return 'x@x.com'; }
    public function getAge(): int { return 25; }
}

// Клиент видит только greet()
function sayHello(Greeter $g): string
{
    return $g->greet();
}

Клиент не знает о других методах.

Разделение по ролям

Один класс — разные роли:

final class User implements JsonSerializable, Arrayable, Stringable
{
    public function jsonSerialize(): array { return $this->toArray(); }
    public function toArray(): array { return []; }
    public function __toString(): string { return 'User'; }
}

Каждая роль — отдельный интерфейс.

Пример: HTTP-клиент

Плохо:

interface HttpClient
{
    public function get(string $url): Response;
    public function post(string $url, array $data): Response;
    public function put(string $url, array $data): Response;
    public function delete(string $url): Response;
    public function patch(string $url, array $data): Response;
    public function download(string $url, string $path): void;
    public function upload(string $url, string $path): Response;
}

Клиент, который делает только GET, зависит от всех методов.

Хорошо:

interface Reader
{
    public function get(string $url): Response;
}

interface Writer
{
    public function post(string $url, array $data): Response;
    public function put(string $url, array $data): Response;
    public function delete(string $url): Response;
}

interface FileTransfer
{
    public function download(string $url, string $path): void;
    public function upload(string $url, string $path): Response;
}

LSP и ISP

LSP — про корректность наследников. ISP — про ширину интерфейсов.

Связаны: ISP помогает не нарушать LSP — узкие интерфейсы легче правильно реализовать.

Тестирование

Маленькие интерфейсы легко мокать:

interface Logger
{
    public function log(string $message): void;
}

$mock = new class implements Logger {
    public array $messages = [];
    public function log(string $message): void
    {
        $this->messages[] = $message;
    }
};

Один метод — просто.

Big interface — мок раздувается.

Практика: уведомления

Плохо:

interface Notifier
{
    public function sendEmail(string $to, string $msg): void;
    public function sendSms(string $to, string $msg): void;
    public function sendPush(string $to, string $msg): void;
    public function sendTelegram(string $to, string $msg): void;
}

Email-сервис вынужден заглушать SMS, Push, Telegram.

Хорошо:

interface Notifier
{
    public function send(string $to, string $message): void;
    public function supports(string $channel): bool;
}

final class EmailNotifier implements Notifier
{
    public function send(string $to, string $message): void
    {
        echo "Email: $to\n";
    }

    public function supports(string $channel): bool
    {
        return $channel === 'email';
    }
}

final class SmsNotifier implements Notifier
{
    public function send(string $to, string $message): void
    {
        echo "SMS: $to\n";
    }

    public function supports(string $channel): bool
    {
        return $channel === 'sms';
    }
}

Не слишком мелко

⚠️ Не атомизируй до абсурда:

interface NameGetter { public function getName(): string; }
interface EmailGetter { public function getEmail(): string; }
interface AgeGetter { public function getAge(): int; }
interface NameSetter { public function setName(string $name): void; }
// ... 20 интерфейсов

Правило: интерфейс = роль, не метод.

Хорошо:

interface UserProfile
{
    public function getName(): string;
    public function getEmail(): string;
    public function getAge(): int;
}

ISP + композиция

Собирай поведение из маленьких:

final class UserService implements
    ReadableUserRepository,
    WritableUserRepository,
    UserNotifier
{
    // ...
}

Разделение обязанностей на уровне интерфейсов.

Проверка

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

  1. Метод-заглушка:
    public function eat(): void { /* пусто */ }
    
  2. Исключение «не поддерживается»:
    throw new RuntimeException('Not supported');
    
  3. Клиенты используют 1-2 метода из 10.
  4. instanceof для разных веток.

Практика: файловое хранилище

<?php

declare(strict_types=1);

interface FileReader
{
    public function read(string $path): string;
    public function exists(string $path): bool;
}

interface FileWriter
{
    public function write(string $path, string $content): void;
    public function delete(string $path): void;
}

interface FileLister
{
    public function list(string $directory): array;
}

final class LocalFileSystem implements FileReader, FileWriter, FileLister
{
    public function read(string $path): string
    {
        return file_get_contents($path) ?: '';
    }

    public function exists(string $path): bool
    {
        return file_exists($path);
    }

    public function write(string $path, string $content): void
    {
        file_put_contents($path, $content);
    }

    public function delete(string $path): void
    {
        unlink($path);
    }

    public function list(string $directory): array
    {
        return scandir($directory) ?: [];
    }
}

final class ReadOnlyCloudStorage implements FileReader
{
    public function read(string $path): string
    {
        return 'cloud content';
    }

    public function exists(string $path): bool
    {
        return true;
    }
}

function loadConfig(FileReader $reader, string $path): string
{
    if (!$reader->exists($path)) {
        throw new RuntimeException("Не найден: $path");
    }

    return $reader->read($path);
}

$local = new LocalFileSystem();
$cloud = new ReadOnlyCloudStorage();

// Оба могут читать
loadConfig($local, '/tmp/config.json');
loadConfig($cloud, 'config.json');

ReadOnlyCloudStorage не вынужден реализовывать запись.

Итог

  • ISP — много маленьких интерфейсов вместо одного большого.
  • Клиент не зависит от того, что не использует.
  • Интерфейс = роль, не метод.
  • Не атомизируй до абсурда.
  • Помогает LSP и упрощает тесты.
  • Разделяй по клиентам.

Дальше — SOLID: DIP — инверсия зависимостей.