В новом переводе от команды 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 классов. За счет понимания их сильных сторон и ограничений, разработчики могут принимать взвешенные решения по поводу того, где и как внедрять такие классы в их кодовую базу.
Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
Ждем всех, присоединяйтесь
ссылка на оригинал статьи https://habr.com/ru/articles/837262/
Добавить комментарий