Delphi风格:如何为可单元测试的代码构建数据模块?

发布于 2024-10-17 04:20:54 字数 909 浏览 6 评论 0 原文

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

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

我试图将业务逻辑进行一定程度的封装,以便可以对其进行单元测试。我见过这个其他答案的链接显示了一些让我相信的例子我走在正确的道路上,但我仍然有兴趣查看某种有关数据模块的最佳实践文档。我可以通过 Google 找到的大多数页面都提供了相同类型的示例,说明您可以在设计时通过将数据绑定控件连接到查询之类的事情来完成所有很酷的事情,我对此不太感兴趣眼下。

I am looking for some advice about structuring Delphi programs for maintainability. I've come to Delphi programming after a couple of decades of mostly C/C++ though I first learned to program with Turbo Pascal, so I'm not uncomfortable with the basic language. In my previous experience with C++ and C#, I became a TDD convert through using cxxtest and NUnit.

I have inherited this program that I am now responsible for maintaining. It consists mainly of forms, and a couple of data modules. The application business logic and data access is mainly scattered about the forms, and the data modules are mostly just places for global ADO objects to live. The database access is generally done by referring to a global instance of TADOQuery or TADOCommand, formatting SQL text right into the relevant property of the object, and calling its Open or Execute method.

I am trying to get the business logic into a degree of encapsulation where it can be unit-tested. I've seen this answer and it makes perfect sense as far as abstracting logic from forms. I am wondering what the best practices are for data access. My thinking is that the data modules should expose a sort of app-specific mini-API (probably with all virtual methods) so that they can be replaced with mock objects for testing. The link at this other answer shows some examples that lead me to believe I'm on the right track, but I'm still interested in seeing some kind of best practices document about data modules. Most of the pages that I can find through Google present the same kind of examples about all the cool stuff you can do at design time with hooking up data-bound controls to queries and that sort of thing, which I'm not very interested in at the moment.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

佞臣 2024-10-24 04:20:54

就我个人而言,我不喜欢 TDataModule。它对于鼓励良好的面向对象设计原则几乎没有什么帮助。如果它的用途只是一个方便的数据库组件容器,那是一回事,但它常常成为业务逻辑的垃圾场,而业务逻辑在域层中会更好。当这种情况发生时,它最终会成为一个神级和一个依赖磁铁。

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

我的建议

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

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

这如何使我的代码更易于测试?

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

这是Delphi 风格吗?

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

这确实不是语言的错。您会在数百家 VB、C# 和 Java 商店中发现同样的快速/肮脏/短视的设计。这些类型的应用程序是新手开发人员不了解的结果(以及经验丰富的开发人员应该了解更多)的结果,IDE 使工作变得如此容易,并带来了快速完成工作的压力。

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

Personally I'm not a fan of TDataModule. It does very little to encourage good OO design principles. If all it was used for was a convenient container for DB components that would be one thing but far too often it becomes a dumping ground for business logic that would be better off in a domain layer. When this happens it winds up becoming a god class and a dependency magnet.

Add to this a bug (or maybe its a feature) that's continued to exist since at least Delphi 2 that causes a form's data aware controls to lose their data sources if those data sources are located in a unit that isn't opened before the form.

My suggestion

  • Add a domain layer between your UI and your database
  • Push as much of your business logic into domain objects as possible.
  • Make your UI and your data persistence layers as shallow as possible by using design and architectural patterns to delegate decision making to the domain layer.

If you're not familiar with it the technique is referred to as domain driven design. Its certainly not the only solution but its a good one. The basic premise is that the UI, business logic and database change at different rates and for different reasons. So make the business logic a model of the problem domain and keep it separated from the UI and database.

How does this make my code more testable?

By moving the business logic to its own layer you can test it without interference from from either the UI or the database. This doesn't mean your code will be inherently testable simply because you put it in its own layer. Making legacy code testable is a difficult task. Most legacy code is tightly coupled so you will spend a good deal of time pulling it apart into classes with clearly defined responsibilities.

Is this the Delphi style?

