Как избежать нарушения принципа интерфейсной сегрегации при проектировании коллекций
Что проверяют
Понимаете ли вы ISP не как лозунг, а как инструмент: клиент зависит только от тех методов, которыми пользуется. Почти всегда следом спрашивают, нарушает ли этот принцип сама Java Collections Framework и почему List.of(1, 2).add(3) падает.
Как отвечать
Начните с честного примера из JDK. Collection объявляет add, remove, clear как «необязательные операции»: неизменяемые реализации (List.of, Collections.unmodifiableList) бросают UnsupportedOperationException. Это осознанный компромисс авторов JCF: отдельные интерфейсы для изменяемых, неизменяемых коллекций и коллекций фиксированного размера дали бы комбинаторный взрыв типов. Цена: ошибка видна в рантайме, а не при компиляции.
Дальше каркас для своего кода:
- узкий тип на входе: метод, который только читает, принимает
Iterable<T>илиCollection<? extends T>, а неArrayList<T>. Правило PECS подсказывает, гдеextends, а гдеsuper; - разделение чтения и записи:
ReadOnlyCatalogсfindиstreamотдельно отCatalogWriterсaddиremove. Реализация может поддерживать оба, клиент видит только нужный; - пример из JDK 21:
SequencedCollectionвынесgetFirst,getLast,reversedв отдельный интерфейс, вместо того чтобы дописывать их вCollection; - read-only представления на выходе: возвращайте
List.copyOfилиCollections.unmodifiableList, чтобы наружу не утекала возможность менять внутреннее состояние; - default-методы: расширяют интерфейс без поломки реализаций, но не повод складывать туда всё подряд.
Для сравнения: в Kotlin List только читает, а изменяемость вынесена в MutableList.
Где спотыкаются
Режут интерфейсы до одного метода: Addable, Removable, Readable. Получается десяток типов, которые никто не комбинирует осмысленно, а ISP про потребности клиентов, а не про количество методов.
Путают ISP с SRP. Сегрегация интерфейсов говорит о том, что видит потребитель, а не о том, сколько обязанностей у класса.
Считают UnsupportedOperationException нормальным решением в своём коде. В JDK это вынужденная мера ради совместимости, а в новом API это ещё и нарушение LSP: подтип обещает метод, которого на деле нет.