创建可维护和可测试的 Windows 窗体应用程序的 10 种方法(译)
我遇到的大多数 Windows 窗体应用程序都不存在或单元测试覆盖率极低。而且它们通常也很难维护,项目中各种 Form 类的代码背后有数百甚至数千行代码。但它不必是这样。仅仅因为 Windows 窗体是一项“遗留”技术,并不意味着你注定会造成无法维护的混乱。下面是创建可维护和可测试的 Windows 窗体应用程序的十个技巧。
1. 用用户控件隔离你的用户界面
首先,避免在一个表单上放置太多控件。通常,你的应用程序的主要形式可以分解为逻辑区域(我们可以称之为“视图”)。如果将这些区域中的每个区域的控件放入它们自己的容器中,那么你自己的生活就会变得更加轻松,而在 Windows 窗体中,最简单的方法是使用用户控件。因此,如果你有一个资源管理器样式的应用程序,左侧是树视图,右侧是详细信息视图,则将 TreeView 放入其自己的 UserControl,并为每个可能的右侧视图创建一个 UserControl。同样,如果你有选项卡控件,请为选项卡控件中的每个页面创建一个单独的 UserControl。
这样做不仅可以防止你的类变得难以管理,而且还可以使设置调整大小和 Tab 键顺序等任务变得更加简单。它还允许你在必要时轻松地一次性禁用用户界面的整个部分。你还会发现,当你将用户界面分解为包含逻辑分组控件的较小 UserControl 时,重新设计应用程序的 UI 布局会变得更加容易。
2. 将非 UI 代码排除在后面的代码之外
在 Windows 窗体应用程序中,你总是会在窗体背后的代码中找到访问网络、数据库或文件系统的代码。这严重违反了“单一责任原则”。你的 Form 或 UserControl 类的重点应该只是用户界面。因此,当你检测到背后的代码中存在与 UI 无关的代码时,请将其重构为具有单一职责的类。因此,你可以创建一个 PreferencesManager 类,或者一个负责调用特定 Web 服务的类。然后可以将这些类作为依赖项注入到你的 UI 组件中(尽管这只是第一步——我们可以进一步扩展这个想法,我们很快就会看到)。
3. 用接口创建被动视图
一种特别有用的技术是使你创建的每个窗体和用户控件都实现一个视图界面。此接口应包含允许设置和检索视图中控件的状态和内容的属性。它还可能包括报告用户交互的事件,例如单击按钮或移动滑块。目标是这些视图接口的实现是完全被动的。理想情况下,你的 Forms 和 UserControls 背后的代码中不应该有任何条件逻辑。
下面是一个用于新用户条目视图的视图界面示例。这个视图的实现应该是微不足道的。任何业务逻辑都不属于后面的代码(我们接下来将讨论它属于哪里)。
interfaceINewUserView{stringFirstName{get;set;}stringLastName{get;set;}eventEventHandlerSaveClicked;}
通过确保你的视图实现尽可能简单,你将能够最大程度地迁移到替代 UI 框架(如 WPF),因为你唯一需要做的就是在新技术中重新创建视图。所有其他代码都可以重复使用。
4.使用presenters控制视图
因此,如果你已将所有视图设为被动并实现接口,则你需要一些能够实现应用程序业务逻辑并控制视图的东西。我们可以称这些为“presenter”类。这是称为“模型视图演示者”或 MVP 的模式。
在模型视图展示器中,你的视图是完全被动的,展示器会指示视图显示哪些数据。还允许视图与演示者通信。在我上面的示例中,它通过引发事件来实现,但通常使用这种模式,你的视图可以直接调用演示者。
绝对不允许视图开始直接操作模型(包括你的业务实体、数据库层等)。如果你遵循 MVP 模式,你的应用程序中的所有业务逻辑都可以轻松测试,因为它位于 Presenter 或其他非 UI 类中。
5. 为错误报告创建服务
通常,你的演示者类需要显示错误消息。但不要只是将 MessageBox.Show 放入非 UI 类中。你将使该方法无法进行单元测试。而是创建一个服务(比如 IErrorDisplayService),你的演示者可以在需要报告问题时调用该服务。这使你的演示者单元保持可测试性,并且还提供了更改将来向用户呈现错误的方式的灵活性。
6. 使用命令模式
如果你的应用程序包含一个带有大量按钮供用户单击的工具栏,则命令模式可能非常适合。命令模式规定你为每个命令创建一个类。这有很大的好处,可以将你的代码分成小类,每个小类都有一个责任。它还允许你集中处理与特定命令有关的所有事情。是否应该启用该命令?它应该是可见的吗?它的工具提示和快捷键是什么?它是否需要特定的特权或许可才能执行?命令运行时抛出的异常应该如何处理?
命令模式允许你标准化处理应用程序中所有命令所共有的每个问题的方式。你的命令对象将有一个 Execute 方法,该方法实际上包含为该命令执行所需行为的代码。在许多情况下,这将涉及调用其他对象和业务服务,因此你需要将它们作为依赖项注入到命令对象中。你的命令对象本身应该可以(并且直接)进行单元测试。
7. 使用 IoC 容器管理依赖项
如果你正在使用 Presenter 类和 Command 类,那么你可能会发现它们所依赖的类的数量随着时间的推移而增长。这是Unity或StructureMap等控制反转容器真正可以帮助你的地方。无论它们具有多少级别的依赖关系,它们都允许你轻松构建视图和演示器。
8. 使用事件聚合器模式
另一种在 Windows 窗体应用程序中非常有用的设计模式是事件聚合器模式(有时也称为“信使”或“事件总线”)。这是一种模式,其中事件的引发者和事件的处理者根本不需要相互耦合。当你的代码中发生需要在其他地方处理的“事件”时,只需向事件聚合器发布一条消息即可。然后需要响应该消息的代码可以订阅和处理它,而无需担心是谁提出的。
例如,你发送一条“请求帮助”消息,其中包含用户当前在 UI 中的位置的详细信息。然后另一个服务处理该消息并确保在 Web 浏览器中启动帮助文档中的正确页面。另一个例子是导航。如果你的应用程序有多个屏幕,则可以将“导航”消息发布到事件聚合器,然后订阅者可以通过确保新屏幕显示在用户界面中来响应该消息。
除了从根本上分离事件的发布者和订阅者之外,事件聚合器还具有创建极易进行单元测试的代码的巨大好处。
9. 使用 Async 和 Await 进行线程处理
如果你的目标是 .NET 4 及更高版本并使用 Visual Studio 12 或更高版本,请不要忘记你可以使用新的 async 和 await 关键字,这将大大简化应用程序中的任何线程代码,并自动处理回送后台任务完成后进入 UI 线程。它们还极大地简化了跨多个链式后台任务的异常处理。它们非常适合 Windows 窗体应用程序,如果你还没有的话,非常值得一试。
10.不要太晚
可以将我上面描述的所有模式和技术改造为现有的 Windows 窗体应用程序,但我可以从痛苦的经验告诉你,这可能需要大量工作,尤其是当窗体背后的代码达到数千行时代码领域。如果你开始使用 MVP、事件聚合器和命令模式等模式构建应用程序,你会发现随着它们变得越来越大,维护起来会少很多痛苦。你还可以对所有业务逻辑进行单元测试,这对于持续的可维护性至关重要。