我有许多哑巴对象类,我想将它们序列化为String,用于进程外存储。这是一个非常典型的使用双重调度/访问者模式的地方。
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 insert语句。
如果我们看一下其中一个数据类的实现,实现看起来与所有其他类几乎相同。该类在传递给它的Serializer
上调用serialize()
方法,并将其自身作为参数提供。
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
}
通过这种方式,可以对Serializer
和Serializable
接口进行打包和重用。然而,这打破了双重分派,并导致代码中的无限循环。这是我在V表查找中不确定的部分。
在调试器中单步执行代码时,在数据类的serialize
方法中出现问题。
public class ExtFileSystemIdentifier implements Serializable {
public <T> T serialize(Serializer<T> serializer) {
return serializer.serialize(this);
}
}
我认为发生的情况是在编译时,编译器试图从Serializer
接口的可用选项中为serialize
方法选择正确的重载(因为编译器只知道它是一个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
重载。此时,对于来自项目A的数据类,ProjectASerializer<T>
接口将具有更具体的serialize
方法;从而避免了无限循环。
这使得双重分派的吸引力略有下降。现在数据类中有了更多的样板代码。这已经够糟糕的了,显然重复的代码不能被分解到父类中,因为它绕过了双重调度的把戏。现在,它更多了,并且它与序列化程序的继承深度相结合。
双重分派是静态类型技巧。有没有更多的静态类型技巧可以帮助我避免重复的代码?
发布于 2016-02-10 12:52:01
正如您所注意到的,serialize
方法
public interface Serializer<T> {
T serialize(Serializable s);
}
没有任何意义。访问者模式是用来进行案例分析的,但是使用这种方法没有任何进展(您已经知道它是一个Serializable
),因此不可避免地要进行无限递归。
有意义的是一个至少有一个具体类型要访问的基本Serializer
接口,以及在两个项目之间共享的具体类型。如果没有共享的具体类型,那么就没有希望使用Serializer
层次结构。
https://stackoverflow.com/questions/34571009
复制相似问题