松耦合原生方法
本文关键字:方法 原生 耦合 | 更新日期: 2023-09-27 18:14:49
我需要使用c#的本地DLL。DLL公开了我可以通过P/Invoke访问的几个方法和几个类型。所有这些代码都在一个标准的原生方法类中。为了保持简单,它看起来像这样:
internal static class NativeMethods
{
[DllImport("Foo.dll", SetLastError = true)]
internal static extern ErrorCode Bar(ref Baz baz);
internal enum ErrorCode { None, Foo, Baz,... }
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
internal struct Baz
{
public int Foo;
public string Bar;
}
}
为了实现松耦合,我需要提取一个接口,并使用调用native emethods来实现它:
public interface IFoo
{
void Bar(ref Baz baz);
}
public class Foo : IFoo
{
public void Bar(ref Baz baz)
{
var errorCode = NativeMethods.Bar(baz);
if (errorCode != ErrorCode.None) throw new FooException(errorCode);
}
}
现在我可以在代码中使用IFoo作为依赖项,并在测试中模拟它:
public class Component : IComponent
{
public Component(IFoo foo, IService service) { ... }
}
这里似乎有些不对劲。根据FxCop, NativeMethods
必须是内部的。IFoo
(从NativeMethods
中提取)也是内部的,这是有意义的。但我不能把它变成内部的b/c,它被用于公共部门(应该保持公共)。因此:为了实现松耦合,我必须改变组件的可见性,否则它将是内部的。你们觉得这个怎么样?
另一个问题:组件有public void DoSomehing(Bar bar)
方法,它使用了在native emethods .cs中定义的Bar
。我也必须把它公开,这样才能正常工作。或者创建一个新的Bar
类来包装NativeMethods+Bar
。如果我选择公开的方式,那么NativeMethods
也会变成公开的,FxCop抱怨"嵌套类型不应该是可见的"。如果我用包装纸的方式…嗯,我觉得为所有的"原生类型"做这个太费功夫了。哦,还有第三种方法:把类型从native emethods中移开,让它们成为公共的。然后FxCop开始分析它们,并找到当它们嵌套在native emethods中时隐藏的所有错误。
你可能需要一个公共抽象类,而不是一个接口。
可以包括内部抽象方法(引用内部类型),这实际上使得从程序集外部以正常方式子类化是不可能的(但是InternalsVisibleTo
将允许您创建一个假的用于测试)。
基本上接口还没有真正的被设计得很好,因为它们本可以从组件的角度来设计。
这正是我在Noda Time为CalendarSystem
所做的-它的API使用内部类型,但我想让它成为一个接口或抽象类。
如何提取INativeMethods
?
我们将免费获得的不完整列表:
- 完全支持TDD
- 您不需要运行应用程序来验证大多数情况
- 非常容易模拟和添加对不同环境的支持'OS
- 分析性能,测量WinApi的调用计数
显示代码
接口与WinApi相同:
internal interface INativeMethods
{
IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wparam, StringBuilder lparam);
bool GetWindowRect(IntPtr hwnd, out Rect rect);
IntPtr GetWindow(IntPtr hwnd, uint cmd);
bool IsWindowVisible(IntPtr hwnd);
long GetTickCount64();
int GetClassName(IntPtr hwnd, StringBuilder classNameBuffer, int maxCount);
int DwmGetWindowAttribute(IntPtr hwnd, int attribute, out Rect rect, int sizeOfRect);
bool GetWindowPlacement(IntPtr hwnd, ref WindowPlacement pointerToWindowPlacement);
int GetDeviceCaps(IntPtr hdc, int index);
}
实现是静态类的瘦代理:
internal class NativeMethodsWraper : INativeMethods
{
public IntPtr SendMessage(IntPtr hwnd, uint msg, IntPtr wparam, StringBuilder lparam)
{
return NativeMethods.SendMessage(hwnd, msg, wparam, lparam);
}
public bool GetWindowRect(IntPtr hwnd, out Rect rect)
{
return NativeMethods.GetWindowRect(hwnd, out rect);
}
public IntPtr GetWindow(IntPtr hwnd, uint cmd)
{
return NativeMethods.GetWindow(hwnd, cmd);
}
public bool IsWindowVisible(IntPtr hwnd)
{
return NativeMethods.IsWindowVisible(hwnd);
}
public long GetTickCount64()
{
return NativeMethods.GetTickCount64();
}
public int GetClassName(IntPtr hwnd, StringBuilder classNameBuffer, int maxCount)
{
return NativeMethods.GetClassName(hwnd, classNameBuffer, maxCount);
}
public int DwmGetWindowAttribute(IntPtr hwnd, int attribute, out Rect rect, int sizeOfRect)
{
return NativeMethods.DwmGetWindowAttribute(hwnd, attribute, out rect, sizeOfRect);
}
public bool GetWindowPlacement(IntPtr hwnd, ref WindowPlacement pointerToWindowPlacement)
{
return NativeMethods.GetWindowPlacement(hwnd, ref pointerToWindowPlacement);
}
public int GetDeviceCaps(IntPtr hdc, int index)
{
return NativeMethods.GetDeviceCaps(hdc, index);
}
}
让我们用p 'Invoke imports
来完成这个internal static class NativeMethods
{
[DllImport("gdi32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)]
public static extern int GetDeviceCaps(IntPtr hdc, int index);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool IsWindowVisible(IntPtr hWnd);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr SendMessage(IntPtr hWnd, uint msg, IntPtr wParam, [Out] StringBuilder lParam);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr GetWindow(IntPtr hWnd, uint uCmd);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount);
[DllImport("kernel32.dll")]
public static extern long GetTickCount64();
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowRect(IntPtr hWnd, out Rect lpRect);
[DllImport(@"dwmapi.dll")]
public static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out Rect pvAttribute, int cbAttribute);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetWindowPlacement(IntPtr hWnd, ref WindowPlacement lpwndpl);
}
internal class GetTickCount64TimeProvider : ITimeProvider
{
private readonly INativeMethods _nativeMethods;
public GetTickCount64TimeProvider(INativeMethods nativeMethods)
{
_nativeMethods = nativeMethods;
}
public Timestamp Now()
{
var gtc = _nativeMethods.GetTickCount64();
var getTickCountStamp = Timestamp.FromMilliseconds(gtc);
return getTickCountStamp;
}
}
单元测试
internal class GetTickCount64TimeProvider : ITimeProvider
{
private readonly INativeMethods _nativeMethods;
public GetTickCount64TimeProvider(INativeMethods nativeMethods)
{
_nativeMethods = nativeMethods;
}
public Timestamp Now()
{
var gtc = _nativeMethods.GetTickCount64();
var getTickCountStamp = Timestamp.FromMilliseconds(gtc);
return getTickCountStamp;
}
}
难以置信,但是可以通过模拟WinApi
来验证任何期望[Test]
public void GetTickCount64_ShouldCall_NativeMethod()
{
var nativeMock = MockRepository.GenerateMock<INativeMethods>();
var target = GetTarget(nativeMock);
target.Now();
nativeMock.AssertWasCalled(_ => _.GetTickCount64());
}
[Test]
public void Now_ShouldReturn_Microseconds()
{
var expected = Timestamp.FromMicroseconds((long) int.MaxValue * 1000);
var nativeStub = MockRepository.GenerateStub<INativeMethods>();
nativeStub.Stub(_ => _.GetTickCount64()).Return(int.MaxValue);
var target = GetTarget(nativeStub);
var actual = target.Now();
Assert.AreEqual(expected, actual);
}
private static GetTickCount64TimeProvider GetTarget(INativeMethods nativeMock)
{
return new GetTickCount64TimeProvider(nativeMock);
}
模拟out'ref
参数可能会引起头痛,所以下面是供将来参考的代码:
[Test]
public void When_WindowIsMaximized_PaddingBordersShouldBeExcludedFromArea()
{
// Top, Left are -8 when window is maximized but should be 0,0
// http://blogs.msdn.com/b/oldnewthing/archive/2012/03/26/10287385.aspx
INativeMethods nativeMock = MockRepository.GenerateStub<INativeMethods>();
var windowRectangle = new Rect() {Left = -8, Top = -8, Bottom = 1216, Right = 1936};
var expectedScreenBounds = new Rect() {Left = 0, Top = 0, Bottom = 1200, Right = 1920};
_displayInfo.Stub(_ => _.GetScreenBoundsFromWindow(windowRectangle.ToRectangle())).Return(expectedScreenBounds.ToRectangle());
var hwnd = RandomNativeHandle();
StubForMaximizedWindowState(nativeMock, hwnd);
StubForDwmRectangle(nativeMock, hwnd, windowRectangle);
WindowCoverageReader target = GetTarget(nativeMock);
var window = target.GetWindowFromHandle(hwnd);
Assert.AreEqual(WindowState.Maximized, window.WindowState);
Assert.AreEqual(expectedScreenBounds.ToRectangle(), window.Area);
}
private void StubForDwmRectangle(INativeMethods nativeMock, IntPtr hwnd, Rect rectToReturnFromWinApi)
{
var sizeOf = Marshal.SizeOf(rect);
var rect = new Rect();
nativeMock.Stub(_ =>
{
_.DwmGetWindowAttribute(
hwnd,
(int)DwmWindowAttribute.DwmwaExtendedFrameBounds,
out rect, // called with zeroed object
sizeOf);
}).OutRef(rectToReturnFromWinApi).Return(0);
}
private IntPtr RandomNativeHandle()
{
return new IntPtr(_random.Next());
}
private void StubForMaximizedWindowState(INativeMethods nativeMock, IntPtr hwnd)
{
var maximizedFlag = 3;
WindowPlacement pointerToWindowPlacement = new WindowPlacement() {ShowCmd = maximizedFlag};
nativeMock.Stub(_ => { _.GetWindowPlacement(Arg<IntPtr>.Is.Equal(hwnd), ref Arg<WindowPlacement>.Ref(new Anything(), pointerToWindowPlacement).Dummy); }).Return(true);
}