C++ 多态性或条件是否有助于更好的设计?
我最近偶然发现了编写更多可测试代码的指导原则。在这一点之前,我一直同意作者的观点: 支持多态性而不是条件:如果您看到switch语句,您应该考虑多态性。如果你看到相同的If条件在你班上的许多地方重复出现,你应该再次考虑多态性。多态性将把您的复杂类分解为几个更小更简单的类,这些类清楚地定义了哪些代码片段是相关的并一起执行。这有助于测试,因为更简单/更小的类更容易测试 我简直无法相信这一点。我可以理解使用多态性代替RTTI(或DIY-RTTI,视情况而定),但这似乎是一个如此宽泛的陈述,我无法想象它实际被有效地用于生产代码中。相反,在我看来,为具有switch语句的方法添加额外的测试用例比将代码分解成几十个单独的类更容易C++ 多态性或条件是否有助于更好的设计?,c++,oop,tdd,polymorphism,C++,Oop,Tdd,Polymorphism,我最近偶然发现了编写更多可测试代码的指导原则。在这一点之前,我一直同意作者的观点: 支持多态性而不是条件:如果您看到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类中捕获的,那么所有动物都需要在发布之前定义(关闭/关闭)。多态性是OO的基石之一,当然非常有用。 通过将关注点划分到多个类上,您可以创建独立的可测试单元。 因此,在这种情况下,您可以创建一个具有多个实现的统一接口,而不是在多个不同类型或实现上调用方法。 当您需要添加一个实现时,您不需要修改客户端,就像switch…case一样。这非常重要,因为这有助于避免回归 您还可以通过只处理一种类型来简化客户端算法:接口
对我来说非常重要的是,多态性最好与纯粹的接口/实现模式结合使用(如古老的Shape面向对象程序的单元测试意味着将每个类作为一个单元进行测试。你想学习的一个原则是“开放扩展,封闭修改”.我从Head-First设计模式中得到了这一点。但它基本上说,您希望能够在不修改现有测试代码的情况下轻松扩展代码
多态性通过消除这些条件语句使这成为可能。 假设您有一个携带武器的角色对象。您可以编写如下攻击方法:
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()
会起作用。如果发明了一种新武器,会发生什么?如果没有多态性,您将不得不修改您的条件语句。使用多态性,您将不得不添加一个新类,而不用处理测试的字符类。不是测试用例含义方面的专家,但从软件开发的角度来看:
- 打开-关闭原则--类应该关闭以进行更改,但可以扩展。如果通过条件构造管理条件操作,则如果添加了新条件,则类需要更改。如果使用多态性,则基类不需要更改
- 不要重复自己的话——准则的一个重要部分是“如果条件相同”这表明你的类有一些不同的操作模式,可以分解成一个类。然后,当你为该模式实例化对象时,该条件出现在代码中的一个地方。同样,如果出现一个新的操作模式,你只需要更改一段代码
这主要是与知识的封装有关的。让我们从一个非常明显的例子开始——toStRew()。这是java,但是很容易转移到C++。假设你想打印一个友好的对象版本,以调试目的。你可以这样做:
switch(obj.type): {
case 1: cout << "Type 1" << obj.foo <<...; break;
case 2: cout << "Type 2" << ...
开关(对象类型):{
案例1:cout这实际上取决于您的编程风格。虽然这在Java或C#中可能是正确的,但我不同意自动决定使用多态性是正确的。您可以将代码拆分为许多小函数,并使用函数指针执行数组查找(在编译时初始化)在C++中,多态和类经常被滥用——可能是来自强大的OOP语言的人在C++中最大的设计错误是,所有的东西都进入一个类,这不是真的。一个类应该只包含一个最小的集合,使它作为一个整体工作。如果一个子类或朋友是必须的,那么我就是。t、 但它们不应该成为标准,任何其他的
cout << object.toString();
interface A{
int foo();
}
final class B implements A{
int foo(){ print("B"); }
}
final class C implements A{
int foo(){ print("C"); }
}
class A{
int foo(){print("A");}
}
class B extends A{
int foo(){print("B");}
}
class C extends B{
int foo(){print("C");}
}
...
class Z extends Y{
int foo(){print("Z");
}
main(){
F* f = new Z();
A* a = f;
a->foo();
f->foo();
}
A* a = new Z;
A a2 = *a;
a->foo();
a2.foo();
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 ;
}
}
void MyProcedure(int p_iCommand, void *p_vParam)
{
switch(p_iCommand)
{
// Only one case. Isn't it cool?
case COMMAND:
{
Command * c = static_cast<Command *>(p_vParam) ;
c->process() ;
}
break ;
default: { /* call default procedure */} break ;
}
}
[+] Command
|
+--[+] CommandServer
| |
| +--[+] CommandServerInitialize
| |
| +--[+] CommandServerInsert
| |
| +--[+] CommandServerUpdate
| |
| +--[+] CommandServerDelete
|
+--[+] CommandAction
| |
| +--[+] CommandActionStart
| |
| +--[+] CommandActionPause
| |
| +--[+] CommandActionEnd
|
+--[+] CommandMessage
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
} ;
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
}
} ;
switch (type)
{
case T_FOO: doFoo(); break;
case T_BAR: doBar(); break;
default:
fprintf(stderr, "You, who are reading this, add a new case for %d to the FooBar function ASAP!\n", type);
assert(0);
}