我最近在谷歌测试博客中偶然发现了关于编写更多可测试代码的指南.在此之前,我与作者达成了一致意见:
优先于条件的多态性:如果你看到一个switch语句,你应该考虑多态性.如果您在班级的许多地方看到相同的if条件,您应该再次考虑多态性.多态性会将您的复杂类分解为几个较小的简单类,这些类可以清楚地定义代码的哪些部分相关并一起执行.这有助于测试,因为更简单/更小的类更容易测试.
我根本无法绕过那个.我可以理解使用多态而不是RTTI(或DIY-RTTI,视情况而定),但这看起来像是一个广泛的陈述,我无法想象它实际上在生产代码中被有效使用.在我看来,更容易为具有switch语句的方法添加其他测试用例,而不是将代码分解为几十个单独的类.
此外,我的印象是多态可能导致各种其他微妙的错误和设计问题,所以我很想知道这里的权衡是否值得.有人可以向我解释这个测试指南的确切含义吗?
实际上,这使得测试和代码更容易编写.
如果你有一个基于内部字段的switch语句,你可能在多个地方有相同的开关做一些不同的事情.当您添加新案例时,这会导致问题,因为您必须更新所有switch语句(如果可以找到它们).
通过使用多态,您可以使用虚函数来获得相同的功能,因为新案例是一个新类,您不必在代码中搜索需要检查的内容,它们对于每个类都是隔离的.
class Animal { public: Noise warningNoise(); Noise pleasureNoise(); private: AnimalType type; }; Noise Animal::warningNoise() { switch(type) { case Cat: return Hiss; case Dog: return Bark; } } Noise Animal::pleasureNoise() { switch(type) { case Cat: return Purr; case Dog: return Bark; } }
在这个简单的情况下,每个新的动物原因都需要更新两个switch语句.
你忘了一个?什么是默认值?砰!!
使用多态
class Animal { public: virtual Noise warningNoise() = 0; virtual Noise pleasureNoise() = 0; }; class Cat: public Animal { // Compiler forces you to define both method. // Otherwise you can't have a Cat object // All code local to the cat belongs to the cat. };
通过使用多态,您可以测试Animal类.
然后分别测试每个派生类.
此外,您还可以将Animal类(已关闭以进行更改)作为二进制库的一部分发送.但是人们仍然可以通过派生从Animal头派生的新类来添加新动物(Open for extension).如果在Animal类中捕获了所有这些功能,则需要在发货前定义所有动物(关闭/关闭).
我想你的问题在于熟悉,而不是技术.熟悉C++ OOP.
在其多种范例中,它具有OOP功能,并且能够支持与大多数纯OO语言的比较.
不要让"C++内部的C部分"让你相信C++不能处理其他范式.C++可以非常慷慨地处理很多编程范例.其中,OOP C++是程序范式之后最成熟的C++范例(即前面提到的"C部分").
没有"微妙的错误"或"不适合生产代码"的事情.有些开发人员会按照自己的方式进行设置,开发人员将学习如何使用工具并为每项任务使用最佳工具.
...但是多态性消除了大多数错误.
区别在于您必须手动处理这些开关,而一旦您使用继承方法覆盖,多态性就更自然了.
使用开关,您必须将类型变量与不同类型进行比较,并处理差异.使用多态,变量本身就知道如何表现.您只需要以逻辑方式组织变量,并覆盖正确的方法.
但最后,如果你忘记在switch中处理一个case,编译器就不会告诉你,而你会被告知你是否从一个类派生而不是覆盖它的纯虚方法.因此避免了大多数开关错误.
总而言之,这两个特征是关于做出选择.但是,多态性使您能够更复杂,同时更自然,更容易选择.
RTTI是一个有趣的概念,可能很有用.但是大多数时候(即95%的时间),方法重写和继承都会绰绰有余,而且大多数代码甚至不知道所处理对象的确切类型,而是相信它能做正确的事情.
如果您使用RTTI作为美化开关,那么您就错过了这一点.
(免责声明:我非常喜欢RTTI概念和dynamic_casts.但是必须使用正确的工具来完成手头的任务,并且大多数时候RTTI被用作美化开关,这是错误的)
如果您的代码在编译时不知道对象的确切类型,那么使用动态多态(即经典继承,虚方法覆盖等)
如果您的代码在编译时知道类型,那么也许您可以使用静态多态,即CRTP模式http://en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern
CRTP将使您拥有类似于动态多态的代码,但其每个方法调用将被静态解析,这对于一些非常关键的代码是理想的.
在生产中使用与此类似的代码(来自存储器).
更简单的解决方案围绕一个由消息循环调用的过程(Win32中的WinProc,但为了简单起见,我写了一个更简单的版本).总结一下,它是这样的:
void MyProcedure(int p_iCommand, void *p_vParam) { // A LOT OF CODE ??? // each case has a lot of code, with both similarities // and differences, and of course, casting p_vParam // into something, depending on hoping no one // did a mistake, associating the wrong command with // the wrong data type in p_vParam switch(p_iCommand) { case COMMAND_AAA: { /* A LOT OF CODE (see above) */ } break ; case COMMAND_BBB: { /* A LOT OF CODE (see above) */ } break ; // etc. case COMMAND_XXX: { /* A LOT OF CODE (see above) */ } break ; case COMMAND_ZZZ: { /* A LOT OF CODE (see above) */ } break ; default: { /* call default procedure */} break ; } }
每增加一个命令就增加了一个案例.
问题在于某些命令类似,并且部分地共享它们的实现.
因此混合病例是进化的风险.
我通过使用Command模式解决了这个问题,即使用一个process()方法创建一个基本Command对象.
因此,我重新编写了消息过程,将危险代码(即使用void*等)最小化,并编写它以确保我永远不需要再次触摸它:
void MyProcedure(int p_iCommand, void *p_vParam) { switch(p_iCommand) { // Only one case. Isn't it cool? case COMMAND: { Command * c = static_cast(p_vParam) ; c->process() ; } break ; default: { /* call default procedure */} break ; } }
然后,对于每个可能的命令,不是在过程中添加代码,而是混合(或者更糟,复制/粘贴)来自类似命令的代码,我创建了一个新命令,并从Command对象派生它,或者它的派生对象:
这导致了层次结构(表示为树):
[+] Command | +--[+] CommandServer | | | +--[+] CommandServerInitialize | | | +--[+] CommandServerInsert | | | +--[+] CommandServerUpdate | | | +--[+] CommandServerDelete | +--[+] CommandAction | | | +--[+] CommandActionStart | | | +--[+] CommandActionPause | | | +--[+] CommandActionEnd | +--[+] CommandMessage
现在,我需要做的就是覆盖每个对象的进程.
简单,易于扩展.
例如,假设CommandAction应该分三个阶段完成它的过程:"before","while"和"after".它的代码如下:
class CommandAction : public Command { // etc. virtual void process() // overriding Command::process pure virtual method { this->processBefore() ; this->processWhile() ; this->processAfter() ; } virtual void processBefore() = 0 ; // To be overriden virtual void processWhile() { // Do something common for all CommandAction objects } virtual void processAfter() = 0 ; // To be overriden } ;
例如,CommandActionStart可以编码为:
class CommandActionStart : public CommandAction { // etc. virtual void processBefore() { // Do something common for all CommandActionStart objects } virtual void processAfter() { // Do something common for all CommandActionStart objects } } ;
正如我所说:易于理解(如果评论正确),并且非常容易扩展.
交换机减少到最低限度(即if-like,因为我们仍然需要将Windows命令委托给Windows默认程序),并且不需要RTTI(或者更糟糕的是,内部RTTI).
我认为(如果只是根据我在工作中的应用程序中看到的"历史"代码的数量来判断)交换机内的相同代码会非常有趣.
单元测试OO程序意味着将每个类作为一个单元进行测试.您想要学习的原则是"开放扩展,关闭修改".我是从Head First Design Patterns那里得到的.但它基本上表示您希望能够轻松扩展代码而无需修改现有的测试代码.
多态性通过消除那些条件语句使这成为可能.考虑这个例子:
假设你有一个携带武器的角色对象.您可以编写这样的攻击方法:
If (weapon is a rifle) then //Code to attack with rifle else If (weapon is a plasma gun) //Then code to attack with plasma gun
等等
通过多态,角色不必简单地"知道"武器的类型
weapon.attack()
会工作.如果发明新武器会发生什么?如果没有多态,则必须修改条件语句.使用多态性,您将不得不添加一个新类并单独保留测试的Character类.
我有点怀疑:我相信继承通常会增加复杂性而不是删除.
不过,我认为你提出了一个很好的问题,我考虑的一点是:
你是否因为处理不同的事情而分成多个班级?或者它是否真的是一回事,以不同的方式行事?
如果它真的是一种新类型,那么继续创建一个新类.但如果它只是一个选项,我通常将它保持在同一个类中.
我认为默认解决方案是单类解决方案,并且程序员提出继承以证明其案例.
不是测试用例影响的专家,而是从软件开发的角度来看:
开放封闭原则 - 课程应该关闭以进行更改,但可以扩展.如果您通过条件构造管理条件操作,那么如果添加了新条件,则您的类需要更改.如果使用多态,则基类不需要更改.
不要重复自己 - 指南的一个重要部分是" 相同条件".这表明您的类具有一些可以在类中考虑的不同操作模式.然后,该条件出现在代码中的一个位置 - 当您为该模式实例化对象时.而且,如果出现一个新的,你只需要改变一段代码.