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 は 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パラメータが抽象型であることのみを認識しますSerializerserializer実行時に、オブジェクトの 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 テーブル ルックアップを実行するためにランタイムに到達するまでに、探しているメソッドが間違っていて、ランタイムが を選択し、無限ループにつながることを意味します。SerializerSerializer<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無限ループを回避。

これにより、二重発送の魅力がわずかに低下します。データ クラスの定型コードが増えました。二重ディスパッチのトリックを回避したため、明らかに重複したコードを親クラスに分解できないほど悪いことでした。現在、それはさらに多く、シリアライザーの継承の深さと相まって複雑になっています。

二重ディスパッチは、静的型付けの策略です。重複したコードを回避するのに役立つ静的型付けのトリックはありますか?

4

1 に答える 1

0

serializeあなたがその方法に気づいたように

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

意味を成さない。ビジターパターンはケース分析を行うためにありますが、この方法では進歩がありません (それが であることはすでにわかっていますSerializable)。そのため、必然的に無限再帰が発生します。

意味のあるのは、少なくとも 1 つの具象型を参照するベースSerializerインターフェイスと、その具象型を 2 つのプロジェクト間で共有することです。共有された具象型がない場合、Serializer階層が有用であるという望みはありません。

ビジター パターンを実装するときにボイラープレートを減らしたい場合は、コード ジェネレーターを (注釈処理を介して) 使用することをお勧めします。adt4jまたはserve4j

于 2016-02-10T12:52:00.850 に答える