【问题标题】:Java Design: dealing with data objects used by many classesJava 设计:处理许多类使用的数据对象
【发布时间】:2012-05-27 11:22:15
【问题描述】:

首先,简要介绍一下提出这个问题的库:

我有一个库,它在提供的串行端口上连续侦听,读取字节块并将它们传递给以某种有意义的方式进行处理(细节对问题并不重要)。为了使库更具可重用性,处理这些字节被抽象到一个接口(FrameProcessor)。库本身中存在一些默认实现来处理无论应用程序使用它总是会发生的处理。但是,支持添加自定义处理器来执行应用程序特别关心的事情。

除了传递给这些处理器的字节之外,还有一个数据对象 (ReceiverData),其中包含大多数(但不保证是全部)处理器可能会感兴趣的信息。它完全由库本身维护(即,应用程序不负责设置/维护任何 ReceiverData 实例。他们不必关心数据是如何可用的,只要它可用就行了)。

现在,ReceiverData 正在作为参数传递给每个处理器:

public interface FrameProcessor {

    public boolean process(byte[] frame, ReceiverData receiverData);
}

但是,我真的不喜欢这种方法,因为它需要将数据传递给可能不一定关心它的东西。此外,对于关心 ReceiverData 的处理器,他们必须在他们进行的任何其他方法调用中传递对象引用(前提是这些方法调用需要访问该数据)。

我考虑将 FrameProcessor 更改为抽象类,然后为受保护的 ReceiverData 成员定义一个设置器。但这似乎也有点恶心 - 必须遍历所有 FrameProcessor 的列表并设置 ReceiverData 实例。

我还考虑过某种静态线程上下文对象(由于库支持同时侦听多个端口,因此必须线程化)。本质上,您将拥有以下内容:

public class ThreadedContext {

    private static Map<Long, ReceiverData> receiverData;

    static {
        receiverData = new HashMap<Long, ReceiverData>();
    }

    public static ReceiverData get() {
        return receiverData.get(Thread.currentThread().getId());
    }

    public static void put(ReceiverData data) {
        receiverData.put(Thread.currentThread().getId(), data);
    }
}

这样,当库中的每个线程启动时,它只需将对其 ReceiverData 的引用添加到 ThreadedContext,然后处理器可以根据需要使用该引用,而无需传递它。

这当然是一个迂腐的问题,因为我已经找到了一个可以正常工作的解决方案。它只是困扰我。想法?更好的方法?

【问题讨论】:

  • 我最喜欢ThreadedContext 方法。仅供参考,您可以使用 java.lang.ThreadLocal&lt;T&gt; 代替带有线程 ID 作为键的 HashMap
  • 您能否详细说明 ReceiverData 的实际功能和包含的内容。在我看来它应该是某些 FrameProcessor 子类的成员,但如果没有更多细节就很难说。

标签: java data-objects


【解决方案1】:

我最喜欢你目前的方法。它本质上是线程安全的(因为无状态)。它允许多个线程使用同一个处理器。它易于理解和使用。例如,它非常类似于 servlet 的工作方式:请求和响应对象被传递给 servlet,即使它们并不关心它们。而且它也很容易进行单元测试,因为您不必设置线程本地上下文来测试处理器。您只需传递一个 ReceiverData(真实的或虚假的),就是这样。

您可以将两者混合在一个参数中,而不是传递一个字节数组和一个 ReceiverData。

【讨论】:

  • 请求/响应对象实际上是一些被认为是 servlet 设计错误的东西(例如,参见 here);厨房水槽对象,服务定位器反模式......
  • 好吧,这个特定的博主认为他们是错误的。厨房水槽和服务定位器问题在这里不适用,因为 ReceiverData 是一个简单的数据对象。我同意 servlet 很难测试。但是将状态放在线程本地比在有关可测试性的方法参数中注入状态要糟糕得多。
  • 我最终还是坚持将 ReceiverData 对象传递给各种处理器。我并没有高度重视您关于易于理解的观点,但是,仔细想想,这是一个我们希望被许多应用程序使用的库,因此了解它的工作原理非常重要。易于测试也是一个不错的好处。感谢您的所有回复。
【解决方案2】:

byte[]ReceiverData 封装到一个新类中并将其传递给帧处理器。这不仅意味着他们可以将相同的单个对象传递给他们自己的方法,而且还可以在必要时允许未来的扩展。

public class Frame {
    private byte[] rawBytes;
    private ReceiverData receiverData;

    public ReceiverData getReceiverData() { return receiverData; }
    public byte[] getRawBytes() { return frame; }
}

public interface FrameProcessor {
    public boolean process(Frame frame);
}

虽然这看起来有点矫枉过正,并且需要处理器进行不必要的方法调用,但您可能会发现您不想提供对原始字节数组的访问。也许您想改用ByteChannel 并提供只读访问权限。这取决于您的库及其使用方式,但您可能会发现您可以在 Frame 中提供比简单字节数组更好的 API。

【讨论】:

  • 这似乎是解决问题的最自然方式。字节和 ReceiverData 总是放在一起,所以把它们放在一起。它确实使数据更难获取(frame.getRawBytes() 而不是frame),但实际上并非如此。
【解决方案3】:

正如 OP 所述,process(byte[] frame, ReceiverData data) 的问题在于,ReceiverData 可能会或可能不会被实现使用。因此,process() 依赖于ReceiverData 是“错误的”。相反,FrameProcessor 实现应该使用Provider,它可以为当前帧按需提供ReceiverData 的实例。

下面的例子说明了这一点。为了清楚起见,我使用了依赖注入,但您也可以在构造函数中传递这些对象。 FrameContext 将使用 ThreadLocal&lt;T&gt;s,就像 OP 中建议的那样。有关实施提示,请参阅 this linkProvider&lt;T&gt; 的 DIY 实现可能直接依赖于 FrameContext

如果您想走这条路,请考虑使用 DI 框架,例如 Google GuiceCDI。使用自定义范围时,Guice 可能更容易。

public class MyProcessor implements FrameProcessor {

    @Inject
    private Provider<ReceiverData> dataProvider;

    public boolean process(byte[] frame) {
        ...
        ReceiverData data = dataProvider.get();
        ...
    }
}

public class Main {

    @Inject
    private FrameContext context;

    public void receiveFrame(byte[] frame, ... ) {

        context.begin();
        ...
        context.setReceiverData(...); // receiver data is thread-local
        ...

        for (FrameProcessor processor : processors)
            processor.process(frame);

        context.end();
    }
}

这种方法可扩展性很强;未来需要的对象可以添加到上下文/范围对象中,并将相应的提供者注入处理器:

public class MyProcessor ... {

    @Inject private Provider<FrameMetaData>;
    @Inject private Provider<FrameSource>;
    ...
}

正如您从这个示例中看到的,这种方法还可以让您避免将来您将“子对象”添加到 ReceiverData 的情况,从而导致厨房水槽对象情况(例如,ReceiverData.metaData , ReceiverData.frameSource, ...)。

注意:理想情况下,您应该处理生命周期等于单帧的对象。然后,您可以在构造函数中声明(并注入!)用于处理单个帧的依赖项,并为每个帧创建一个新的处理器。但我假设您正在处理大量帧,因此出于性能原因希望坚持使用当前方法。

【讨论】:

    猜你喜欢
    • 2012-05-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-08
    • 2020-11-18
    • 1970-01-01
    • 2010-09-24
    • 1970-01-01
    相关资源
    最近更新 更多