我发现自己经常遇到以下问题。我有一些标记接口(为简单起见,我们使用java.io.Serializable
)和几个包装器(Adapter,Decorator,Proxy,...)。但是当你将Serializable实例包装在另一个实例(不可序列化)中时,你会失去功能。 java.util.RandomAccess也会出现同样的问题,可以通过List实现来实现。是否有一个很好的OOP方式来处理它?</ p>
答案 0 :(得分:7)
这是最近关于番石榴邮件列表的讨论 - 我的回答涉及到这个相当基本的问题。
http://groups.google.com/group/guava-discuss/browse_thread/thread/2d422600e7f87367/1e6c6a7b41c87aac
它的要点是这样的: 当您希望包裹对象时,请勿使用标记界面。 (嗯,这很普遍 - 如何做你知道你的对象不会被客户端包裹?)
例如,ArrayList
。它显然实现了RandomAccess
。然后,您决定为List
个对象创建一个包装器。哎呀!现在当你换行时,你必须检查被包装的对象,如果它是RandomAccess,你创建的包装器应该也实现RandomAccess!
如果您只有一个标记界面,这可以“正常”...但是如果被包装的对象可以是Serializable呢?如果它是“不可变”(假设你有一个类型来表示),该怎么办?还是同步? (用同样的假设)。
正如我在对邮件列表的回答中所指出的那样,这种设计缺陷也体现在良好的旧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
和实现它的开放式对象集(可能是多个枚举)。
因此,当您包装这些对象时,您将获得一组功能,并且您可以轻松决定要保留哪些功能,要删除哪些功能。
这个确实显然有它的缺点,所以它只能用于你真正感受到包装器隐藏功能表达为标记接口的痛苦的情况。例如,假设您编写了一段带有List的代码,但它必须是RandomAccess AND Serializable。通过常规方法,这很容易表达:
<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));
...
}
我真的希望有一种比其中任何一种更令人满意的方法,但从前景来看,它似乎不可行(至少没有引起组合型爆炸)。
编辑:另一个缺点是,如果没有明确的类型每个功能,我们就没有自然的位置来放置表达此功能提供的方法。这在讨论中并不太重要,因为我们讨论的是标记接口,即没有通过其他方法表达的功能,但我提到它是为了完整性。
PS:顺便说一下,如果你浏览Guava的集合代码,你真的可以感受到这个问题引起的痛苦。是的,一些优秀的人试图隐藏在好的抽象背后,但是潜在的问题仍然是痛苦的。答案 1 :(得分:5)
如果您感兴趣的接口都是标记接口,那么您可以让所有包装类实现接口
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)
答案 2 :(得分:1)
对于像RandomAccess
这样的人,你可以做的并不多。当然,您可以执行instanceof
检查并创建相关类的实例。类的数量随着标记呈指数级增长(尽管您可以使用java.lang.reflect.Proxy
),并且您的创建方法需要了解所有标记。
Serializable
并不是那么糟糕。如果间接类实现了Serializable
,那么如果目标类是Serializable
,那么整个将是可序列化的,而如果不是{。}}则不是。
答案 3 :(得分:1)
有一些替代品,虽然没有一个非常好
使包装器实现接口,如果在编译时已知包装对象也实现了接口。如果包装对象将实现接口,则直到运行时才知道工厂方法可用于创建包装器。这意味着您可以使用单独的包装类来实现已实现接口的可能组合。 (使用一个接口,您需要2个包装器,一个带有一个包装器,一个没有包装器。对于2个接口,4个包装器等等。)
从包装器中公开包装的对象,以便客户端可以使用instanceof
遍历链并使用链接测试链中的每个对象。这破坏了封装。
有一个专用的方法来检索由包装器和包装对象实现的接口。例如。 asSomeInterface()
。包装器委托给包装对象,或者在包装对象周围创建代理以保持封装。
为每个接口创建一个包装类 - 包装器照常实现 - 它实现接口并委托给该接口的另一个实现。包装对象可以实现多个接口,因此通过使用动态代理将多个包装器实例组合到一个逻辑实例中,以将代理实现的接口方法委托给适当的包装器实例。代理实现的接口集必须没有任何共同的方法签名。
Microsoft将aggregation(Wikipedia)烘焙到其组件对象模型(COM)中。它似乎未被大多数人使用,但却导致COM对象实现者相当复杂,因为每个对象都必须遵守规则。通过使包装对象知道它们是包装器来封装包装对象,必须维护指向包装器的指针,该指针在为公开的公共接口实现QueryInterface(松散instanceof
)时使用 - 包装对象返回接口在包装器上实现而不是它自己的实现。
我没有看到干净,易于理解/实施和正确封装的解决方案。 COM聚合工作并提供完整的封装,但这是您为实现的每个对象支付的成本,即使它从未在聚合中使用。