Ограничить вызовы метода классом

В Котлине я часто встречаю следующий шаблон:

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?

Я бы сказал, просто сдавайся. Просто напишите это в документации — когда что-то ломается из-за того, что кто-то вызвал это откуда-то еще, это они сами виноваты в несоблюдении правил. Или переосмыслите свой дизайн, чтобы вам вообще не нужно было этого делать. Если вы действительно хотите пойти по пути аннотаций, вам нужно написать плагин IntelliJ (или любую другую IDE, которую вы используете). Но опять же, это можно легко обойти, не устанавливая этот плагин.

Sweeper 23.02.2024 10:46

То есть это Проблема XY.

Sweeper 23.02.2024 10:49
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
57
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

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

Включение аннотаций

Однако вы можете приблизиться к этому, используя аннотацию по выбору, если вы также не против аннотировать приемлемых вызывающих абонентов.

Для этого определите аннотацию согласия:

@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(), а внутри он вызывает связанного менеджера. Мне кажется, ООП становится чище, и тогда ваша проблема перестает существовать. Но это зависит от того, можно ли считать диалог «управляемым объектом», чтобы он знал своего менеджера.

Другие вопросы по теме