如何模拟一个没有虚方法的类?

8

假设您有一个非常好设计的Delphi项目,遵循依赖注入和其他一些良好的实践。

现在假设您需要模拟定义为:

TMyClass = class
public
  procedure Method1;
  procedure Method2
end;  
Method1Method2不是虚方法。在这种情况下,我们需要模拟一个对象,我们需要继承它并且覆盖想要模拟的每个方法,但在这种情况下不可能,因为它们不是virtual。我应该改变源代码,在每个需要模拟的方法上添加virtual吗?这不好吧? 编辑: 我在考虑创建一个编译器指令,使类中的所有字段都成为virtual,这是一个好主意吗?只有我的测试套件会设置编译器指令。 编辑2*: Embarcadero 应该提供一种简单的方法来更改类的方法指针到另一个方法指针,而不需要virtual

1
你的编辑是一个完全独立的问题。请在新问题中提出它。但在此之前,请查看访问私有字段如何使用DUnit测试私有方法? - Rob Kennedy
抱歉,应该是 virtual 而不是 public - Rafael Colucci
2
@Rafael,关于你的EDIT2,这正是virtual所能实现的,为什么要用某些东西来混淆15年来使用virtual的经验呢?我相信有一个正则表达式可以将所有非虚拟声明替换为virtual; {temp},也有一个正则表达式可以将virtual {temp}替换回只有; - Johan
你嘲笑它的静态做事方式。 - Johan
@Rafael,英语双关语:to mock = 嘲笑,取笑;virtual的反义词= static(有点);method = 做事的方式 {我知道...很无聊} - Johan
@Johan,哈哈..现在我明白了。我学到了新东西! - Rafael Colucci
3个回答

12

将方法设为虚函数,以便您可以模拟它们。(它们不需要是抽象的。)

如果您无法这样做,则使用另一个类来包装该类。让包装器的方法为虚函数,在默认实现中,只需将调用转发到原始类。无论您的程序在哪里使用原始类,都将其替换为包装器。现在,模拟包装器。


嗯...这有点复杂。我喜欢包装类的想法,但改变所有源代码会非常困难。 - Rafael Colucci
1
我的项目有超过200个类,这是一个10年的老项目。相信我:按照你告诉我的方式改变源代码是不可能的。而且,如果我只想模拟一个类,这可能很容易,但是如果需要模拟10个或20个呢? - Rafael Colucci
是的,但是进行所有更改所需的努力并不相同。 - Rafael Colucci
我认为这并不需要太多的工作量。如果IDE无法为您进行更改,那么一个简单的sed脚本应该能够在几分钟内完成所有更改,如果不起作用,请从源代码控制中恢复并重试。但是,如果您仍然不喜欢包装类的工作量,那么您可以选择我的第一个建议,只需将方法设置为虚拟即可。这只需要更改一个文件。 - Rob Kennedy
根据我的经验,模拟需要使用接口或虚方法。 - Robert Love
显示剩余7条评论

6
为了模拟一个对象,我们需要继承它,但在这种情况下不可能实现。我建议每个需要模拟的类都应该基于一个接口。换句话说,如果您有需要被模拟的方法,它们应该被放入一个接口中,然后要被模拟的类就实现该接口。然后,通过在模拟对象中实现相同的接口来创建模拟。另一种选择是使用模拟库。您可以查看以下问题:What is your favorite Delphi mocking library?和这个框架也包括模拟对象:http://code.google.com/p/emballo/模拟对象还应该鼓励您在代码中正确使用依赖注入。

1
我同意应该使用接口,但至少不要使用Interface关键字,如果你需要避免引用计数的话。Delphi的Interface关键字自动要求使用引用计数,这是一个主要的缺点。请注意。 - Warren P
2
@Warren:在Delphi的interface关键字中,没有使用refcounting的“要求”。您可以简单地在接口的_AddRef和_Release方法中返回-1,以禁用refcounting机制。只是请注意,当您还有使用refcounting进行生命周期管理的接口对象时,您会引入歧义。 - Marjan Venema
1
@Marjan,这些函数的返回值对引用计数绝对没有影响。你通过实现这些函数并简单地不计算引用来禁用引用计数。但是无论这些函数返回什么,以及在这些函数中你做或不做什么其他事情,它们仍然总是被调用,因此即使你不计算引用,仍然需要管理引用。如果你在任何地方手动释放一个对象,而该对象仍有接口引用存在,你的程序最终将崩溃。 - Rob Kennedy
Marjan:我认为这种歧义更准确地称为“说谎”。那些提供IUnknown接口但在没有引用计数的情况下可能被随机释放的东西,是一片混乱。TANSTAAFL。我不相信欺骗别人。如果你实现了IUnknown,并且只在应用程序终止之前的某个时刻释放对象,你将面临巨大的问题风险。不仅仅是歧义。还有崩溃。 - Warren P
@WarrenP:如果你想“禁用”引用计数,只需调用一次_add和_release。这样可以保持引用计数为1,并使您能够在任何时候释放。 - Brave Cobra
显示剩余3条评论

5

不仅要使方法虚拟,还应该声明一个纯虚基类,并且让其他类只使用纯虚基类名称。这样,您就可以使用我们所谓的“Liskov替换原则”,并且可以创建所需数量的抽象基类的具体子类型。由于抽象基类就像Delphi中的接口一样运作(减去引用计数部分),因此您可以在两个世界中同时获得最佳结果。通过这种方式,您真正可以保持应用程序代码简单,并减少不良耦合,同时获得可单元测试的“组合对象”,并自己组装。

// in UnitBase.pas
TMyClassBase = class
public
  procedure Method1; virtual; abstract;
  procedure Method2; virtual; abstract;
end; 

// in UnitReal.pas
TMyClassReal = class(TMyClassbase)
public
  procedure Method1; override;
  procedure Method2; override;
end; 

// in UnitMock.pas 
TMyClassMock = class(TMyClassbase)
public
  procedure Method1; override;
  procedure Method2; override;
end; 

在使用"TMyClass"的地方,将其更改为使用TMyClassbase:

TMyOtherClass = class(TMyOtherClassBase)
private
  FMyThing:TMyClassbase;
public
  property MyThing:TMyClassBase read FMyThing write FMyThing;
end;

通过在运行时连接TMyOtherclass,您可以决定是使用真实的还是模拟的类:

// in my realapp.pas
MyOtherClassObj :=   TMyotherClass.Create;
MyOtherClassObj.MyThing :=  TMyOtherClassReal.Create;  // real object

// in my unittest.pas
MyOtherClassObj :=   TMyotherClass.Create;
MyOtherClassObj.MyThing :=  TMyOtherClassMock.Create;  // mock object

别忘了设置方法来释放 MyOtherClassObj.MyThing,否则就会有内存泄漏。


2
制作抽象基类有什么优势?您提到了具有多个子类型,但应用程序已经可以在没有子类型的情况下工作,那么为什么要费力引入更多继承呢? - Rob Kennedy
@Warren,谢谢,但我想知道拥有这样的东西有什么好处。 - Rafael Colucci
这个想法很好,但现在对我来说无法实现。但我会将其保留以供未来新应用程序使用。谢谢分享。 - Rafael Colucci
使用这种方式相比使用“真正的”接口有什么优势?据我所见,您的抽象基类可以被接口替代。这样,您就可以消除一层依赖关系了。 - Undreren
在 Delphi 中,如果您不想实现引用计数语义,而是想要手动分配和释放对象以及精确描述对象生命周期,则纯虚基类是更清晰的解决方案。虽然可以从接口实现中删除引用计数,但这很丑陋,不应该这样做。 - Warren P
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接