Is there a workaround for Composition and Marker Interfaces?
I see myself regularly confronted with the following problem. I have some kind of Marker Interface (for simplicity let's use java.io.Serializable
) and several wrappers (Adapter, Decorator, Pr开发者_JAVA技巧oxy, ...). But when you wrap a Serializable instance in another instance (which is not serializable) you loose functionality. The same problem occurs with java.util.RandomAccess which can be implemented by List implementations. Is there a nice OOP way to handle it?
Here is a recent discussion on Guava mailing list - my answer touches upon this, rather fundamental issue.
http://groups.google.com/group/guava-discuss/browse_thread/thread/2d422600e7f87367/1e6c6a7b41c87aac
The gist of it is this: Don't use marker interfaces when you expect your objects to be wrapped. (Well, that's pretty general - how do you know that your object isn't going to be wrapped by a client?)
For example, an ArrayList
. It implements RandomAccess
, obviously. Then you decide to create a wrapper for List
objects. Oops! Now when you wrap, you have to check the wrapped object, and if it is RandomAccess, the wrapper you create should also implement RandomAccess!
This works "fine"...if you only have a single marker interface! But what if the wrapped object can be Serializable? What if it is, say, "Immutable" (assuming you have a type to denote that)? Or synchronous? (With the same assumption).
As I also note in my answer to the mailing list, this design deficiency also manifest itself in the good old java.io
package. Say you have a method accepting an InputStream
. Will you read directly from it? What if it is a costly stream, and nobody cared to wrap it in a BufferedInputStream
for you? Oh, that's easy! You just check stream instanceof BufferedInputStream
, and if not, you wrap it yourself! But no. The stream might have buffering somewhere down the chain, but you may get a wrapper of it, that is not an instance of BufferedInputStream. Thus, the information that "this stream is buffered" is lost (and you have to pessimistically waste memory to buffer it again, perhaps).
If you want to do things properly, just model the capabilities as objects. Consider:
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!
}
Edit: It should be noted that I use an enum just for convenience. There could by an interface Capability
and an open-ended set of objects implementing it (perhaps multiple enums).
So when you wrap an object of these, you get a Set of capabilities, and you can easily decide which capabilities to retain, which to remove, which to add.
This does, obviously, have its shortcomings, so it is to be used only in cases where you really feel the pain of wrappers hiding capabilities expressed as marker interfaces. For example, say you write a piece of code that takes a List, but it has to be RandomAccess AND Serializable. With the usual approach, this is easy to express:
<T extends List<Integer> & RandomAccess & Serializable> void method(T list) { ... }
But in the approach I describe, all you can do is:
void method(YourType object) {
Preconditions.checkArgument(object.getCapabilities().contains(SERIALIZABLE));
Preconditions.checkArgument(object.getCapabilities().contains(RANDOM_ACCESS));
...
}
I really wish there were a more satisfying approach than either, but from the outlook, it seems not doable (without, at least, causing a combinatorial type explosion).
Edit: Another shortcoming is that, without an explicit type per capability, we don't have the natural place to put methods that express what this capability offers. This is not too important in this discussion since we talk about marker interfaces, i.e. capabilities that are not expressed through additional methods, but I mention it for completeness.
PS: by the way, if you skim through Guava's collections code, you can really feel the pain that this problem is causing. Yes, some good people are trying to hide it behind nice abstractions, but the underlying issue is painful nonetheless.
If the interfaces you're interested in are all marker interfaces, you could have all your wrapper classes implement an interface
public interface Wrapper {
boolean isWrapperFor(Class<?> iface);
}
whose implementation would look like this:
public boolean isWrapperFor(Class<?> cls) {
if (wrappedObj instanceof Wrapper) {
return ((Wrapper)wrappedObj).isWrapperFor(cls);
}
return cls.isInstance(wrappedObj);
}
This is how it's done in java.sql.Wrapper
. If the interface is not just a marker, but actually has some functionality, you can add a method to unwrap:
<T> T unwrap(java.lang.Class<T> cls)
For the likes of RandomAccess
there is not much you can do. You can, of course, do an instanceof
check and create an instance of the relevant class. The number of classes grows exponentially with markers (although you could use java.lang.reflect.Proxy
) and your creation method needs to know about all markers ever.
Serializable
isn't so bad. If the indirection class implements Serializable
then the whole will be serialisable if the target class is Serializable
and not if it isn't.
There are a few alternatives, although none are very nice
Make the wrapper implement the interface, if it's known at compile time if the wrapped object also implements the interface. A factory method can be used to create the wrapper if it is not known until runtime if the wrapped object will implement the interface. This means you then have separate wrapper classes for the possible combinations of implemented interfaces. (With one interface, you need 2 wrappers, one with and one without. For 2 interfaces, 4 wrappers and so on.)
Expose the wrapped objects from the wrapper, so that clients can walk the chain and test each object in the chain for the interface using
instanceof
. This breaks encapsulation.Have a dedicated method to retrieve the interface, implemented by both the wrapper and the wrapped object. E.g.
asSomeInterface()
. The wrapper delegates to the wrapped object, or creates a proxy around the wrapped object to preserve encapsulation.Create one wrapper class for each interface - the wrapper is implemented as usual - it implements the interface and delegates to another implementation of that interface. A wrapped object may implement several interfaces so several wrapper instances are combined into one logical instance by using a dynamic proxy to delegate the interface methods implemented by the proxy to the appropriate wrapper instance. It's necessary that the set of interfaces implemented by the proxy do not have any method signatures in common.
Microsoft baked aggregation (Wikipedia) into their Component Object Model (COM). It appears to be unused by the majority yet results in considerable complexity for COM object implementors, since there are rules that every object must adhere to. Wrapped objects are encapsulated by having wrapped objects know about they're wrappers, having to maintain a pointer to the wrapper, which is used when implementing QueryInterface (loosely instanceof
) for the exposed public interfaces - the wrapped object returns the interface implemented on the wrapper rather than it's own implementation.
I've not seen a clean, easy to understand/implement and correctly encapsulated solution to this. COM aggregation works and provides complete encapsulation, but it's a cost you pay for every single object you implement, even if it is never used in an aggregate.
精彩评论