ISP (Interface Segregation Principle) — четвёртый принцип SOLID, который утверждает: клиенты не должны зависеть от методов, которые они не используют. Принцип был сформулирован Робертом Мартином в контексте проектирования интерфейсов для объектно-ориентированных систем. Как описано в книге Clean Architecture (2017), принцип разделения интерфейсов требует создавать узкоспециализированные интерфейсы вместо одного универсального, что уменьшает связанность и упрощает внесение изменений.
Главное
ISP (Interface Segregation Principle) — принцип разделения интерфейсов, который запрещает создавать <<толстые>> интерфейсы с методами, не используемыми всеми клиентами. Вместо одного интерфейса с десятком методов проектируются несколько маленьких интерфейсов, каждый для своей группы клиентов.
Принцип был введён Робертом Мартином как решение проблемы <<загрязнения интерфейсов>>, когда один класс вынужден реализовывать методы, которые ему не нужны, только потому что они объявлены в общем интерфейсе. В статически типизированных языках это выливается в пустые реализации или выброс исключений — прямой признак нарушения ISP.
ISP и SRP дополняют друг друга: SRP про ответственность класса, ISP про контракты интерфейсов. SRP говорит <<один класс — одна причина для изменения>>, ISP — <<один интерфейс — один клиентский сценарий>>. Вместе они формируют модульную архитектуру, где каждый элемент системы имеет чёткие границы.
Fat Interface — интерфейс, содержащий больше методов, чем нужно конкретному клиенту. Например, интерфейс Worker с методами work, eat, sleep. Робот-рабочий не должен реализовывать eat и sleep, но вынужден. Решение — разделить на Workable, Eatable, Sleepable. Каждый клиент получает ровно то, что ему нужно.
В мобильной разработке толстые интерфейсы встречаются в протоколах делегатов и DataSource. Один протокол может содержать методы для двух разных сценариев (редактирование + отображение), хотя конкретный экран использует только один из них.
Реализация ISP начинается с анализа клиентов каждого интерфейса. Если два клиента используют разные наборы методов одного интерфейса — интерфейс следует разделить. Каждый новый интерфейс группирует методы, которые вызываются вместе в рамках одного сценария.
Механизм разделения: исходный интерфейс разбивается на несколько узких, каждый из которых наследует общую часть (если она есть). Клиенты переключаются на зависимость от нужного узкого интерфейса вместо общего. Классы, реализующие исходный интерфейс, теперь реализуют только те узкие интерфейсы, которые им действительно нужны.
Важное уточнение: степень дробления определяется количеством клиентов и их сценариев. ISP не требует максимального дробления (микроинтерфейсы по одному методу). Это привело бы к избыточной сложности. Цель — устранить зависимость клиентов от ненужных методов, а не минимизировать размер каждого интерфейса.
Основные признаки нарушения ISP включают: классы, реализующие интерфейс с пустыми методами (фиктивная реализация), выброс исключения UnsupportedOperationException в реализациях, большое количество параметров или возвращаемых типов, которые не используются частью клиентов, и частые изменения интерфейса, затрагивающие только часть клиентов.
В Android-разработке типовой пример нарушения ISP — интерфейс OnItemClickListener, который включает методы для клика, долгого клика и свайпа. Если конкретный экран использует только клик — остальные методы остаются пустыми. Решение — разделить на OnItemClickListener, OnItemLongClickListener, OnItemSwipeListener.
В iOS-разработке нарушение ISP проявляется в делегатах UIKit: один протокол содержит методы для разных состояний компонента. UITableViewDelegate включает методы для отображения, выделения, редактирования и swipe-action. Часто разработчики реализуют протокол целиком с десятком пустых методов. Разделение на несколько протоколов по группам ответственности решает проблему.
Проблема не только в эстетике кода. Когда интерфейс меняется (добавляется новый метод), все классы-реализаторы должны быть обновлены — даже те, кому новый метод не нужен. В мобильной разработке с десятками экранов это приводит к каскадным изменениям. ISP изолирует каждый клиент от изменений, которые его не касаются.
Неявное нарушение ISP происходит через параметры конфигурации. Если метод принимает объект с большим количеством полей, а клиент использует только 2-3 из них — это сигнал к дроблению. Альтернатива: несколько специализированных методов с минимальным набором параметров.
В Android-разработке ISP нарушается при использовании одного SharedPreferencesManager для чтения и записи всех настроек приложения. Fragment, которому нужно только прочитать тему, получает зависимость от глобального менеджера с десятком методов для разных типов данных. Разделение на ThemePreferenceProvider, AuthPreferenceProvider, FeatureFlagProvider — применение ISP на уровне сервисов конфигурации. Каждый провайдер содержит ровно те методы, которые нужны его клиентам.
Рассмотрим Android-пример с интерфейсом для работы с данными. Нарушение ISP — один интерфейс для всех CRUD операций, хотя не всем клиентам нужны все операции.
// Нарушение ISP: толстый интерфейс
interface UserRepository {
fun getAll(): List<User>
fun getById(id: Int): User
fun save(user: User)
fun delete(id: Int)
}
// После применения ISP: узкие интерфейсы
interface UserReader {
fun getAll(): List<User>
fun getById(id: Int): User
}
interface UserWriter {
fun save(user: User)
fun delete(id: Int)
}
// ReadOnlyViewModel не зависит от методов записи
class ReadOnlyViewModel(
private val reader: UserReader
)
iOS-пример с разделением протоколов для работы с медиа:
// Нарушение ISP: один протокол для всей работы с медиа
protocol MediaService {
func play(url: URL)
func pause()
func stop()
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// После ISP: разделение на протоколы по ответственности
protocol MediaPlayer {
func play(url: URL)
func pause()
func stop()
}
protocol MediaTransfer {
func upload(data: Data) async -> URL
func download(url: URL) async -> Data
}
// PlayerViewModel не зависит от методов загрузки
class PlayerViewModel {
private let player: MediaPlayer
}
Практический вывод: ISP защищает клиентов от изменений в независящих от них частях интерфейса. Разделение UserRepository на UserReader и UserWriter означает, что изменения в save не затрагивают ReadOnlyViewModel, и наоборот. Каждый клиент изолирован от функциональности, которую не использует, и не требует изменений при доработке других частей системы.
ISP и SRP — естественная пара. SRP определяет, что класс должен иметь одну причину для изменения. ISP применяет ту же логику к интерфейсам: интерфейс должен обслуживать один клиентский сценарий. Класс может реализовывать несколько узких интерфейсов (каждый соответствует одной ответственности), что чище, чем один толстый интерфейс с несколькими ответственностями.
ISP и OCP также связаны: узкие интерфейсы легче расширять. Добавление нового метода в узкий интерфейс затрагивает только его клиентов. Добавление метода в толстый интерфейс затрагивает всех клиентов — потенциально нарушая OCP, если клиенты вынуждены менять свою реализацию.
ISP и DIP работают вместе: DIP требует зависимости от абстракций. ISP делает эти абстракции узкими и сфокусированными. Зависимость от широкого интерфейса — это всё ещё зависимость от абстракции, но <<плохой>> абстракции с точки зрения ISP. Четыре принципа (SRP, OCP, ISP, DIP) формируют <<пирамиду модульности>>: SRP и ISP определяют границы, OCP и DIP — способы расширения и связывания.
Компонентная архитектура в мобильных проектах (модули, фичи, слои) выигрывает от ISP на уровне публичных API. Каждый модуль экспортирует узкие интерфейсы для своих потребителей, а не один общий фасад. Это позволяет изменять внутреннюю реализацию модуля без влияния на потребителей, которые используют только часть его функциональности.
В Android-проектах с архитектурой Clean Architecture ISP применяется к UseCase: каждый UseCase — отдельный интерфейс с одним методом invoke или execute. Клиент (ViewModel) зависит только от того UseCase, который ему нужен, а не от целого репозитория. Это делает зависимости прозрачными и тестируемыми.
Часто задаваемые вопросы
Да, чрезмерное дробление возможно. ISP не требует одного интерфейса на каждый метод. Критерий: есть ли клиент, которому нужна только часть методов интерфейса. Если все клиенты используют все методы — интерфейс не нужно делить. Оптимальный уровень дробления определяется реальными сценариями использования.
ISP на уровне параметров означает: функция не должна принимать объекты с большим количеством полей, если использует только часть из них. Вместо этого стоит передавать только необходимые данные или использовать специализированные интерфейсы (например, интерфейс Renderable вместо полного User).
LSP про корректное наследование и поведенческую совместимость подтипов. ISP про проектирование интерфейсов: клиенты не должны зависеть от методов, которые не используют. LSP отвечает на вопрос «можно ли использовать подкласс вместо базового класса?», ISP — «нужен ли клиенту весь интерфейс целиком?».
Узкие интерфейсы упрощают создание mock-объектов: тест создаёт mock с одним-двумя методами, а не с десятком. Чем меньше методов в интерфейсе, тем проще заглушить его поведение. Это снижает когнитивную нагрузку на разработчика теста и уменьшает вероятность ошибок в mock-логике.
Если интерфейс стабилен и все клиенты используют все методы — дробление избыточно. Типичный пример: протоколы UIKit, которые проектировала Apple. Разделять их рискованно, так как UIKit ожидает полной реализации делегата. В таких случаях нарушение ISP оправдано стабильностью API.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также