В новом переводе от команды Spring АйО рассмотрим, как sealed классы и интерфейсы позволяют строго контролировать иерархию классов, обеспечивая тем самым безопасность и простоту поддержки кода.
Мы познакомимся с ключевыми особенностями sealed классов и интерфейсов, их влиянием на архитектуру приложений и практическими примерами их использования.
Когда в язык Java были введены sealed классы и интерфейсы (термин “sealed” в различных источниках может переводиться как “запечатанные”, “закрытые” или “изолированные”, либо не переводиться вовсе — прим. пер.), это стало существенным шагом в сторону улучшения безопасности типов и предсказуемости кода. Эти возможности языка предоставляют мощный механизм для ограничения иерархий наследования и для гарантии того, что только явно определенные подклассы или имплементации могли расширять или имплементировать заданный класс или интерфейс. Sealed классы и интерфейсы помогают создать более устойчивую, легко поддерживаемую и понятную кодовую базу, ограничивая возможное множество типов, которые могут наследоваться от них.
В этой статье рассматриваются тонкости sealed классов и интерфейсов, подробно изучаются их синтаксис, примеры применения и преимущества, которые они предлагают для современной разработки на Java.
1. Понимание sealed классов
Что такое sealed классы?
Представьте себе sealed класс как класс, который сам решает, кто может от него наследоваться. В отличие от обычных классов, которые может расширять любой класс, sealed классы в явном виде перечисляют классы, которым разрешено быть их дочерними классами. Такой контроль дает нам больше уверенности в том, как именно тот или иной класс будет использоваться в нашем коде.
Как создать sealed класс
Чтобы создать sealed класс, мы используем ключевое слово sealed, за которым следует ключевое слово permits для задания списка разрешенных подклассов:
sealed class Shape permits Circle, Rectangle, Triangle {
// ...
}
В этом примере только Circle, Rectangle, и Triangle могут расширять класс Shape.
Non-sealed и final классы
Чтобы прояснить ситуацию, упомянем два других типа классов, относящихся к наследованию:
Non-sealed классы: это тип класса, используемый по умолчанию, любой класс может расширять их.
Final классы: эти классы вообще нельзя расширять.
Зачем использовать sealed классы?
Sealed классы имеют несколько преимуществ:
Предсказуемость: когда мы точно знаем, какие классы могут расширять sealed класс, код становится проще понимать и поддерживать.
Безопасность типов: ограничения на возможные подтипы помогает предотвращать неожиданное поведение и ошибки.
Паттерны проектирования: sealed классы могут эффективно использоваться для реализации паттернов проектирования, таких как State (состояние) или Visitor (посетитель).
Пример: иерархия класса Shape
Для иллюстрации сказанного давайте создадим простую иерархию, используя sealed классы:
sealed class Shape permits Circle, Rectangle, Triangle {
abstract double area();
}
final class Circle extends Shape {
// ...
}
final class Rectangle extends Shape {
// ...
}
final class Triangle extends Shape {
// ...
}
В этом примере класс Shape является sealed, и только Circle, Rectangle, и Triangle могут наследовать от него. Это гарантирует нам, что любая соответствующая форме предмета переменная shape, с которой мы можем столкнуться, будет принадлежать только к одному из трех типов.
2. Случаи использования sealed классов
Моделирование конечных состояний
Одно из наиболее распространенных применений для sealed классов — это представление конечного множества состояний. Например:
Состояния UI: загрузка, ошибка, успех, бездействие.
Состояния сетевого запроса: ожидает, в процессе, успех, неудача.
Состояния игры: играет, на паузе, окончена.
Используя sealed класс, мы можем гарантировать, что все возможные состояния определены в явном виде и корректно обрабатываются, предотвращая ошибки в runtime:
sealed class UIState {
data class Loading(val isLoading: Boolean) : UIState()
data class Error(val message: String) : UIState()
data class Success(val data: Any) : UIState()
object Idle : UIState()
}
Версия для Java от команды Spring АйО
import lombok.Getter;
import lombok.RequiredArgsConstructor;
public sealed class UIState permits UIState.Loading, UIState.Error, UIState.Success, UIState.Idle {
@Getter
@RequiredArgsConstructor
public static final class Loading extends UIState {
private final boolean isLoading;
}
@Getter
@RequiredArgsConstructor
public static final class Error extends UIState {
private final String message;
}
@Getter
@RequiredArgsConstructor
public static final class Success extends UIState {
private final Object data;
}
public static final class Idle extends UIState {
private Idle() {}
public static final Idle INSTANCE = new Idle();
}
}
Представление алгебраических типов данных (Algebraic Data Types - ADTs)
Sealed классы идеальны для моделирования ADTs, которые являются фундаментом функционального программирования. ADTs состоят из конечного набора конструкторов данных, при этом с каждым таким типом ассоциируются специфичные для него данные.
sealed class Maybe<T> {
data class Just(val value: T) : Maybe<T>()
object Nothing : Maybe<Nothing>()
}
Версия для Java от команды Spring АйО
public sealed class Maybe<T> permits Maybe.Just, Maybe.Nothing {
@Getter
@RequiredArgsConstructor
public static final class Just<T> extends Maybe<T> {
private final T value;
}
public static final class Nothing<T> extends Maybe<T> {
private Nothing() {}
private static final Nothing<?> INSTANCE = new Nothing();
public static <T> Nothing<T> get() {
return (Nothing<T>) Nothing.INSTANCE;
}
}
}
Реализация паттернов проектирования
Sealed классы могут использоваться для реализации паттернов проектирования, таких как “Состояние” (State), в которых состояние объекта может меняться с течением времени. Различные состояния могут быть представлены в виде подклассов sealed класса.
Расширение сопоставления паттернов
Sealed классы идеально работают с сопоставлением паттернов, позволяя вам полностью проверить все возможные случаи:
fun processUIState(uiState: UIState) {
when (uiState) {
is UIState.Loading -> showLoadingIndicator()
is UIState.Error -> showErrorMessage(uiState.message)
is UIState.Success -> showData(uiState.data)
is UIState.Idle -> doNothing()
}
}
Версия для Java от команды Spring АйО
public void processUIState(UIState uiState) {
switch (uiState) {
case UIState.Loading loading -> showLoadingIndicator();
case UIState.Error error -> showErrorMessage(error.getMessage());
case UIState.Success success -> showData(success.getData());
case UIState.Idle idle -> doNothing();
default -> throw new IllegalStateException("Unexpected value: " + uiState);
}
}
Ограничение наследования
Sealed классы предлагают способ контроля за тем, какие классы могут расширять базовый класс, тем самым улучшая поддерживаемость кода и предотвращая появление неожиданных подклассов.
3. Сравнение с традиционным наследованием
Классическое наследование обеспечивает неограниченную расширяемость, позволяя любому классу наследовать другой, если это не ограничено явно с помощью ключевого слова final. Такая гибкость дает много возможностей, но при неправильном подходе может привести к неожиданным результатам.
Sealed классы вводят контролируемую форму наследования. Задавая в явном виде разрешенные подклассы, они обеспечивают более предсказуемый и управляемый подход. В то время как традиционное наследование предлагает нам максимальную гибкость, sealed классы приоритезируют безопасность типов и понятность кода.
Ключевые различия:
Открытые / закрытые: традиционное наследование является незамкнутым, в то время как sealed классы закрыты и ограничивают количество потенциальных подклассов.
Предсказуемость: sealed классы предлагают нам больше предсказуемости в том, что касается иерархии классов и поведения.
Безопасность типов: sealed классы могут повысить безопасность типов, ограничивая набор возможных типов во время компиляции.
В целом, sealed классы представляют собой компромисс между полной свободой традиционного наследования и жесткой ограниченностью final классов. Они предоставляют полезный инструмент для создания надежных и предсказуемых иерархий классов.
4. Преимущества и недостатки sealed классов
Внизу представлена таблица, сравнивающая sealed классы и традиционное наследование, за которой следует объяснение преимуществ и недостатков sealed классов.
Показатель |
Традиционное наследование |
Sealed классы |
Расширяемость |
Незамкнутая |
Закрытая (ограниченный набор подклассов) |
Предсказуемость |
Ниже |
Выше |
Безопасность типов |
Умеренная |
Выше |
Поддерживаемость |
Умеренная |
Выше |
Сопоставление паттернов |
Менее эффективное |
Более эффективное |
Обеспечение соблюдения спроектированной архитектуры |
Ограниченное |
Более сильное |
Преимущества sealed классов
Повышенная предсказуемость кода: ограничивая набор возможных подклассов, sealed классы делают код более предсказуемым и упрощают его анализ. Разработчики могут с уверенностью предполагать, что инстансы sealed классов могут принадлежать только к специфическому набору типов.
Улучшенная безопасность типов: sealed классы вносят свой вклад в повышение безопасности типов, предотвращая появление подклассов, которые могут нарушать инварианты класса. Это может помочь поймать потенциальные ошибки во время компиляции.
Лучшая поддерживаемость: когда набор возможных подклассов ограничен, код становится более прост в поддержке. Также менее вероятной становится ситуация, когда изменения в базовом классе приводят к нежелательным последствиям.
Облегчают сопоставление паттернов: sealed классы идеально работают с сопоставлением паттернов, позволяя совершать полные проверки и писать более чистый код.
Обеспечивают соблюдение решений по спроектированной архитектуре: sealed классы могут использоваться для обеспечения специфических паттернов проектирования и ограничений, предотвращая неправильное использование иерархии классов.
Недостатки sealed классов
Пониженная гибкость: sealed классы ограничивают способность к расширению класса за пределы предзаданных подклассов, что может оказаться чрезмерно строгим ограничением в некоторых случаях.
Повышенная сложность: в то время как они могут повысить читаемость кода во многих ситуациях, sealed классы вводят дополнительную сложность в язык.
Потенциал для чрезмерного использования: sealed классы должны использоваться разумно. Чрезмерное их использование может привести к появлению слишком закостенелых иерархий классов и сделать код менее адаптивным к будущим изменениям.
Еще один недостаток по мнению сообщества
Редакция Spring АйО упоминает тот факт, что sealed классы провоцируют разработчиков на использование instanceof-проверок. Например, у некоторого класса X наследник может A или B. В случае необходимости расширения класса, происходит явное нарушение Open/Closed principle. Данный код является по своей сути бомбой замедленного действия, так как при расширении данный код:
if (o instanceof A) {
// logic for A
} else {
// logic for B
}
перестает работать. Митигировать можно использованием конструкции switch-case, однако факт остается фактом.
Если подвести итог, sealed классы предлагают ценный инструмент для улучшения качества кода и упрощения процесса его поддержки во многих сценариях. Однако, важно взвешивать преимущества и потенциальные недостатки, чтобы использовать их должным образом в вашей кодовой базе.
Дополнение от сообщества
Команда Spring АйО отдельно подчеркивает, что до введения sealed классов в Java отсутствовала возможность именно ограничить (не запретить) наследование без того, чтобы делать предка inaccesible
5. Best practices и на что обратить внимание
Sealed классы предлагают значительные преимущества, но их эффективное использование требует внимательного рассмотрения всех факторов. Далее приведено обобщение ключевых best practices и отдельных моментов, которые могут потребовать дополнительного рассмотрения:
Практика / повод к рассмотрению |
Описание |
Идентифицируйте подходящие случаи использования |
Используйте sealed классы, когда у вас есть хорошо определенный конечный набор подклассов, и вы хотите гарантированно обеспечить строгую иерархию. |
Ищите баланс между гибкостью и контролем |
В то время как sealed классы обеспечивают контроль, старайтесь не использовать их чрезмерно, так как это может помешать расширению функционала приложения в будущем. |
Подумайте о влиянии на производительность |
В коде, где критична производительность, проанализируйте потенциальное влияние вызовов виртуальных методов и непроизводительных затрат ресурсов при создании объектов. |
Используйте сопоставление шаблонов |
Комбинируйте sealed классы с сопоставлением паттернов для полных проверок и улучшения читаемости. |
Придерживайтесь соглашений по именованию |
Используйте понятные и описательные имена для sealed классов и их разрешенных подклассов. |
Подумайте о версионировании |
Если вы ожидаете, что в разрешенных подклассах со временем появятся изменения, планируйте стратегии по версионированию или миграции. |
Оцените поддержку в рамках инструмента |
Убедитесь, что ваша IDE и другие инструменты в должной мере поддерживают sealed классы. |
Регулярно проверяйте код |
Периодически перепроверяйте, как вы используете sealed классы, чтобы убедиться в том, что они все еще соответствуют вашим запланированным целям. |
Дополнительные поводы для рассмотрения
Избегайте чрезмерного использования подклассов: в то время как sealed классы позволяют использовать более одного подкласса, наличие чрезмерного их количества может привести к невозможности контролировать иерархию, как планировалось изначально.
Подумайте об альтернативах: в некоторых случаях перечисления (enums) или интерфейсы могут быть более подходящим выбором, чем sealed классы.
Ищите баланс между открытостью и закрытостью: в то время как sealed классы ограничивают расширение, они все еще могут соответствовать принципу открытости-закрытости через разрешение модификаций путем внесения изменений в поведение.
6. Выводы
Sealed классы в Java предлагают мощный механизм для контроля наследования и улучшения надежности кода. Ограничивая набор разрешенных подклассов, они улучшают предсказуемость кода, безопасность типов и поддерживаемость. В то время как они вводят дополнительное усложнение в код, из преимущества часто перевешивают недостатки. Тщательное рассмотрение случаев использования, потенциальные компромиссы и использование best practices чрезвычайно важно для эффективного использования sealed классов. За счет понимания их сильных сторон и ограничений, разработчики могут принимать взвешенные решения по поводу того, где и как внедрять такие классы в их кодовую базу.
![](https://habrastorage.org/getpro/habr/upload_files/bd9/31f/9e3/bd931f9e36dfe98194f8b83965a05564.png)
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм - Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
Ждем всех, присоединяйтесь
Комментарии (5)
valery1707
20.08.2024 20:22+2Интересно что структура
sealed
классов не обязана быть полностью закрытой так как один из разрешённых вариантов может иметь модификаторnon-sealed
и от него можно будет наследоваться.
Может быть полезным для библиотек и прочих мест где нужно всё же дать возможность неконтролируемо расширять иерархию классов.public sealed interface Demo permits Demo.Sealed1, Demo.Sealed2, Demo.Open { // Эти классы входят в закрытую часть структуры final class Sealed1 implements Demo {} final class Sealed2 implements Demo {} non-sealed class Open implements Demo {} // Это пример структуры, которая уже не контролируется ограничениями по `sealed` class External extends Open {} }
Klopper
20.08.2024 20:22Этотвсе прекрасно конечно, но блин, тут же явно получается циклическая зависимость между предком и наследником. Вот это точно повод не использовать эти классы.
valery1707
20.08.2024 20:22Я так понимаю что наличие информации о всех наследниках в родителе это не побочная проблема, а одна из целей функционала.
А ещё такая зависимость есть вenum
-ах, причём даже более жёсткая: родительский класс не только знает все его реализации, но они ещё и объявлены должны быть строго в пределах родительского класса.
Тогда как наследникиsealed
-класса могут быть объявлены где угодно (но в пределах видимости родительского - так как в нём необходимо указать потомка). Ну и с учётом возможности дать возможность расширять иерархию классов черезnon-sealed
потомка это вообще не выглядит не преодолимой проблемой.
Другое дело что не все классы имеет смысл объявлятьsealed
.
valery1707
Да,
Lombok
это часто удобно, но реализоватьUIState
удобнее на рекордах:и вот пример работы с ним:
Даже с типизированным
Maybe
фактически нет проблем, причём за счёт использованияenum
создать ещё один инстансNothing
даже через рефлексию будет проблематично.spring_aio Автор
Отличное дополнение, спасибо!