首页 > 解决方案 > 在单例方法上使用 sorbet 接口抽象

问题描述

我喜欢冰糕的界面功能!

在 sorbet 文档中有一段使单例方法抽象。这似乎是反序列化和迁移(向上转换)的一个很棒的功能。

我的想法是将类型化结构的序列化版本存储在数据库中。因为结构可能会随着时间而发展,所以我还想提供一些功能来将结构的旧序列化版本转换为当前版本。

实现这一点的方法是将类的名称、数据和版本保存到数据库中。假设这个结构

class MyStruct < T::Struct
  const :v1_field, String
  const :v2_field, String

  def self.version
    2
  end
end

数据存储中的旧序列化版本可能如下所示:

班级 数据 版本
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)`

即使方法是在类级别上定义的。我不包括实例级别的方法的主要原因是因为我无法在没有正确数据的情况下实例化结构。

标签: rubyinterfacesorbet

解决方案


我认为您不想extend VersionedStruct尝试在类方法中使用魔术混合技巧。效果很好

# 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

推荐阅读