11

我正在寻找一些关于构建 Delphi 程序以实现可维护性的建议。尽管我第一次学习使用 Turbo Pascal 编程,但我在主要使用 C/C++ 几十年后才开始接触 Delphi 编程,所以我对基本语言并不感到不舒服。在我之前使用 C++ 和 C# 的经验中,我通过使用 cxxtest 和 NUnit 成为了 TDD 转换者。

我继承了我现在负责维护的这个程序。它主要由表单和几个数据模块组成。应用的业务逻辑和数据访问主要分散在表单上,​​数据模块多半只是全局ADO对象生存的地方。数据库访问一般通过引用 TADOQuery 或 TADOCommand 的全局实例,将 SQL 文本直接格式化为对象的相关属性,并调用其 Open 或 Execute 方法来完成。

我正在尝试将业务逻辑进行一定程度的封装,以便对其进行单元测试。我已经看到了这个答案,就从表单中抽象逻辑而言,它非常有意义。我想知道数据访问的最佳实践是什么。我的想法是数据模块应该公开一种特定于应用程序的迷你 API(可能带有所有虚拟方法),以便可以用模拟对象替换它们以进行测试。这个其他答案的链接展示了一些示例,让我相信自己走在正确的轨道上,但我仍然有兴趣查看有关数据模块的某种最佳实践文档。我可以通过 Google 找到的大多数页面都提供了相同类型的示例,这些示例介绍了您在设计时可以做的所有很酷的事情,例如将数据绑定控件连接到查询之类的事情,我对这些事情不太感兴趣在这一刻。

4

4 回答 4

8

我个人不是 TDataModule 的粉丝。它对鼓励好的 OO 设计原则几乎没有什么帮助。如果它的全部用途是为 DB 组件提供一个方便的容器,那将是一回事,但它经常成为业务逻辑的垃圾场,在域层中会更好。当这种情况发生时,它最终会成为一个神级和一个依赖磁铁。

再加上一个 bug(或者它的一个特性),至少从Delphi 2开始就一直存在,如果这些数据源位于表单之前未打开的单元中,它会导致表单的数据感知控件丢失其数据源.

我的建议

  • 在 UI 和数据库之间添加域层
  • 将尽可能多的业务逻辑推送到域对象中。
  • 通过使用设计架构模式将决策委托给领域层,使您的 UI 和数据持久层尽可能浅。

如果您不熟悉它,该技术被称为领域驱动设计。它当然不是唯一的解决方案,但它是一个很好的解决方案。基本前提是 UI、业务逻辑和数据库以不同的速度和不同的原因发生变化。因此,让业务逻辑成为问题域的模型,并将其与 UI 和数据库分开。

这如何使我的代码更具可测试性?

通过将业务逻辑移动到它自己的层,您可以在不受 UI 或数据库干扰的情况下对其进行测试。这并不意味着您的代码本身就是可测试的,因为您将它放在自己的层中。使遗留代码可测试是一项艰巨的任务。大多数遗留代码都是紧密耦合的,因此您将花费大量时间将其分解为具有明确定义职责的类。

这是德尔福风格吗?

这取决于你的观点。传统上,大多数 Delphi 应用程序是通过同时开发 UI 和数据库来创建的。在表单设计器上放置一些 db 感知控件。添加/更新带有字段的表以存储控件的数据。使用事件处理程序散布大量的业务逻辑。中提琴!你刚刚烘焙了一个应用程序。对于非常小的应用程序,这可以节省大量时间。但我们不要自欺欺人,小型应用程序往往会变成大型应用程序,这种设计成为不可持续的维护噩梦。

这真的不是语言的错。您可以从数百家 VB、C# 和 Java 商店中找到相同的快速/肮脏/短视的设计。这些类型的应用程序是新手开发人员的结果,他们不了解更多(以及经验丰富的开发人员应该了解更多),IDE 使得它很容易完成,并且压力很大,需要快速完成工作。

