包装本机库时保持封装性
我正在编写一个 C# 库来包装 Win32 API(waveOut...
系列函数),并且已经达到了我不确定如何管理我的不同部分之间交互的程度。代码而不破坏封装。到目前为止,我有这样的设置:
public class AudioDevice
{
...
private WaveOutSafeHandle hWaveOut;
...
}
// All native method calls happen in wrapper methods here, providing
// uniformity of access, exceptions on error MMRESULTs, etc.
static class NativeWrappers
{
...
internal static WaveOutSafeHandle OpenDevice(...) { ... waveOutOpen(...) ... }
...
}
// Native methods live in a class of their own, and are only called
// from NativeWrappers
static class NativeMethods
{
...
internal static extern MMResult waveOutOpen(...);
...
}
这段代码中最重要的一点是设备包装的句柄对于设备之外的任何东西都是不可见的。
现在我想添加一个 AudioBlock
类,它将包装原生 WAVEHDR
结构和音频样本数据。我遇到的问题是,从现在开始,几乎所有我感兴趣的其他本机函数(waveOut[Un]PrepareHeader
、waveOutWrite
)都需要句柄和一个 WAVEHDR
。看起来要么设备必须接触 WAVEHDR
,要么块必须有权访问句柄。这两种方法都意味着某个类与它在概念上无权了解的事物进行交互。
当然,有几种方法可以解决这个问题:
将句柄和/或
WAVEHDR
设置为内部的而不是私有的。将
AudioBlock
设为Device
的嵌套类。有第三个类(我什至不愿意想到这个名字
(foo)Manager
),它维护(例如)从块到标头的映射,给定一个块,设备可以使用它帮助它在不接触块内部的情况下播放样本。
可能还有其他的;我希望如此:)我
对这些方法的反对(正确或错误):
从严格意义上来说,它们可能不是公开的,但使用内部成员对我来说似乎是一种逃避。什么是有效的实现细节对于图书馆中不需要它们的部分仍然是可见的。我一直在想“我想向任何使用或修改此代码的人展示什么界面?”
这几乎在我的脑海中起作用。通过将块视为设备的“实现细节”,允许它依赖于设备的内部结构似乎更容易被接受。除了区块确实是一个独立的实体之外;它不绑定到单个设备,也不用于帮助实现设备的逻辑。
这最接近我想要维持的分离水平,但开始误入过度设计的领域,就像我经常做的那样:P它还引入了一个人为的想法,即块必须从某个地方集中分配以保持映射完好无损。
那么,有人对(重新)构造这段代码有什么建议吗?有效的答案包括“你的反对#X是一个热气腾腾的瓦罐”,只要你能说服我:)预计到达时间:例如,如果你认为尝试强制执行这种事情最好由社交来完成手段(文档、约定)而不是技术手段(访问修饰符、程序集边界),请让我知道并指出一些参考资料。
I'm writing a C# library to wrap a Win32 API (the waveOut...
family of functions), and have reached a point where I'm unsure how to manage the interaction between different parts of my code without breaking encapsulation. So far, I have a setup like this:
public class AudioDevice
{
...
private WaveOutSafeHandle hWaveOut;
...
}
// All native method calls happen in wrapper methods here, providing
// uniformity of access, exceptions on error MMRESULTs, etc.
static class NativeWrappers
{
...
internal static WaveOutSafeHandle OpenDevice(...) { ... waveOutOpen(...) ... }
...
}
// Native methods live in a class of their own, and are only called
// from NativeWrappers
static class NativeMethods
{
...
internal static extern MMResult waveOutOpen(...);
...
}
The most important point in this code is that the handle wrapped by a Device is not visible to anything outside a Device.
Now I want to add an AudioBlock
class, which will wrap the native WAVEHDR
structure and the audio sample data. The problem I'm encountering is that from here on out, pretty much every other native function I'm interested in (waveOut[Un]PrepareHeader
, waveOutWrite
) needs both a handle and a WAVEHDR
. It seems that either a device will have to touch a WAVEHDR
, or a block will have to have access to a handle. Either approach means that some class interacts with something it conceptually has no business knowing about.
There are, of course, several ways to get around this:
Make handles and/or
WAVEHDR
s internal rather than private.Make
AudioBlock
a nested class ofDevice
.Have a third class (I hesitate to even think the name
(foo)Manager
) which maintains (for example) a mapping from blocks to headers, which, given a block, a device can use to help it play samples without touching the block's internals.
There may be others; I'm hoping so :)
My objections (right or wrong) to these approaches:
They might not be public in the strictest sense of the word, but using internal members seems like a copout to me. What are effectively implementation details are still visible to parts of the library which don't need them. I keep thinking "what interface do I want to present to anyone either using or modifying this code?"
This almost works in my head. By regarding a block as an "implementation detail" of a device, it seems more acceptable to allow it to rely on a device's internals. Except that a block really is an independent entity; it's not tied to a single device and isn't used to help implement a device's logic.
This gets the closest to the level of separation I want to maintain, but is starting to stray into overengineering territory, as I so often do :P It also introduces the artificial idea that blocks have to be centrally allocated from somewhere to keep the mapping intact.
So, does anyone have any recommendations for (re)structuring this code? Valid answers include "Your objection #X is a steaming crock," as long as you can persuade me :) ETA: For example, if you think trying to enforce this kind of thing is better done by social means (documentation, conventions) than technical ones (access modifiers, assembly boundaries), please let me know and point me to some references.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
就我个人而言,我只是将包装器设为内部,并将整个类集视为单个公共 API。
我理解避免这种情况的愿望 - 它迫使您创建类,这在您的开发过程中违反了单一责任原则。
然而,从“外部”世界的角度来看,任何使用您的软件的人都会看到您提供的每个类都有一个单一的、明确的责任和一个单一的目的。该 API 至少可以与您所包装的 API 一样干净(考虑到托管代码,可能要简单得多)。
在这种情况下,我这样做的主要动机是实用性。是的,我同意您在此处尝试遵循的准则 - 但是,它们只是准则,只要准则不会造成弊大于利,就值得遵循。我赞扬您尝试使其尽可能保持干净和优雅,但不幸的是,听起来在这种情况下,尝试使此“更优雅”将导致更多代码,这将等同于降低可维护性。
我在这里坚持使用最短、最简单的解决方案 - 将本机包装器置于内部,这样您就可以在包装器类中获取所需的数据结构。只需记录您在做什么以及为什么。
Personally, I'd just make the wrappers internal, and treat your whole set of classes as a single public API.
I understand the desire to avoid this - it forces you to create classes, which, for you during your development, violate the single responsibility principles.
However, from the POV of the "outside" world, anybody using your software will see each class you provide having a single, clear responsibility, and a single purpose. The API can be at least as clean as the ones you're wrapping (probably much simpler, given the managed code).
My main motivation for doing this, in this situation, is one of practicality. Yes, I agree with the guidelines you're trying to follow here - but, they're guidelines, and guidelines are something worth following provided they don't cause more harm than good. I commend you for trying to keep this as clean and elegant as possible, but unfortunately, it sounds like, in this situation, trying to make this "more elegant" is going to lead to more code, which will equate to less maintainable.
I'd stick with the shortest, simplest solution here - making the native wrappers internal, so you can get to the data structures you need in your wrapper classes. Just document what you're doing, and why.