我发现自己经常遇到以下问题。我有某种标记接口(为简单起见,我们使用 java.io.Serialized)和几个包装器(适配器、装饰器、代理...)。但是,当您将可序列化实例包装在另一个实例(不可序列化)中时,您就会失去功能。同样的问题也发生在 java.util.RandomAccess 上,它可以通过 List 实现来实现。有没有好的 OOP 方法来处理它?
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?
发布评论
评论(4)
这是最近关于 Guava 邮件列表的讨论 - 我的回答涉及这个相当基本的问题。
http://groups.google.com/group/guava- Discuss/browse_thread/thread/2d422600e7f87367/1e6c6a7b41c87aac
其要点是这样的:
当您希望包装对象时,不要使用标记接口。 (嗯,这很笼统 - 你如何知道你的对象不会被客户端包装?)
例如,一个 ArrayList。显然,它实现了RandomAccess。然后您决定为
List
对象创建一个包装器。哎呀!现在,当您包装时,您必须检查包装的对象,如果它是 RandomAccess,您创建的包装器应该也实现 RandomAccess!如果您只有一个标记界面,这“很好”!但是如果包装的对象可以序列化怎么办?如果它是“不可变”(假设您有一个类型来表示它)怎么办?还是同步? (具有相同的假设)。
正如我在对邮件列表的回答中指出的那样,这种设计缺陷也体现在旧的
java.io
包中。假设您有一个接受InputStream
的方法。你会直接阅读它吗?如果它是一个昂贵的流,并且没有人愿意为您将其包装在 BufferedInputStream 中怎么办?哦,这很容易!您只需检查 BufferedInputStream 的流实例,如果没有,您自己包装它!但没有。流可能在链下游的某个地方进行缓冲,但您可能会得到它的包装器,它不是 BufferedInputStream 的实例。因此,“此流已缓冲”的信息丢失了(也许您必须悲观地浪费内存来再次缓冲它)。如果您想正确做事,只需将功能建模为对象即可。考虑:
编辑: 应该注意的是,我使用枚举只是为了方便。可以有一个接口
Capability
和一组实现它的开放式对象(可能是多个枚举)。因此,当您包装这些对象时,您将获得一组功能,并且您可以轻松决定保留哪些功能、删除哪些功能、添加哪些功能。
显然,这确实有其缺点,因此仅在您真正感受到包装器隐藏表示为标记接口的功能的痛苦的情况下才使用它。例如,假设您编写了一段采用列表的代码,但它必须是随机访问和可序列化的。用通常的方法,这很容易表达:
但在我描述的方法中,你所能做的就是:
我真的希望有一种比这两种方法更令人满意的方法,但从前景来看,这似乎是不可行的(至少没有,引起组合型爆炸)。
编辑:另一个缺点是,如果每个功能没有明确的类型,我们就没有合适的位置来放置表达此功能所提供功能的方法。这在本次讨论中并不是太重要,因为我们讨论的是标记接口,即不通过其他方法表达的功能,但为了完整性我提到它。
PS:顺便说一下,如果你浏览一下 Guava 的集合代码,你就能真正感受到这个问题造成的痛苦。是的,一些好人试图将其隐藏在漂亮的抽象背后,但潜在的问题仍然令人痛苦。
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 implementsRandomAccess
, obviously. Then you decide to create a wrapper forList
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 anInputStream
. Will you read directly from it? What if it is a costly stream, and nobody cared to wrap it in aBufferedInputStream
for you? Oh, that's easy! You just checkstream 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:
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:
But in the approach I describe, all you can do is:
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.
如果您感兴趣的接口都是标记接口,则可以让所有包装器类实现一个接口,
其实现如下所示:
这就是在
java.sql.Wrapper
中完成的方式。如果接口不仅仅是一个标记,而且实际上具有一些功能,您可以添加一个方法来解包:If the interfaces you're interested in are all marker interfaces, you could have all your wrapper classes implement an interface
whose implementation would look like this:
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:对于
RandomAccess
之类的东西,您无能为力。当然,您可以执行instanceof
检查并创建相关类的实例。类的数量随着标记呈指数级增长(尽管您可以使用 java.lang.reflect.Proxy),并且您的创建方法需要了解所有标记。Serialized
并没有那么糟糕。如果间接类实现了Serialized
,那么如果目标类是Serialized
,则整个类将是可序列化的,如果不是,则整个类将不是可序列化的。For the likes of
RandomAccess
there is not much you can do. You can, of course, do aninstanceof
check and create an instance of the relevant class. The number of classes grows exponentially with markers (although you could usejava.lang.reflect.Proxy
) and your creation method needs to know about all markers ever.Serializable
isn't so bad. If the indirection class implementsSerializable
then the whole will be serialisable if the target class isSerializable
and not if it isn't.有一些替代方案,尽管没有一个非常好
如果在编译时知道被包装的对象是否也实现了该接口,则使包装器实现该接口。如果直到运行时才知道被包装的对象是否将实现该接口,则可以使用工厂方法来创建包装器。这意味着您可以为实现的接口的可能组合拥有单独的包装类。 (对于一个接口,您需要 2 个包装器,一个有,一个没有。如果有 2 个接口,则需要 4 个包装器,依此类推。)
从包装器中公开包装的对象,以便客户端可以遍历链并测试其中的每个对象。使用
instanceof
的接口链。这会破坏封装。有一个专门的方法来检索接口,由包装器和被包装对象实现。例如
asSomeInterface()
。包装器委托给被包装的对象,或者围绕被包装的对象创建代理以保留封装。为每个接口创建一个包装器类 - 包装器照常实现 - 它实现该接口并委托给该接口的另一个实现。一个包装对象可以实现多个接口,因此通过使用动态代理将代理实现的接口方法委托给适当的包装器实例,将多个包装器实例组合成一个逻辑实例。代理实现的接口集必须没有任何共同的方法签名。
Microsoft 烘焙了聚合 (Wikipedia)进入其组件对象模型(COM)。它似乎未被大多数人使用,但却给 COM 对象实现者带来了相当大的复杂性,因为每个对象都必须遵守一些规则。包装对象是通过让包装对象知道它们是包装器来封装的,必须维护一个指向包装器的指针,该指针在为公开的公共接口(包装对象)实现 QueryInterface(松散的
instanceof
)时使用返回在包装器上实现的接口而不是它自己的实现。我还没有看到一个干净的、易于理解/实现且正确封装的解决方案。 COM 聚合起作用并提供完整的封装,但这是您为实现的每个对象付出的成本,即使它从未在聚合中使用。
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.