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