アウト プロセス ストレージの目的で文字列としてシリアル化したいダム オブジェクト クラスがいくつかあります。これは、ダブル ディスパッチ / ビジター パターンを使用するかなり典型的な場所です。
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 は 1 つのオプションですが、別のシリアライザーは、たとえば、データ クラスを SQL 挿入ステートメントに "シリアル化" する場合があります。
データ クラスの 1 つの実装を見ると、実装は他のすべてのものとほとんど同じに見えます。クラスは、渡された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);
}
// ...
}
のインスタンスで V テーブル ルックアップが実行され、たとえばSerializable
が検出されますExtFileSystemIdentifier.serialize
。最も近い一致するオーバーロードが for であることを静的に判断できますSerializer<T>
(たまたま唯一のオーバーロードでもあります)。
これはすべて順調です。これにより、入力および出力データ クラスがシリアライゼーション クラスに気付かないようにするという主な目標が達成されます。また、シリアライゼーションの種類に関係なく、シリアライゼーション クラスのユーザーに一貫した API を提供するという第 2 の目標も達成します。
別のプロジェクトにダム データ クラスの 2 番目のセットが存在するとします。これらのオブジェクト用に新しいシリアライザーを作成する必要があります。この新しいプロジェクトでは、既存のSerializable
インターフェイスを使用できます。ただし、Serializer
インターフェイスには、他のプロジェクトからのデータ クラスへの参照が含まれています。
これを一般化するために、Serializer
インターフェイスを 3 つに分割できます。
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
で使用可能なオプションからメソッドの正しいオーバーロードを選択しようとしているということです (コンパイラーはそれを としてのみ認識しているため)。これは、V テーブル ルックアップを実行するためにランタイムに到達するまでに、探しているメソッドが間違っていて、ランタイムが を選択し、無限ループにつながることを意味します。Serializer
Serializer<T>
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
無限ループを回避。
これにより、二重発送の魅力がわずかに低下します。データ クラスの定型コードが増えました。二重ディスパッチのトリックを回避したため、明らかに重複したコードを親クラスに分解できないほど悪いことでした。現在、それはさらに多く、シリアライザーの継承の深さと相まって複雑になっています。
二重ディスパッチは、静的型付けの策略です。重複したコードを回避するのに役立つ静的型付けのトリックはありますか?