我是否应该跳过常量而只在辅助方法中定义它:get_min_auth
?
是的,就是这样。
# typed: true
class AuthLevel < T::Struct
const :level, Symbol
end
class Guardian
def get_min_auth
AuthLevel.new(level: :STRONG)
end
def min_auth
get_min_auth || raise("Minimum auth must be specified")
end
end
→ 在 sorbet.run 上查看
要真正了解为什么在这里使用方法更好:您可以使用抽象方法来要求子类实现该方法。没有办法要求子类定义一个常量。这将使您的整个示例基本上消失,因为您不再需要raise
. 它会变成一个静态类型错误:
# typed: true
class AuthLevel < T::Struct
const :level, Symbol
end
class Guardian
extend T::Sig
extend T::Helpers
abstract!
sig {abstract.returns(AuthLevel)}
def min_auth
end
end
class MyGuardian < Guardian
end
→ 在 sorbet.run 上查看
我很想知道为什么这样的设计模式是(隐含的)不好的做法。
冰糕分阶段工作。首先,它了解代码库中的所有类/模块/常量。然后,它了解这些类/模块上的所有方法。然后它了解这些方法的类型。最后,它了解这些方法中局部变量的类型。
当 Sorbet 想要查看(...)::MIN_AUTH
一个常量是否实际存在时,它只知道到目前为止已经定义的常量,而不知道方法和局部变量。self
本质上是一个局部变量,并且.class
是一个可以在子类上被覆盖的方法。由于它既不知道局部变量也不知道方法,因此它报告一个动态常量引用。self.class
是“任意表达式”,而不是静态常量。
所以也许下一个问题是:为什么 Sorbet 会首先强加这种看似任意的解析常量的顺序?最大的两个原因:
速度。说“这是一个动态常量引用”并要求程序员重构代码比允许循环引用需要更少的分析。鉴于有一个相对容易的重构(你提到),这似乎是一个值得的权衡,以使每个后续类型检查运行得更快。
可读性。self.class::MIN_AUTH
本质上是通过 Ruby 的常量解析算法进行动态调度。事实上,常量解析在很多方面比方法解析更难理解,因为它受模块嵌套和继承层次结构的影响(而方法查找只受继承影响)。依靠复杂的查找和分派比仅仅使用人们更熟悉的方法(尤其是从其他语言转向 Ruby)更难阅读。