3

我有许多愚蠢的对象类,我想将它们序列化为字符串,以用于进程外存储。这是一个非常典型的使用双重调度/访问者模式的地方。

public interface Serializeable {
  <T> T serialize(Serializer<T> serializer);
}

public interface Serializer<T> {
  T serialize(Serializeable s);
  T serialize(FileSystemIdentifier fsid);
  T serialize(ExtFileSystemIdentifier extFsid);
  T serialize(NtfsFileSystemIdentifier ntfsFsid);
}

public class JsonSerializer implements Serializer<String> {
  public String serialize(Serializeable s) {...}
  public String serialize(FileSystemIdentifier fsid) {...}
  public String serialize(ExtFileSystemIdentifer extFsid) {...}
  public String serialize(NtfsFileSystemIdentifier ntfsFsid) {...}
}

public abstract class FileSystemIdentifier implements Serializeable {}
public class ExtFileSystemIdentifier extends FileSystemIdentifier {...}
public class NtfsFileSystemIdentifier extends FileSystemIdentifier {...}

使用此模型,保存数据的类不需要知道序列化该数据的可能方法。JSON 是一种选择,但另一个序列化程序可能会将数据类“序列化”为 SQL 插入语句。

如果我们看一下其中一个数据类的实现,它的实现看起来与所有其他类几乎相同。类调用传递给它的serialize()方法,提供自己作为参数。Serializer

public class ExtFileSystemIdentifier extends FileSystemIdentifier {
  public <T> T serialize(Serializer<T> serializer) {
    return serializer.serialize(this);
  }
}

我理解为什么不能将这个通用代码拉入父类。尽管代码是共享的,但编译器清楚地知道在该方法中的类型thisExtFileSystemIdentifier并且可以(在编译时)写出字节码以调用serialize().

我相信我也了解 V 表查找时发生的大部分情况。编译器只知道serializer参数是抽象类型Serializer。它必须在运行时查看serializer对象的 V 表以发现serialize()特定子类的方法的位置,在这种情况下JsonSerializer.serialize()

典型的用法是获取一个已知为的数据对象,Serializable并通过将其提供给一个已知为Serializer. 对象的具体类型在编译时是未知的。

List<Serializeable> list = //....
Serializer<String> serializer = //....

list.stream().map(serializer::serialize)

此实例的工作方式与其他调用类似,但相反。

public class JsonSerializer implements Serializer<String> {
  public String serialize(Serializeable s) {
    s.serialize(this);
  }
  // ...
}  

V 表查找现在在 的实例上完成,Serializable它将找到,例如ExtFileSystemIdentifier.serialize. 它可以静态地确定最接近的匹配重载是 for Serializer<T>(它恰好也是唯一的重载)。

这一切都很好。它实现了保持输入和输出数据类忽略序列化类的主要目标。它还实现了第二个目标,即为序列化类的用户提供一致的 API,而不管正在执行哪种序列化。

现在想象一下,另一个项目中存在第二组哑数据类。需要为这些对象编写一个新的序列化程序。现有Serializable接口可以在这个新项目中使用。Serializer但是,该接口包含对来自其他项目的数据类的引用。

为了概括这一点,Serializer接口可以分为三个

public interface Serializer<T> {
  T serialize(Serializable s);
}

public interface ProjectASerializer<T> extends Serializer<T> {
  T serialize(FileSystemIdentifier fsid);
  T serialize(ExtFileSystemIdentifier fsid);
  // ... other data classes from Project A
}

public interface ProjectBSerializer<T> extends Serializer<T> {
  T serialize(ComputingDevice device);
  T serialize(PortableComputingDevice portable);
  // ... other data classes from Project B
}

这样,SerializerSerializable接口可以被打包和重用。但是,这会破坏双重调度,并导致代码中的无限循环。这是我在 V 表查找中不确定的部分。

在调试器中单步执行代码时,在数据类的serialize方法中会出现问题。

public class ExtFileSystemIdentifier implements Serializable {
  public <T> T serialize(Serializer<T> serializer) {
    return serializer.serialize(this);
  }
}

我认为正在发生的是,在编译时,编译器正试图serialize从接口中的可用选项中为方法选择正确的重载Serializer(因为编译器只知道它是 a Serializer<T>)。这意味着当我们到达运行时进行 V 表查找时,正在寻找的方法是错误的,运行时将选择JsonSerializer.serialize(Serializable),导致无限循环。

这个问题的一个可能的解决方案是serialize在数据类中提供一个更特定于类型的方法。

public interface ProjectASerializable extends Serializable {
  <T> T serialize(ProjectASerializer<T> serializer);
}

public class ExtFileSystemIdentifier implements ProjectASerializable {
  public <T> T serialize(Serializer<T> serializer) {
    return serializer.serialize(this);
  }
  public <T> T serialize(ProjectASerializer<T> serializer) {
    return serializer.serialize(this);
  }
}

程序控制流将反弹,直到达到最特定类型的Serializer重载。届时,该ProjectASerializer<T>接口将为来自项目 A 的数据类提供更具体serialize的方法;避免无限循环。

这使得双重调度稍微不那么有吸引力。数据类中现在有更多样板代码。显然重复的代码不能被分解到父类中,这已经够糟糕的了,因为它规避了双重分派的诡计。现在,它更多了,而且它与序列化器的继承深度相结合。

双重调度是静态类型的诡计。是否有更多静态类型技巧可以帮助我避免重复代码?

4

1 回答 1

0

正如你注意到的serialize方法

public interface Serializer<T> {
  T serialize(Serializable s);
}

没有意义。访问者模式用于进行案例分析,但使用这种方法你没有任何进展(你已经知道它是 a Serializable),因此不可避免的无限递归。

有意义的是一个基本Serializer接口,它至少有一个具体类型可供访问,并且该具体类型在两个项目之间共享。如果没有共享的具体类型,那么Serializer层次结构就没有用处的希望。

现在,如果您希望在实现访问者模式时减少样板文件,我建议使用代码生成器(通过注释处理),例如。adt4j派生4j

于 2016-02-10T12:52:00.850 回答