【问题标题】:Is there a workaround for Composition and Marker Interfaces?组合和标记接口是否有解决方法?
【发布时间】:2011-03-26 19:24:56
【问题描述】:

我发现自己经常遇到以下问题。我有某种标记接口(为简单起见,我们使用java.io.Serializable)和几个包装器(适配器、装饰器、代理......)。但是,当您将 Serializable 实例包装在另一个实例(不可序列化)中时,您会失去功能。可以通过 List 实现实现的 java.util.RandomAccess 也会出现同样的问题。有没有很好的 OOP 方式来处理它?

【问题讨论】:

  • “......当你包装......你失去了功能” - 你的意思是包装的可序列化实例不再这样?
  • 是的,因为外部实例没有实现Serializable。
  • 好问题。在我看来,这是一个非常普遍的问题,我们需要更好地理解它。

标签: java oop decorator composition marker-interfaces


【解决方案1】:

这是最近关于 Guava 邮件列表的讨论 - 我的回答涉及到这个相当基本的问题。

http://groups.google.com/group/guava-discuss/browse_thread/thread/2d422600e7f87367/1e6c6a7b41c87aac

它的要点是: 当您希望您的对象被包装时,不要使用标记接口。 (嗯,这很笼统 - 如何您知道您的对象不会被客户端包装?)

例如,ArrayList。显然,它实现了RandomAccess。然后你决定为List 对象创建一个包装器。哎呀!现在,当你包装时,你必须检查被包装的对象,如果它是 RandomAccess,你创建的包装器应该实现 RandomAccess!

这“很好”...如果您只有一个标记界面!但是如果被包装的对象可以是可序列化的呢?如果它是,比如说,“不可变的”(假设你有一个类型来表示它)怎么办?还是同步的? (同样的假设)。

正如我在对邮件列表的回答中还指出的那样,这种设计缺陷也体现在旧的 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 可序列化的。使用通常的方法,这很容易表达:

<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));
  ...
}

我真的希望有一种比这两种方法都更令人满意的方法,但从前景来看,它似乎不可行(至少不会导致组合类型爆炸)。

编辑:另一个缺点是,如果没有明确的type每个功能,我们没有自然的地方来放置表达此功能提供的方法的方法。这在本次讨论中并不太重要,因为我们讨论的是 marker 接口,即不通过其他方法表达的功能,但为了完整起见,我提到它。

PS:顺便说一下,如果你浏览一下 Guava 的集合代码,你可以真正感受到这个问题造成的痛苦。是的,一些优秀的人试图将它隐藏在好的抽象背后,但潜在的问题仍然是痛苦的。

【讨论】:

  • 我希望有一个很好的解决方案,但我已经预料到“不,但是...”-答案。我最喜欢你的。
【解决方案2】:

如果你感兴趣的接口都是标记接口,你可以让你的所有包装类实现一个接口

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)

【讨论】:

  • 这行得通,考虑以下自然错误:给定一个标记接口 X 和一个(包装的)对象 o,客户端的倾向是编写常规的 o instanceof X,它会默默地返回 false ,而不是以某种方式引起程序员对错误的注意。我认为拥有 both 标记接口并让 instanceof 测试变得毫无意义是危险的/违反直觉的。 (续)
  • 在我的回答中,我将 instanceof 替换为方法调用,类似于您的方法调用,避免将其与标记接口结合使用。这些可以替换为任何值,例如 MyEnum.CAPABILITY_ONE,这样用户就不会误解并执行x instanceof MyEnum.CAPABILITY_ONE。 /希望比较笔记有用:)
  • 当然,比较笔记总是有用的 :) 我实际上非常喜欢你的回答(给了你一个 +1);但它有一些缺点 - 显而易见的一个是您需要提前定义所有功能(因此,如果不修改 Capability 枚举,就无法创建一种新的列表);
  • 嘿,你应该在另一个答案下发表评论,就像我在这里所做的那样! :) 无论如何,好点,让它看起来僵硬不是我的注意力 - 任何一组值都可以,不需要只是特定枚举的值(尽管一个狭窄的通用超类型有帮助)。我认为真正的缺点来自缺少类型。既要表达参数中需要哪些功能,又要在编译时对其进行检查,并且(效果较小,因为我们谈论的是 marker 接口)有一个自然的位置来放置与之相关的方法能力。
【解决方案3】:

对于RandomAccess 这样的人,您无能为力。当然,您可以进行instanceof 检查并创建相关类的实例。类的数量随着标记呈指数增长(尽管您可以使用 java.lang.reflect.Proxy),并且您的创建方法需要了解所有标记。

Serializable 还不错。如果间接类实现了Serializable,那么如果目标类是Serializable,那么整个将是可序列化的,如果不是,则不是。

【讨论】:

  • 好吧,Serializable 是一个的例子。你知道其他语言/概念(不同于 java)是如何处理这个问题的吗?
【解决方案4】:

有一些选择,虽然没有一个很好

  1. 让包装器实现接口,如果在编译时知道被包装的对象是否也实现了接口。如果直到运行时才知道被包装的对象是否将实现接口,则可以使用工厂方法来创建包装器。这意味着您可以为实现的接口的可能组合拥有单独的包装类。 (1个接口需要2个包装器,1个有,1个不带。2个接口,4个包装器,以此类推。)

  2. 从包装器中公开包装的对象,以便客户端可以遍历链并使用instanceof 为接口测试链中的每个对象。这会破坏封装。

  3. 有一个专门的方法来检索接口,由包装器和被包装对象实现。例如。 asSomeInterface()。包装器委托给被包装的对象,或者在被包装的对象周围创建一个代理以保持封装。

  4. 为每个接口创建一个包装器类 - 包装器像往常一样实现 - 它实现接口并委托给该接口的另一个实现。一个包装对象可以实现多个接口,因此通过使用动态代理将代理实现的接口方法委托给适当的包装实例,将多个包装实例组合成一个逻辑实例。代理实现的接口集必须没有任何共同的方法签名。

Microsoft 将 aggregation (Wikipedia) 加入到他们的组件对象模型 (COM) 中。大多数人似乎都没有使用它,但对 COM 对象实现者来说却相当复杂,因为每个对象都必须遵守一些规则。包装对象通过让包装对象知道它们是包装器来封装,必须维护指向包装器的指针,该指针在为公开的公共接口实现 QueryInterface(松散instanceof)时使用 - 包装对象返回实现的接口包装器而不是它自己的实现。

我还没有看到一个干净、易于理解/实施和正确封装的解决方案。 COM 聚合可以工作并提供完整的封装,但它是您为实现的每个对象支付的成本,即使它从未在聚合中使用。

【讨论】:

    猜你喜欢
    • 2020-05-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-12-16
    • 2021-12-30
    • 2023-04-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多