【问题标题】:Delphi style: How to structure data modules for unit-testable code?Delphi 风格:如何为可单元测试的代码构建数据模块?
【发布时间】:2011-06-25 17:55:05
【问题描述】:

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

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

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

【问题讨论】:

  • 现有表单是使用数据感知控件(TDBEdits)还是标准控件(TEdits)?
  • 目前没有数据感知控件。我一直在考虑。
  • 数据感知控件往往会使应用程序更难进行单元测试。虽然我自己也是他们的粉丝,但我建议你在这个阶段避开他们。

标签: delphi unit-testing refactoring datamodule


【解决方案1】:

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

添加一个错误(或者可能是它的一个功能)至少从Delphi 2 开始一直存在,如果这些数据源位于不存在的单元中,它会导致表单的数据感知控件丢失其数据源在表单之前打开。

我的建议

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

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

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

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

这是 Delphi 风格吗?

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

这真的不是语言的错。您会从数百家 VB、C# 和 Java 商店中找到相同的快速/肮脏/短视的设计。这些类型的应用程序是新手开发人员的成果,他们对此一无所知(而有经验的开发人员则应该更了解),而 IDE 让操作变得如此简单,并且压力很大,需要快速完成工作。

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

【讨论】:

  • 数据模块对“大泥球”设计没有错。数据模块是一个简单的类,可以支持非可视控件。你用它做什么取决于你。如果你把自己挖进坑里,不要责怪那个做铲子的人。
  • 一个糟糕的类比。填坑比挖坑花的时间少。对于设计不佳的应用程序,情况并非如此。我的观点是缺乏封装实际上会鼓励糟糕的设计。同样,双击表单控件以创建事件处理程序的简便性也鼓励将业务逻辑直接耦合到 UI。之所以有这么多蹩脚的代码,是因为这些工具让以这种方式编写代码变得非常容易,并且在某些情况下实际上阻碍了您进行良好的设计。
【解决方案2】:

我认为您需要(事实上,大多数 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。

【讨论】:

    【解决方案3】:

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

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

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

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

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

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

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

    【讨论】:

      【解决方案4】:

      请阅读this article,它关于单元测试和模拟对象,包括模拟对象理论、本地化UT 和接口发现。

      希望你喜欢它。

      【讨论】:

      • 你不需要依赖倒置和接口发现来模拟 Delphi 中的东西。单位名称别名通常就足够了。 Delphi 已经有一个接口部分。您无需 ISomethingEverything 即可使模拟对象(固定装置)工作。
      猜你喜欢
      • 1970-01-01
      • 2013-03-08
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-12-07
      相关资源
      最近更新 更多