[Перевод] Модификаторы классов для сопровождающих API
Hola Amigos! На связи Тимур Моисеев, руководитель мобильной разработки Amiga. Я кандидат технических наук, в IT уже более 20 лет, а последние 4 года создаю мобильные приложения на Flutter.
Вместе с командой ведем телеграм-канал Flutter.Много, где рассказываем про свой личный опыт, делимся статьями, в том числе и переводами иностранных СМИ, описываем свои кейсы и анонсируем конференции, на которых с радостью встречаемся вживую с любителями кроссплатформенности. Нас собралось уже больше 1600 разработчиков в телеграм, так что присоединяйтесь, кому интересно!
Сегодня делюсь с вами переводом статьи, из которой вы узнаете, как использовать новые модификаторы и как они влияют на пользователей ваших библиотек.
Содержание:
Модификатор
mixin
классовДругие модификаторы согласия
Модификатор
interface_
Модификатор
base_
Модификатор
final_
Модификатор
sealed_
Модификатор классов — mixin (миксин)
Наиболее важным модификатором, о котором следует знать, является mixin
. Версии языка до Dart 3.0 позволяют использовать любой класс в качестве mixin
в with
предложении другого класса, ЕСЛИ только класс:
Объявляет любые нестандартные конструкторы.
Расширяет любой класс, отличный от
Object
.
Это упрощает случайное нарушение чужого кода путем добавления конструктора или extends
предложения к классу, не осознавая, что другие используют его в with
предложении.
Dart 3.0 больше не разрешает использовать классы в качестве mixin
по умолчанию. Вместо этого необходимо согласиться на такое поведение, объявив mixin class
:
mixin class Both {}
class UseAsMixin with Both {}
class UseAsSuperclass extends Both {}
Если обновить пакет до Dart 3.0 и не внести никаких изменений в код, то есть вероятность, что вы не увидите никаких ошибок. Но тогда можно непреднамеренно нарушить работу пользователей вашего пакета, если они использовали классы в качестве миксинов.
Миграция классов в mixin
Если класс имеет не factory
конструктор с extends
или with
, то он уже не может быть использован в качестве mixin
. Поведение такого класса не изменится с Dart 3.0.
На практике это описывает около 90% существующих классов. Что касается остальных классов, которые можно использовать в качестве миксинов, вы должны решить, что вы хотите поддерживать.
Вот несколько вопросов, которые помогут принять решение. Первый — прагматический:
Вы хотите рискнуть сломать (код) каких-либо пользователей? Если ответом является твердое «нет», то поставьте
mixin
перед любыми классами, которые можно было бы использовать в качестве миксина. Это в точности сохранит существующее поведение вашего API.
С другой стороны, если вы хотите воспользоваться этой возможностью, чтобы переосмыслить возможности, предлагаемые вашим API, тогда вы можете не превращать его в mixin class
. Рассмотрим эти два вопроса дизайна:
Хотите, чтобы пользователи могли создавать его экземпляры напрямую? Другими словами, намеренно ли класс не является абстрактным?
Хотите, чтобы люди могли использовать объявление в качестве дополнения? Другими словами, вы хотите, чтобы они могли использовать его в
with
предложениях?
Если ответ на оба вопроса «да», то сделайте его mixin class
. Если ответ на второй вопрос «нет», то просто оставьте его как class
. Если ответом на первый вопрос является «нет», а на второй — «да», то измените его с объявления class
на объявление mixin
.
Последние два варианта, оставляющие его классом или превращающие его в чистый микс, нарушают изменения API. Если вы сделаете это, вам захочется обновить основную версию вашего пакета.
Другие модификаторы
Обработка классов как миксинов — единственное критическое изменение в Dart 3.0, влияющее на API пакета. Как только вы зашли так далеко, вы можете остановиться, если не хотите вносить другие изменения в то, что ваш пакет позволяет делать пользователям.
Обратите внимание: если вы продолжите и будете использовать какой-либо из модификаторов, описанных ниже, это потенциально станет критическим изменением API вашего пакета, которое потребует увеличения основной версии.
Модификатор interface
В Dart нет отдельного синтаксиса для объявления чистых интерфейсов. Вместо этого объявляете абстрактный класс, который содержит только абстрактные методы. Когда пользователь видит этот класс в API пакета, он может не знать, содержит ли он код, который они могут повторно использовать путем расширения класса, или же он предназначен для использования в качестве интерфейса.
Можно уточнить это, поместив interface модификатор в класс. Это позволяет использовать класс в implements
предложении, но предотвращает его использование в extends
.
Даже если класс имеет не абстрактные методы, можно запретить пользователям расширять его. Наследование — один из самых мощных видов связи в программном обеспечении, поскольку оно позволяет повторно использовать код. Но эта связь также опасна и хрупка. Когда наследование выходит за границы пакета, может быть трудно развивать суперкласс, не нарушая подклассы.
Маркировка класса interface
позволяет пользователям создавать его (если он также не помечен abstract) и реализовывать интерфейс класса, но не позволяет им повторно использовать какой-либо его код.
Когда класс помечен interface
, ограничение может быть проигнорировано в библиотеке, где объявлен класс. Внутри библиотеки можно свободно расширять ее, поскольку это весь код разработчика. Ограничение распространяется на другие пакеты и даже другие библиотеки в личном пакете.
Модификатор — base
Модификатор base в некоторой степени противоположен interface
. Он позволяет использовать класс в extends
предложении или использовать mixin
, или mixin class
в with
предложении. Но это запрещает коду за пределами библиотеки класса использовать class
или mixin
в implements
предложении.
Это гарантирует, что каждый объект, являющийся экземпляром вашего класса или миксин интерфейса, наследует фактическую реализацию. В частности, это означает, что каждый экземпляр будет включать в себя все приватные члены, объявленные вашим классом или миксином. Это может помочь предотвратить ошибки во время выполнения, которые могли бы возникнуть в противном случае.
Рассмотрим эту библиотеку:
// a.dart
class A {
void _privateMethod() {
print('I inherited from A');
}
}
void callPrivateMethod(A a) {
a._privateMethod();
}
Этот код кажется прекрасным сам по себе, но ничто не мешает пользователю создать другую библиотеку, подобную этой:
// b.dart
import 'a.dart';
class B implements A {
// No implementation of _privateMethod()!
}
main() {
callPrivateMethod(B()); // Runtime exception!
}
Добавление base
модификатора к классу может помочь предотвратить эти ошибки во время выполнения. Как и в случае с interface
, вы можете проигнорировать это ограничение в той же библиотеке, где объявлен base class
или mixin
. Тогда подклассам в той же библиотеке будет напомнено о необходимости реализации приватных методов. Но обратите внимание, что следующий раздел действительно применим.
Базовая транзитивность
Транзити́вность
Транзити́вность (от лат. transitivus — переходный), свойство бинарных отношений, выражающее их «переносимость» с одних пар объектов на другие. Точнее, отношение R называется транзитивным, если для любых объектов x, y, z из xRy и yRz следует xRz. Наиболее важные классы транзитивных отношений — отношения типа равенств (эквивалентности) и отношения порядка.
Цель маркировки класса base
— гарантировать, что каждый экземпляр этого типа конкретно наследуется от него. Для поддержания этого (модификатора), base
ограничение является «заразным». Каждый подтип отмеченного типа base
— прямой или косвенный — также должен препятствовать реализации. Это означает, что он должен быть помечен base
(или final
или sealed
, о котором будет далее).
Тогда применение base
к типу требует некоторой осторожности. Это влияет не только на то, что пользователи могут делать с классом или миксином, но и на возможности, которые могут предложить их подклассы. После того, как произведен ввод base
типа, вся иерархия под ним не может быть реализована.
Звучит замысловато, но именно так всегда работало большинство других языков программирования. Большинство из них вообще не имеют неявных интерфейсов, поэтому, когда объявляется класс на Java, C# или других языках, разработчик фактически сталкивается с тем же ограничением.
Модификатор — final
Если нужны все ограничения обоих interface
и base
, можно пометить класс или смешанный класс final. Это не позволяет никому за пределами библиотеки создавать какие-либо ее подтипы: не использовать их в предложениях implements
, extends
, with
или on
.
Это наиболее ограничительный параметр для пользователей класса. Все, что они могут сделать, это сконструировать его (если он не отмечен abstract
). В свою очередь, у разработчика, как у сопровождающего класса, наименьшее количество ограничений. Он может добавлять новые методы, превращать конструкторы в фабричные конструкторы и т.д., не беспокоясь о том, что кто-то из нижестоящих пользователей может нарушить работу.
Модификатор — sealed
Последний модификатор, sealed, является специальным. Он существует в первую очередь для того, чтобы обеспечить проверку исчерпываемости при сопоставлении с образцом. Если switch
имеет case
для каждого подтипа, отмеченного sealed
, то компилятор знает, что переключатель является исчерпывающим.
// amigos.dart
sealed class Amigo {}
class Lucky extends Amigo {}
class Dusty extends Amigo {}
class Ned extends Amigo {}
String lastName(Amigo amigo) =>
switch (amigo) {
case Lucky _ => 'Day';
case Dusty _ => 'Bottoms';
case Ned _ => 'Nederlander';
}
Этот switch
имеет case
для каждого из подтипов Amigo
. Компилятор знает, что каждый экземпляр Amigo
должен быть экземпляром одного из этих подвидов, поэтому он знает, что переключатель является безопасным и не требует какого-либо default case
.
Чтобы это было правильно, компилятор накладывает два ограничения:
sealed
класс сам по себе не может быть непосредственно создан. В противном случае у вас мог бы быть экземплярAmigo
, который не является экземпляром ни одного из подтипов. Таким образом, каждыйsealed
класс также является неявнымabstract
.Каждый подтип
sealed
типа должен находиться в той же библиотеке, где объявлен запечатанный тип. Таким образом, компилятор может найти их все. Он знает, что нет других скрытых плавающих подтипов, которые не соответствовали бы ни одному из случаев.
Второе ограничение похоже на final
. Например, final
это означает, что класс, отмеченный sealed
, не может быть напрямую расширен, реализован или добавлен за пределы библиотеки, в которой он объявлен. Но, в отличие от base
и final
, здесь нет транзитивного ограничения:
// amigo.dart
sealed class Amigo {}
class Lucky extends Amigo {}
class Dusty extends Amigo {}
class Ned extends Amigo {}
// other.dart
// This is an error:
class Bad extends Amigo {}
// But these are both fine:
class OtherLucky extends Lucky {}
class OtherDusty implements Dusty {}
Конечно, если нужно, чтобы подтипы sealed
типа также были ограничены, можно получить это, пометив их с помощью interface
, base
, final
или sealed
.
sealed против final
Пара простых правил:
Если нужно, чтобы пользователи могли напрямую создавать экземпляры класса, то невозможно использовать
sealed
, посколькуsealed
типы неявно абстрактны.Если у класса нет подтипов в вашей библиотеке, то нет смысла использовать
sealed
, поскольку нет преимуществ проверки исчерпываемости.
В противном случае, если у класса действительно есть некоторые подтипы, которые необходимо определять, то sealed
— то, что нужно. Если пользователи видят, что класс имеет несколько подтипов, удобно иметь возможность обрабатывать каждый из них отдельно в качестве вариантов переключения и сообщать компилятору, что весь тип охвачен.
Использование sealed
означает, что если позже добавить в библиотеку другой подтип, это приведет к кардинальному изменению API. Когда появляется новый подтип, все эти существующие параметры становятся неполными, поскольку они не обрабатывают новый тип. Это точно так же, как добавление нового значения в перечисление.
Эти неполные ошибки компиляции switch полезны для пользователей, поскольку они привлекают внимание пользователя к местам в его коде, где им нужно будет обрабатывать новый тип.
Но это означает, что всякий раз, когда добавляется новый подтип, это кардинальное изменение. Для свободы добавления новых подтипов без сбоев лучше пометить супертип с помощью final
вместо sealed
. Это означает, что когда пользователь включает значение этого супертипа, даже если у него есть регистры для всех подтипов, компилятор заставит их добавить другой регистр по умолчанию.
Краткие сведения
Эти новые модификаторы дают вам контроль над тем, как пользователи работают с вашим кодом, и наоборот, как вы можете развивать свой код, не нарушая их.
Но эти опции влекут за собой сложность: теперь у вас есть больше возможностей для выбора. Кроме того, поскольку эти функции являются новыми, еще не известно, какими будут наилучшие практики. Экосистема каждого языка отличается и имеет разные потребности.
Если вы просто хотите сохранить свой API таким, каким он был, установите mixin
на классы, которые уже поддерживали это, и готово.
Со временем, когда вы поймете, где вам нужен более точный контроль, вы можете рассмотреть возможность применения некоторых других модификаторов:
Используйте
interface
, чтобы запретить пользователям повторно использовать код вашего класса, позволяя им повторно реализовать его интерфейс.Используйте
base
, чтобы потребовать от пользователей повторного использования кода вашего класса и убедиться, что каждый экземпляр типа вашего класса является экземпляром этого фактического класса или подкласса.Используйте
final
для полного предотвращения расширения класса.Используйте
sealed
, чтобы включить проверку исчерпываемости семейства подтипов.
Когда вы это сделаете, увеличьте основную версию при публикации вашего пакета, поскольку все эти модификаторы подразумевают ограничения, которые нарушают изменения.
На этом все! Подписывайтесь на наш телеграм-канал Flutter.Много, чтобы не пропустить выхода новых статей и другого интересного контента.