我有许多愚蠢的对象类,我想将其作为字符串序列化,以用于进程外存储。这是一个非常典型的使用双重调度/访问者模式的地方。
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-table查找的大部分内容。编译器仅将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-table查找现在在Serializable
的实例上完成,它会找到,例如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);
}
}
我认为发生的是,在编译时,编译器正试图从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>
接口将为项目A中的数据类提供更具体的serialize
方法;避免无限循环。
这使得双重派遣的吸引力略微降低。现在数据类中有更多的样板代码。很糟糕的是,显然重复的代码不能被分解为父类,因为它绕过了双重调度技巧。现在,它有更多,它与Serializer的继承深度相结合。
Double-dispatch是静态输入技巧。是否有一些静态类型技巧可以帮助我避免重复的代码?
答案 0 :(得分:0)