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

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);
  }
}


我了解为什么不能将此通用代码放入父类。尽管代码是共享的,但是编译器明确地知道在该方法中this的类型为ExtFileSystemIdentifier,并且可以(在编译时)写出字节码以调用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);
  }
  // ...
}


现在,在Serializable的实例上进行V表查找,并且它将找到例如ExtFileSystemIdentifier.serialize。它可以静态确定最接近的匹配重载是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方法选择正确的重载(因为编译器仅将其视为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>接口将为Project A中的数据类提供更具体的serialize方法;避免无限循环。

这使得双调度的吸引力略微降低。现在,数据类中有更多样板代码。这很糟糕,显然不能将重复的代码分解为父类,因为它避免了两次分派的麻烦。现在,它的功能更多了,它与Serializer继承的深度融为一体。

双重派遣是静态的打字欺骗。还有更多静态键入技巧可以帮助我避免重复的代码吗?

最佳答案

正如您注意到的serialize方法

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


没有道理。可以使用访问者模式进行案例分析,但是使用此方法,您不会取得任何进展(您已经知道它是Serializable),因此不可避免地要进行无限递归。

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

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

关于java - 双重 dispatch 和继承,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/34571009/

10-10 10:53