私は定期的に次の問題に直面しているのを目にします。ある種のマーカーインターフェイス(簡単にするために使用しましょうjava.io.Serializable
)といくつかのラッパー(アダプター、デコレーター、プロキシなど)があります。ただし、シリアライズ可能なインスタンスを別のインスタンス(シリアライズ可能ではない)でラップすると、機能が失われます。List実装で実装できるjava.util.RandomAccessでも同じ問題が発生します。それを処理するための優れたOOP方法はありますか?
4 に答える
これがGuavaメーリングリストに関する最近の議論です-私の答えはこれ、かなり根本的な問題に触れています。
http://groups.google.com/group/guava-discuss/browse_thread/thread/2d422600e7f87367/1e6c6a7b41c87aac
その要点は次のとおり です。オブジェクトがラップされることを期待する場合は、マーカーインターフェイスを使用しないでください。(まあ、それはかなり一般的です-オブジェクトがクライアントによってラップされないことをどうやって知るのですか?)
たとえば、ArrayList
。RandomAccess
明らかに、それは実装します。次に、オブジェクトのラッパーを作成することにしList
ます。おっと!ここで、ラップするときに、ラップされたオブジェクトを確認する必要があります。それがRandomAccessの場合、作成するラッパーもRandomAccessを実装する必要があります。
これは「問題なく」機能します...マーカーインターフェイスが1つしかない場合!しかし、ラップされたオブジェクトをシリアル化できる場合はどうなるでしょうか。たとえば、「不変」である場合はどうなりますか(それを示すタイプがあると仮定します)。または同期?(同じ仮定で)。
メーリングリストへの回答でも指摘しているように、この設計上の欠陥は古き良きjava.io
パッケージにも現れています。を受け入れるメソッドがあるとしInputStream
ます。そこから直接読みますか?それが高価なストリームであり、誰もあなたのBufferedInputStream
ためにそれを包むことを気にしない場合はどうなりますか?ああ、それは簡単です!チェックするだけstream instanceof BufferedInputStream
で、チェックしない場合は自分でラップします。しかし、違います。ストリームにはチェーンのどこかにバッファリングがある可能性がありますが、BufferedInputStreamのインスタンスではないラッパーを取得する可能性があります。したがって、「このストリームがバッファリングされている」という情報は失われます(そして、おそらく、メモリを再度バッファリングするには、悲観的にメモリを浪費する必要があります)。
適切に処理したい場合は、機能をオブジェクトとしてモデル化するだけです。検討:
interface YourType {
Set<Capability> myCapabilities();
}
enum Capability {
SERIALIAZABLE,
SYNCHRONOUS,
IMMUTABLE,
BUFFERED //whatever - hey, this is just an example,
//don't throw everything in of course!
}
編集:便宜上、列挙型を使用していることに注意してください。インターフェイスCapability
とそれを実装するオブジェクトのオープンエンドセット(おそらく複数の列挙型)によって存在する可能性があります。
したがって、これらのオブジェクトをラップすると、一連の機能が得られ、保持する機能、削除する機能、追加する機能を簡単に決定できます。
これには明らかに欠点があるため、マーカーインターフェイスとして表現された機能を隠すラッパーの苦痛を実際に感じる場合にのみ使用してください。たとえば、リストを取得するコードを記述したが、それはRandomAccessANDSerializableである必要があるとします。通常のアプローチでは、これは簡単に表現できます。
<T extends List<Integer> & RandomAccess & Serializable> void method(T list) { ... }
しかし、私が説明するアプローチでは、あなたができることは次のとおりです。
void method(YourType object) {
Preconditions.checkArgument(object.getCapabilities().contains(SERIALIZABLE));
Preconditions.checkArgument(object.getCapabilities().contains(RANDOM_ACCESS));
...
}
どちらよりも満足のいくアプローチがあったらいいのにと思いますが、見通しからすると(少なくとも組み合わせ型の爆発を起こさずに)実行できないようです。
編集:もう1つの欠点は、機能ごとに明示的な型がないと、この機能が提供するものを表現するメソッドを配置する自然な場所がないことです。マーカーインターフェイス、つまり追加のメソッドでは表現されない機能について説明しているため、この説明ではこれはそれほど重要ではありませんが、完全を期すために言及します。
PS:ちなみに、Guavaのコレクションコードをざっと見てみると、この問題が引き起こしている痛みを実感できます。はい、一部の優れた人々はそれを素晴らしい抽象化の背後に隠そうとしていますが、それでも根本的な問題は苦痛です。
関心のあるインターフェイスがすべてマーカーインターフェイスである場合は、すべてのラッパークラスにインターフェイスを実装させることができます
public interface Wrapper {
boolean isWrapperFor(Class<?> iface);
}
その実装は次のようになります。
public boolean isWrapperFor(Class<?> cls) {
if (wrappedObj instanceof Wrapper) {
return ((Wrapper)wrappedObj).isWrapperFor(cls);
}
return cls.isInstance(wrappedObj);
}
これはで行われる方法java.sql.Wrapper
です。インターフェイスが単なるマーカーではなく、実際にいくつかの機能を備えている場合は、アンラップするメソッドを追加できます。
<T> T unwrap(java.lang.Class<T> cls)
のようなもののためRandomAccess
にあなたができることはあまりありません。もちろん、instanceof
チェックを行って、関連するクラスのインスタンスを作成することもできます。クラスの数はマーカーとともに指数関数的に増加し(使用することもできますがjava.lang.reflect.Proxy
)、作成メソッドはこれまでにすべてのマーカーについて知る必要があります。
Serializable
それほど悪くはありません。間接クラスが実装されSerializable
ている場合、ターゲットクラスが実装されている場合は全体がシリアル化可能でありSerializable
、そうでない場合はシリアル化できません。
いくつかの選択肢がありますが、どれも非常に良いものではありません
ラップされたオブジェクトもインターフェイスを実装するかどうかがコンパイル時にわかっている場合は、ラッパーにインターフェイスを実装させます。ラップされたオブジェクトがインターフェイスを実装するかどうかが実行時までわからない場合は、ファクトリメソッドを使用してラッパーを作成できます。これは、実装されたインターフェースの可能な組み合わせに対して個別のラッパークラスがあることを意味します。(1つのインターフェースでは、2つのラッパーが必要です。1つはあり、もう1つはありません。2つのインターフェースの場合、4つのラッパーなど。)
ラップされたオブジェクトをラッパーから公開して、クライアントがチェーンをウォークし、を使用してインターフェイスのチェーン内の各オブジェクトをテストできるようにします
instanceof
。これはカプセル化を破ります。ラッパーとラップされたオブジェクトの両方によって実装された、インターフェイスを取得するための専用メソッドがあります。例
asSomeInterface()
:ラッパーは、ラップされたオブジェクトに委任するか、カプセル化を保持するためにラップされたオブジェクトの周りにプロキシを作成します。インターフェイスごとに1つのラッパークラスを作成します。ラッパーは通常どおりに実装されます。ラッパーはインターフェイスを実装し、そのインターフェイスの別の実装に委任します。ラップされたオブジェクトは複数のインターフェイスを実装する場合があるため、動的プロキシを使用してプロキシによって実装されたインターフェイスメソッドを適切なラッパーインスタンスに委任することにより、複数のラッパーインスタンスが1つの論理インスタンスに結合されます。プロキシによって実装されるインターフェイスのセットには、共通のメソッドシグネチャがない必要があります。
Microsoftは、集約(Wikipedia)をコンポーネントオブジェクトモデル(COM)に焼き付けました。すべてのオブジェクトが順守しなければならないルールがあるため、大多数の人は使用していないように見えますが、COMオブジェクトの実装者にとってはかなり複雑になります。ラップされたオブジェクトは、ラップされたオブジェクトがラッパーであることを認識し、ラッパーへのポインターを維持する必要があることによってカプセル化されます。これはinstanceof
、公開されたパブリックインターフェイスのQueryInterfaceを(大まかに)実装するときに使用されます。ラップされたオブジェクトは、ラッパーに実装されたインターフェイスを返します。それ自身の実装よりも。
私はこれに対するクリーンで理解しやすい/実装され、正しくカプセル化されたソリューションを見たことがありません。COMアグリゲーションは機能し、完全なカプセル化を提供しますが、アグリゲートで使用されない場合でも、実装するすべてのオブジェクトに対して支払うコストです。