Delphi 社区中的一些人(与其他社区一样)长期以来一直在倡导更好的设计技术。

于 2011-02-15T19:40:44.297 回答
7

我认为您需要(事实上,大多数 delphi 数据库开发人员将需要)一个模拟数据集(查询、表等)组件,您可以使用该组件,并在模块初始化时将它们替换为您当前的 ADO 数据集对象到这个模拟数据集,用于测试目的。与其在设计中强制使用接口(这是提供替代能力的一种方式),不如考虑这样一个事实,即通过 Liskov 替代原则,您应该能够(在测试夹具设置时)将模拟的集合注入到您的数据模块中-您要使用的数据集,并在测试执行时将您正在使用的 ADO 数据集简单地替换为一些其他功能等效的实体(模拟数据集或文件支持的表数据集)。

也许您甚至可以从数据模块中完全删除数据集,并在运行时(在您的主应用程序中)将它们连接到正确的 ADO 数据集对象,并在单元测试中附加您的模拟数据集。

由于您没有编写 ADO 数据集,因此不需要对其进行单元测试。但是,模拟这样的数据集可能很困难。

我建议您考虑使用 JvCsvDataSet 或 ClientDataSet 作为夹具(模拟)数据集的基础。然后,您将能够使用这些来确保您的所有数据库平台依赖项(编写远程过程或数据库 SQL 的东西)被抽象到其他类中,您将不得不再次模拟这些类。这样的努力可能不仅需要使您的业务逻辑单元可测试,还可能是朝着在您的业务逻辑中变得对多数据库平台友好的一步。

假设您有一个名为 CustomerQuery 的 ADOQuery,将拖放到数据模块中的对象重命名为 CustomerQueryImpl,并将其添加到数据模块类声明中:

  private
        FCustomerQuery:TADOQuery;

  published
        property CustomerQuery:TADOQuery read FCustomerQuery write FCustomerQuery;

然后在创建事件的数据模块中,将属性连接到对象:

   FCustomerQuery := CustomerQueryImpl

现在您可以编写单元测试,它将在运行时“挂钩”并用自己的测试夹具(模拟对象)替换 CustomerQuery。

于 2011-02-11T18:34:57.570 回答
4

首先,在您更改任何内容之前,您需要进行一些单元测试,以确保您不会破坏任何内容。我会尝试在不更改任何内容的情况下针对当前的 GUI 编写单元测试。DUnit支持GUI 测试(以及传统的单元测试),虽然它有点笨拙并且不能处理模态对话框,但它是功能性的。

接下来,由于您的表单不使用数据感知控件,因此我将通过在表单和现有全局数据模块之间引入另一层数据模块(如果您愿意的话是服务层)来解决此问题。

对于应用程序中的每个表单,我都会创建一个相应的新服务层数据模块。这可能听起来像很多数据模块,但它们非常轻量级,如果需要,您可以稍后合并它们。

如果您喜欢,您可以使用普通的 TObjects 而不是 TDataModules 作为服务层,但是使用数据模块可以让您灵活地稍后在它们上放置非可视组件,例如 TClientDataSet 和 TDataSource 如果您了解数据感知稍后控制路线。

最初,每个服务层数据模块将仅充当访问全局数据模块的代理。您此时的目标只是消除表单对全局数据模块的直接依赖性。

一旦表单仅通过服务层数据模块间接访问全局数据模块,那么我将开始将功能从表单移动到服务层。使用服务层数据模块中的此功能,您会发现为新代码和现有代码编写单元测试要容易得多。

此时,您还可以开始整合执行服务层数据模块。在从表单中提取逻辑完成后,现在合并它们比在该过程中尝试这样做要容易得多。

于 2011-02-11T23:00:25.790 回答
0

请阅读这篇关于单元测试和模拟对象的文章,包括模拟对象理论、本地化 UT 和接口发现。

希望你喜欢它。

于 2011-02-11T20:54:22.790 回答