在单例方法上使用 sorbet 接口抽象
Using sorbet interface abstraction on singleton methods
我喜欢冰糕界面功能!
并且在 sorbet 文档中有一段制作 singleton methods abstract。这似乎是反序列化和迁移(向上转换)的一个很好的特性。
我的想法是将 Typed Struct 的序列化版本存储在数据库中。因为该结构可能会随着时间的推移而发展,所以我还想提供一些功能来将该结构的旧序列化版本转换为当前版本。
实现此目的的方法是将 class 的名称、数据和版本保存到数据库中。假设这个结构
class MyStruct < T::Struct
const :v1_field, String
const :v2_field, String
def self.version
2
end
end
数据存储中的旧序列化版本可能如下所示:
class
data
version
MyStruct
{"v1_field": "v1 value"}
1
我无法反序列化数据,因为它缺少必填字段 v2_field
。所以我的想法是为迁移提供单例方法。
module VersionedStruct
module ClassMethods
abstract!
sig { abstract.returns(Integer) }
def version; end
sig { abstract.params(payload: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def migrate(payload); end
end
mixes_in_class_methods(ClassMethods)
end
class MyStruct < T::Struct
include VersionedStruct
const :v1_field, String
const :v2_field, String
sig { override.returns(Integer) }
def self.version
2
end
sig { override.params(payload: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def self.migrate(data)
return if data[:v2_field]
data.merge(v2_field: "default value")
end
end
注意:我知道结构字段有一个默认选项,但有些迁移无法用它建模(比如重命名字段名称)。不幸的是,这些单例方法接口的行为方式与我期望接口的工作方式不同:
class DataDeserializer
sig { params(data_class: String, data_version: Integer, data: T::Hash[Symbol, T.untyped]).returns(T.any(MyStruct, MyOtherStruct, ...)) }
def load(data_class, data_version, data)
struct_class = Object.const_get(data_class)
migrated_data = if struct_class.include?(VersionedStruct) # This seems to be the only check that actually returns true for all classes that include the interface
migrate(data_version, T.cast(struct_class, VersionedStruct), data)
else
data # fallback if the persistent data model never changed
end
struct_class.new(migrated_data)
end
private
sig { params(data_version: Integer, struct: VersionedStruct, data: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def migrate(data_version, struct, data)
return data if data_version == struct.version # serialized data is up to date
struct.migrate(data)
end
end
此代码(或此代码的变体)无效,因为 sorbet 会引发错误:
Method `version` does not exist on `VersionedStruct`
Method `migrate` does not exist on `VersionedStruct`
将签名更改为 T.class_of(VersionedStruct)
将引发相同的错误:
Method `version` does not exist on `T.class_of(VersionedStruct)`
Method `migrate` does not exist on `T.class_of(VersionedStruct)`
即使这些方法是在 class 级别上定义的。我不在实例级别包括方法的主要原因是因为我无法在没有正确数据的情况下实例化结构。
我认为您想 extend VersionedStruct
而不是尝试在 class 方法中进行魔术混合。 That works really well:
# typed: true
module VersionedStruct
extend T::Sig
extend T::Helpers
abstract!
sig { abstract.returns(Integer) }
def version; end
sig { abstract.params(payload: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def migrate(payload); end
end
class MyStruct < T::Struct
extend T::Sig
extend VersionedStruct
const :v1_field, String
const :v2_field, String
sig { override.returns(Integer) }
def self.version
2
end
sig { override.params(data: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def self.migrate(data)
return {} if data[:v2_field]
data.merge(v2_field: "default value")
end
end
我喜欢冰糕界面功能!
并且在 sorbet 文档中有一段制作 singleton methods abstract。这似乎是反序列化和迁移(向上转换)的一个很好的特性。
我的想法是将 Typed Struct 的序列化版本存储在数据库中。因为该结构可能会随着时间的推移而发展,所以我还想提供一些功能来将该结构的旧序列化版本转换为当前版本。
实现此目的的方法是将 class 的名称、数据和版本保存到数据库中。假设这个结构
class MyStruct < T::Struct
const :v1_field, String
const :v2_field, String
def self.version
2
end
end
数据存储中的旧序列化版本可能如下所示:
class | data | version |
---|---|---|
MyStruct |
{"v1_field": "v1 value"} |
1 |
我无法反序列化数据,因为它缺少必填字段 v2_field
。所以我的想法是为迁移提供单例方法。
module VersionedStruct
module ClassMethods
abstract!
sig { abstract.returns(Integer) }
def version; end
sig { abstract.params(payload: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def migrate(payload); end
end
mixes_in_class_methods(ClassMethods)
end
class MyStruct < T::Struct
include VersionedStruct
const :v1_field, String
const :v2_field, String
sig { override.returns(Integer) }
def self.version
2
end
sig { override.params(payload: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def self.migrate(data)
return if data[:v2_field]
data.merge(v2_field: "default value")
end
end
注意:我知道结构字段有一个默认选项,但有些迁移无法用它建模(比如重命名字段名称)。不幸的是,这些单例方法接口的行为方式与我期望接口的工作方式不同:
class DataDeserializer
sig { params(data_class: String, data_version: Integer, data: T::Hash[Symbol, T.untyped]).returns(T.any(MyStruct, MyOtherStruct, ...)) }
def load(data_class, data_version, data)
struct_class = Object.const_get(data_class)
migrated_data = if struct_class.include?(VersionedStruct) # This seems to be the only check that actually returns true for all classes that include the interface
migrate(data_version, T.cast(struct_class, VersionedStruct), data)
else
data # fallback if the persistent data model never changed
end
struct_class.new(migrated_data)
end
private
sig { params(data_version: Integer, struct: VersionedStruct, data: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def migrate(data_version, struct, data)
return data if data_version == struct.version # serialized data is up to date
struct.migrate(data)
end
end
此代码(或此代码的变体)无效,因为 sorbet 会引发错误:
Method `version` does not exist on `VersionedStruct`
Method `migrate` does not exist on `VersionedStruct`
将签名更改为 T.class_of(VersionedStruct)
将引发相同的错误:
Method `version` does not exist on `T.class_of(VersionedStruct)`
Method `migrate` does not exist on `T.class_of(VersionedStruct)`
即使这些方法是在 class 级别上定义的。我不在实例级别包括方法的主要原因是因为我无法在没有正确数据的情况下实例化结构。
我认为您想 extend VersionedStruct
而不是尝试在 class 方法中进行魔术混合。 That works really well:
# typed: true
module VersionedStruct
extend T::Sig
extend T::Helpers
abstract!
sig { abstract.returns(Integer) }
def version; end
sig { abstract.params(payload: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def migrate(payload); end
end
class MyStruct < T::Struct
extend T::Sig
extend VersionedStruct
const :v1_field, String
const :v2_field, String
sig { override.returns(Integer) }
def self.version
2
end
sig { override.params(data: T::Hash[Symbol, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def self.migrate(data)
return {} if data[:v2_field]
data.merge(v2_field: "default value")
end
end