Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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, Proxy, ...). 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?

like image 510
whiskeysierra Avatar asked Aug 07 '10 20:08

whiskeysierra


3 Answers

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.

like image 144
Dimitris Andreou Avatar answered Nov 15 '22 00:11

Dimitris Andreou


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)
like image 27
mkadunc Avatar answered Nov 14 '22 23:11

mkadunc


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.

like image 45
Tom Hawtin - tackline Avatar answered Nov 15 '22 01:11

Tom Hawtin - tackline