Как изолированные классы, так и изолированный интерфейс помогают представлять ограниченные иерархии классов, гарантируя, что все возможные подклассы или реализации будут известны на этапе компиляции.

Источник: unsplash

Введение

Начиная изучать продвинутые концепции Kotlin, вы непременно столкнетесь с термином «изолированный/запечатанный» (англ. «sealed»)  — в отношении как изолированных классов, так и изолированных интерфейсов.

Когда же следует использовать изолированный класс, а когда подойдет изолированный интерфейс?

Рассмотрим подробно оба понятия.

Изолированный класс

Изолированный класс в Kotlin используется для ограничения наследования заранее определенным набором подклассов.

Представьте его как перечисление (enum) с дополнительной гибкостью — каждый подкласс может содержать различные типы данных или логику. Перечисление используется для простых констант, а изолированный класс — когда состояния содержат контекстные данные или поведение.

В данном случае UiState может быть представлен только одним из трех типов: SuccessError и Loading.

Можно использовать это в выражении when без необходимости применять ветку else:

Изолированный класс может иметь конструктор и, следовательно, может содержать свойства (состояние), общие для всех его подклассов. Класс может расширять только один другой класс (изолированный класс). Это ограничивает подтипы одним путем наследования.

Все прямые подклассы должны находиться в том же файле, что и изолированный родительский класс (если родительский класс не находится в той же единице компиляции, начиная с Kotlin 1.9).

Изолированный интерфейс

Изолированный интерфейс ограничивает классы, которые могут его реализовывать. Но он предоставляет больше гибкости, поскольку классы могут реализовывать несколько интерфейсов.

Теперь можно создать класс, который реализует несколько изолированных интерфейсов при необходимости:

Изолированный интерфейс не может иметь конструктора. Это означает, что сам изолированный тип не может хранить состояние.

Класс может реализовывать несколько интерфейсов. Это главное преимущество изолированного интерфейса, поскольку подтип может реализовывать изолированный интерфейс, а также расширять другой класс или реализовывать другие интерфейсы.

Проще говоря, изолированный интерфейс направлен на обеспечение большей гибкости при написании кода.

Когда что использовать 

Изолированный класс: когда иерархия представляет различные состояния одной сущности и нужен базовый тип, который потенциально может содержать общие данные или логику. Такое часто встречается при управлении состояниями (например, сетевые результаты, UI-состояния). Каждый подкласс должен содержать разные данные.

Изолированный интерфейс: когда иерархия представляет собой закрытый набор типов, которые имеют общее поведение (определенное в методах интерфейса), но могут быть частью других, не связанных между собой иерархий классов. Вам необходимо определить контракты или возможности, которые может иметь тип.

Оба типа — как изолированные классы, так и изолированные интерфейсы — теперь могут функционировать на верхнем уровне или быть вложенными.

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

Еще одно важное отличие заключается в том, что изолированные интерфейсы поддерживают обобщенные модификаторы вариантности — такие как  out (ковариантность) и in (контрвариантность) — тогда как изолированные классы их не поддерживают.

Разберемся, что это значит, на небольшом примере:

В данном случае Response<out T> означает ковариантность — можно безопасно присвоить Response<String> переменной типа Response<Any>.

Это особенно полезно при работе с обобщенными обертками для результатов API или репозиториев, поскольку такой подход сохраняет гибкость и типобезопасность архитектуры.

А вот что будет, если попытаться сделать то же самое с изолированным классом:

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

Изолированные классы и интерфейсы повышают типобезопасность и удобочитаемость Kotlin-кода — особенно при моделировании UI-состояний, сетевых ответов или доменных событий.

Поэтому при проектировании обобщенных типобезопасных иерархий результатов — таких как Response<T>UiState<T>, or Action<T> — следует использовать изолированный интерфейс вместо изолированного класса.

Ссылка на документацию:
https://kotlinlang.org/docs/sealed-classes.html

В следующий раз, перед проектированием доменного слоя или слоя представления, остановитесь и спросите себя: «Мне нужен закрытый набор состояний или гибкий набор поведений?».

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Richa Sharma: Kotlin: Sealed Classes and Interface

Предыдущая статья5 частых ошибок начинающих программистов
Следующая статьяКак сделать собственный Git с нуля на Go