【问题标题】:Writing data classes with builder pattern使用构建器模式编写数据类
【发布时间】:2019-05-31 01:19:47
【问题描述】:

我有一个数据类,它使用构建器创建对象并将数据以序列化形式存储在缓冲区中。我计划更改课程以添加和删除一些字段。有些系统将使用该类的两个版本来创建数据,即具有所有字段的当前版本和具有删除/添加字段的较新版本。我想看看最好的方法是什么,以便向后兼容(不破坏任何消费者)?

我有几个关于如何做到这一点的建议,但我很难选择其中一个。

要求:
存储的数据必须是二进制的。
两个版本的序列化记录长度相同

现有代码

public class A implements Comparable<A>, Serializable {

private final Buffer buffer;
public static final Builder {
  private Header header//header with version
  private long creationTime;
  private SomeObject someObject;//this is removed in next version
  private OtherObject otherObject;//this is added in next version

  public Builder() { }

 //bunch of getters setters for fields

  public A build() {return new A(this);}

  private A(Builder b) {
   //build the object and put into the buffer
   validate()
  }
  private void validate() {//validates the object}

  public A(Buffer buf) {
   this.buffer=buf;
   validate();
  }
  public A(String encodedString) {
   this(ByteBuffer.wrap(encodedString));
  }
}
// consumers use this to get creationTime for object A
public long getCreationTime() {
 return buffer.getLong(OFFSET_CREATION_DATE);
}
}

解决方案 1: 在构建器中添加新字段并在标头中使用版本来决定在构建时(在构建方法中)使用哪些字段来创建对象。这种方法的问题是所有方法都将在编译时存在于消费者,除非他们测试他们的代码,否则每个对象都是有效的。因此,在构建时很难推断出哪个版本需要哪些字段。

解决方案 2: 在类中添加一个具有所需字段的新构建器。现有构建器中将存在重复的字段。 然后,消费者可以使用他们想要的构建器。这似乎更干净,因为构建器将完全独立。这种方法的问题在于,由于我们正在添加和删除字段,因此字段将处于不同的偏移量,因此 getter 必须更改以使用基于 versionType 的偏移量。这对于未来的版本也是有问题的,因为那时我们将拥有这个巨大的类,每个版本都有很多构建器和 getter 中的逻辑

解决方案 3: 创建一个扩展 A 并拥有自己的构建器的新类(假设是 B)。这样代码就更加模块化了。问题是现在需要在某个地方有一些逻辑来区分并知道要调用哪个构造函数。例如,如果消费者通过 base64 来获取对象 A,则需要确定它是哪个版本。

String encodedString = "someString form of A"
A a = new A(encodedString);

是否有推荐的方法来使用构建器模式对这些数据类进行编码,以使其既未来兼容又向后兼容。

【问题讨论】:

    标签: java design-patterns serialization builder-pattern


    【解决方案1】:

    方法 2 结合 方法 1 + 正确的二进制表示 就是答案。为您的二进制表示选择正确的格式,最简单的方法是选择 json 。为 V1 和 V2 对象创建具体的构建器,并使用字节缓冲区来构建它们。每个 builder/Factory 只对它识别的字段感兴趣。如果生成器/工厂尝试反序列化错误的版本异常可能会抛出,您可以考虑使用版本字段。具体的构建器/工厂将只构建它识别的版本的对象。

    在我看来,子类化是不必要的。您可以将 Builder/factory 类与对象类分开。以 Hazelcast 中的“StreamSerializer”为例,它完全是实体的外部类,仅专用于进行编组。

    使用正确的格式将解决方法二中的偏移问题。如果您必须以二进制形式保存它,那么一种解决方法是使用平面格式,其中记录大小大于 nessesary,并且您已为更改保留了可用空间。在过去的 Cobol 时代,他们就是这样做的。我不建议你这样做。使用 json :) 它最简单可能不是最有效的。您也可以查看https://developers.google.com/protocol-buffers/ 协议缓冲区。

    根据您在解组时为序列化选择的布局,您可以配置尝试反序列化流部分的责任链。当您弃用某个版本时,编组器只会从链中删除/停用。

    【讨论】:

    • 我忘记提及的一个细节是,对于某些奇怪的区域(为了向后兼容),两个版本的记录大小(二进制)必须相同。这就是使解决方法二的问题变得有点困难的原因。在使用 json 时,这会有点困难,因为由于内存原因(当访问与 GC 相关的不同字段和事物时)选择了二进制形式。我确实喜欢方法 2 而不是 1,我将研究布局选项,看看我们是否可以以某种方式配置责任链。
    • @jigsaw 垃圾收集和内存的争论在 99% 的应用程序中并不严重。我严重怀疑你正在做下一个实时交易平台,这将是一个争论。
    • 无论如何,如果您要使用二进制文件。您无需阅读完整记录即可确定版本。将记录分为两部分,第一部分是包含元数据的标头,第二部分是动态的,具体取决于对象的类型。我认为你没有给出所有的要求,所以我很难说更多。但是你不应该使用自定义二进制格式,而是使用 protobufferer。
    • 是的,我可以先读取标题以找出对象的版本,但这并不会改变 getter 必须通过更改偏移量来根据版本返回字段的事实基于版本,我试图避免这种情况。这是有道理的还是我在你说的话中遗漏了什么?例如:public getCreationTime() { if (version == 0) { return buffer.get(offset1); } else { return buffer.get(offset2); } }
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-12-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-08-11
    • 2013-11-16
    • 2012-02-12
    相关资源
    最近更新 更多