В Котлине я часто встречаю следующий шаблон:
class DlgManager {
fun dismiss(dlg: Dialogue) {
// ...
dlg.onDismiss()
// ...
}
}
class Dialogue {
// Never call this directly, always go through DlgManager!
fun onDismiss() {
// ...
}
}
Я хотел бы сделать невозможным вызов onDismiss произвольными абонентами, за исключением случаев, когда вызывающий абонент DlgManager. Возможно ли это с аннотацией?
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class RestrictCallers(val caller: KClass<out Any>)
class DlgManager {
fun dismiss(dlg: Dialogue) {
// ...
dlg.onDismiss()
// ..
}
}
class Dialogue {
@RestrictCallers(DlgManager::class)
fun onDismiss() {
// ...
}
}
Как будет выглядеть такая реализация @RestrictCallers?
То есть это Проблема XY.





Чтобы сделать именно то, что вы хотите, потребуется выйти за рамки обычных языковых правил и написать плагин для компилятора.
Однако вы можете приблизиться к этому, используя аннотацию по выбору, если вы также не против аннотировать приемлемых вызывающих абонентов.
Для этого определите аннотацию согласия:
@RequiresOptIn(message = "Only to be called by annotated callers")
@Retention(AnnotationRetention.BINARY)
annotation class RestrictOnDismissCallers
Комментируйте onDismiss:
class Dialogue {
@RestrictOnDismissCallers
fun onDismiss() {
// ...
}
}
Тогда ошибка будет сгенерирована любой функцией, пытающейся вызвать onDismiss, за исключением тех, которые помечены аннотацией согласия, например:
@OptIn(RestrictOnDismissCallers::class)
fun dismiss(dlg: Dialogue)
Это сильно зависит от конкретного случая, но если какая-то операция считается внутренней, то в идеале мы вообще не должны ее раскрывать. Мы можем создать отдельные типы для пользователя API и для внутренней обработки, а затем при необходимости привести их:
class DlgManager {
fun dismiss(dlg: Dialogue) {
if (dlg !is DismissableDialogue) error("unknown dialogue type")
dlg.onDismiss()
// ...
}
fun getDialog(): Dialogue = DismissableDialogue()
}
interface Dialogue {
fun foo()
}
private class DismissableDialogue : Dialogue {
fun onDismiss() {}
...
}
Пользователь диалогового API даже не знает о типе DismissableDialogue и его функции onDismiss. Предполагая, что DlgManager — единственный способ создания/получения диалогов, мы даже можем убедиться, что все диалоги относятся к типу DismissableDialogue, поэтому dismiss не нужно проверять, а просто приводить к нему — тогда это решение на 100% надежно.
Вы также можете рассмотреть возможность не передавать диалог менеджеру для запуска его onDismiss, а на самом деле сделать наоборот: мы вызываем dialogue.dismiss(), а внутри он вызывает связанного менеджера. Мне кажется, ООП становится чище, и тогда ваша проблема перестает существовать. Но это зависит от того, можно ли считать диалог «управляемым объектом», чтобы он знал своего менеджера.
Я бы сказал, просто сдавайся. Просто напишите это в документации — когда что-то ломается из-за того, что кто-то вызвал это откуда-то еще, это они сами виноваты в несоблюдении правил. Или переосмыслите свой дизайн, чтобы вам вообще не нужно было этого делать. Если вы действительно хотите пойти по пути аннотаций, вам нужно написать плагин IntelliJ (или любую другую IDE, которую вы используете). Но опять же, это можно легко обойти, не устанавливая этот плагин.