首先,简要介绍一下提出这个问题的库:
我有一个库,它可以连续侦听提供的串行端口,读取字节块并将它们传递以某种有意义的方式进行处理(细节对问题并不重要(。 为了使库更具可重用性,处理这些字节被抽象到一个接口(FrameProcessor(。 库本身中存在一些默认实现,用于处理无论使用何种应用程序都会发生的处理。 但是,支持添加自定义处理器来执行应用程序特别关心的操作。
除了传递给这些处理器的字节之外,还有一个数据对象 (ReceiverData(,其中包含大多数(但不能保证是全部(处理器可能感兴趣的信息。 它完全由库本身维护(即应用程序不负责设置/维护 ReceiverData 的任何实例。 他们不应该关心数据是如何可用的,只需要关心数据是否可用(。
目前,ReceiverData 作为参数传递给每个处理器:
public interface FrameProcessor {
public boolean process(byte[] frame, ReceiverData receiverData);
}
但是,我真的不喜欢这种方法,因为它需要将数据传递给可能不一定关心它的东西。 此外,对于关心 ReceiverData 的处理器,他们必须在他们进行的任何其他方法调用中传递对象引用(前提是这些方法调用需要访问该数据(。
我考虑过将 FrameProcessor 更改为抽象类,然后为受保护的 ReceiverData 成员定义一个 setter。 但这似乎也有点恶心 - 必须遍历所有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,然后处理器就可以根据需要使用它,而无需传递它。
这当然是一个迂腐的问题,因为我已经有一个工作正常的解决方案。 这让我很困扰。 思潮? 更好的方法?
我最喜欢你目前的方法。它本质上是线程安全的(因为无状态(。它允许多个线程使用相同的处理器。它易于理解和使用。例如,它与 servlet 的工作方式非常相似:请求和响应对象被传递给 servlet,即使它们不关心它们。单元测试也非常容易,因为您不必设置线程本地上下文即可测试处理器。你只需传递一个ReceiverData(真实的或假的(,就是这样。
您可以不传递字节数组和 ReceiverData,而是在单个参数中混合使用两者。
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。
正如 OP 所述,process(byte[] frame, ReceiverData data)
的问题在于实现可能会也可能不会使用ReceiverData
。因此,process()
依赖ReceiverData
是"错误的"。相反,FrameProcessor
实现应使用可以按需为当前帧提供ReceiverData
实例的Provider
。
下面的示例对此进行了说明。为了清楚起见,我使用了依赖注入,但您也可以在构造函数中传递这些对象。FrameContext
将使用ThreadLocal<T>
,就像OP中建议的那样。有关实现提示,请参阅此链接。DIY Provider<T>
的实施可能直接取决于FrameContext
。
如果你想走这条路,考虑使用DI框架,如Google Guice或CDI。使用自定义范围时,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
、...(。
注意:理想情况下,您的处理对象生存期等于单个帧。然后,您只需声明(并注入!(用于在构造函数中处理单个帧的依赖项,并为每个帧创建一个新处理器。但我假设您正在处理大量帧,因此出于性能原因,您希望坚持使用当前的方法。