That depends on your perspective. Traditionally, most Delphi applications were created by developing the UI and the database in tandem. Drop a few db aware controls on the form designer. Add/update a table with fields to store the control's data. Sprinkle with a liberal amount of business logic using event handlers. Viola! You just baked an application. For very small applications this is a great time saver. But lets not kid ourselves, small applications tend to turn into big ones and this design becomes an unsustainable maintenance nightmare.

This really isn't the fault of the language. You find the same quick/dirty/shortsighted designs from hundreds of VB, C# and Java shops. These kinds of applications are the result of novice developers that don't know any better (and experienced developers that should know better), an IDE that makes it so easy to do and pressure to get the job done quickly.

There are those in the Delphi community (as there are in other communities) that have been advocating better design techniques for a long time.

南汐寒笙箫 2024-10-24 04:20:54

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

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

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

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

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

  private
        FCustomerQuery:TADOQuery;

  published
        property CustomerQuery:TADOQuery read FCustomerQuery write FCustomerQuery;

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

   FCustomerQuery := CustomerQueryImpl

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

I think you need (and in fact, most delphi database developers are going to need) a Mock Dataset (Query, table, etc etc) Component that you could use, and substitute them at module-init time, for your current ADO dataset objects to this mock dataset, for test purposes. Instead of forcing Interfaces into your design, which are one way to provide a substitution capability, consider the fact that by Liskov substitution principle, you should be able to (at test fixture setup time), inject into your data module, the set of mock-datasets that you want to use, and simply replace the ADO datasets that you are using, at test execution time, with some other functionally equivalent entity (a mock dataset, or a file-backed table dataset).

Perhaps you could even remove the datasets completely from the data module, and have them hooked up at runtime (in your main
application) to the correct ADO dataset objects, and in unit tests, attach your mock datasets.

Since you did not write the ADO dataset, you don't need to unit test it. However, mocking up such a dataset might be difficult.

I would suggest you consider using a JvCsvDataSet or a ClientDataSet as the basis for your fixture (mock) datasets. You would be able to then use these to make sure that all your database platform dependencies (stuff that writes remote procedures or database SQL) is abstracted out into other classes, which again you are going to have to mock up. Such an effort might not only be required to make your business logic unit testable, it might also be a step towards becoming multiple-database-platform friendly in your business logic.

imagine you have an ADOQuery called CustomerQuery, rename the object that you dropped onto your data module, to CustomerQueryImpl, and add this to your data module class declaration:

  private
        FCustomerQuery:TADOQuery;

  published
        property CustomerQuery:TADOQuery read FCustomerQuery write FCustomerQuery;

then in your data module on create event, hook up the property to the objects:

   FCustomerQuery := CustomerQueryImpl

Now you can write unit tests, which will 'hook' in and replace CustomerQuery with its own test fixture (mock object) at runtime.

划一舟意中人 2024-10-24 04:20:54

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

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

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

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

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

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

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

Firstly before you change anything you need some unit tests so you can ensure you don't break anything. I would attempt to write unit tests against the current GUI without changing anything. DUnit has support for GUI testing (along with traditional unit testing) and although it's a little clunky and can't handle modal dialogs it is functional.

Next, since your forms don't use data aware controls I would approach this by introducing another layer of data modules, a service layer if you will, between the forms and the existing global data modules.

For every form in your application I would create a corresponding new service layer data module. This may sound like a lot of data modules but they're very lightweight and you can consolidate them later if you want.

You could use ordinary TObjects rather than TDataModules for the service layer if you liked however using data modules gives you the flexibility of being able to place non-visual components on them later, for example a TClientDataSet and TDataSource if you went down the data-aware controls route at a later date.

Initially each service layer data module would merely act as a proxy for accessing the global data modules. Your goal at this point would be simply to remove the direct dependency of the forms on the global data modules.

Once the forms only indirectly accessed the global data modules via the service layer data modules then I would start to move functionality from the forms into the service layer. With this functionality in the service layer data modules you will find it much easier to write unit tests for new and existing code.

At this point you could also start consolidating the per-form service layer data modules. It will be much easier to consolidate them now after the logic extraction from the forms is complete than if you try do do it during that process.

長街聽風 2024-10-24 04:20:54

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

希望你喜欢它。

Please read this article, its about Unit Testing and Mock Objects including the theory of mock objects, localizing UT and interfaces discovery.

hope you enjoy it.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文