多态性或条件是否促进更好的devise?

我最近在googletesting博客中偶然发现了关于编写更多可testing代码的指导原则。 直到现在,我还是同意这位作者:

满足条件的多态性:如果你看到一个switch语句,你应该考虑多态性。 如果你看到相同的条件,在你class里的许多地方重复,你应该再次考虑多态性。 多态性将把你复杂的类分成几个更小的更简单的类,它们清楚地定义哪些代码片段是相关的并且一起执行。 这有助于testing,因为更简单/更小的类更容易testing。

我根本无法把头围住。 我可以理解使用多态而不是RTTI(或DIY-RTTI,视情况而定),但是这似乎是一个如此广泛的陈述,我无法想象它实际上在生产代码中被有效使用。 在我看来,为具有switch语句的方法添加额外的testing用例会比较容易,而不是将代码分解成几十个单独的类。

另外,我的印象是,多态性可能导致各种其他微妙的错误和devise问题,所以我很想知道这里的权衡是否值得。 有人可以向我解释这个testing指南是什么意思吗?

其实这使得testing和代码编写起来更容易。

如果你有一个基于内部字段的switch语句,你可能会在多个地方使用相同的switch来做一些稍微不同的事情。 由于必须更新所有switch语句(如果可以find它们),因此在添加新案例时会导致问题。

通过使用多态性,你可以使用虚函数来获得相同的function,因为新的案例是一个新的类,你不必search你的代码,需要检查的东西,它是所有类都孤立。

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; } } 

在这种简单的情况下,每个新的动物原因都需要更新开关语句。
你忘了一个? 什么是默认? 砰!!

使用多态性

 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. }; 

通过使用多态可以testingAnimal类。
然后分别testing每个派生类。

此外,这允许您将动物类( 封闭的更改 )作为二进制库的一部分。 但是,人们仍然可以通过派生从Animal头部派生的新类来添加新的动物( Open for extension )。 如果所有这些function都被捕获到Animal类中,那么所有的动物都需要在运输之前定义(闭合/closures)。

不要害怕…

我想你的问题在于熟悉,而不是技术。 熟悉C ++ OOP。

C ++是一种OOP语言

在其多种范例中,它具有OOPfunction,并且能够支持与最纯粹的OO语言进行比较。

不要让“C ++里面的C部分”让你相信C ++不能处理其他的范例。 C ++可以很好地处理很多编程范例。 其中,OOP C ++是程序范式(即前面提到的“C部分”)之后最成熟的C ++范例。

多晶现象对于生产是可以的

没有“微妙的错误”或“不适合生产的代码”的东西。 有一些开发人员仍然坚持自己的方式,开发人员将学习如何使用工具,并为每项任务使用最好的工具。

开关和多态性[几乎]类似…

…但多态消除了大多数错误。

不同之处在于,您必须手动处理这些开关,而多态性更自然,一旦您使用inheritance方法重写。

使用开关,您必须比较不同types的typesvariables,并处理差异。 使用多态性,variables本身知道如何performance。 您只需以逻辑方式组织variables,并覆盖正确的方法。

但是最终,如果你忘记在switch中处理一个case,编译器不会告诉你,而你会被告知你是否从一个类派生而不重写它的纯虚方法。 因此避免了大多数开关错误。

总而言之,这两个特点是关于做出select的。 但是多态性使你能够做出更复杂,同时更自然的select。

避免使用RTTI来查找对象的types

RTTI是一个有趣的概念,可以是有用的。 但大多数情况下(即95%的时间),方法覆盖和inheritance将会绰绰有余,大部分代码甚至不应该知道处理对象的确切types,而是相信它做正确的事情。

如果你使用RTTI作为荣耀的开关,你就错过了这一点。

(免责声明:我是RTTI概念和dynamic_casts的忠实拥趸,但是我们必须使用正确的工具来完成任务,而RTTI大部分时间都被用作荣耀的开关,这是错误的)

比较dynamic和静态多态性

如果您的代码在编译时不知道对象的确切types,那么使用dynamic多态(即经典inheritance,虚拟方法覆盖等)

如果你的代码在编译时知道types,那么也许你可以使用静态多态,也就是CRTP模式http://en.wikipedia.org/wiki/Curiously_Recurring_Template_Pattern

CRTP将使您能够拥有像dynamic多态性那样的代码,但是每个方法调用都将静态地parsing,这对于一些非常关键的代码来说是非常理想的。

生产代码示例

在生产中使用类似于这个(从内存)的代码。

更简单的解决scheme是围绕着一个由消息循环(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 ; } } 

每增加一个命令都会添加一个case。

问题是有些命令在类似的地方,部分地分享了它们的实现。

所以混合病例是一种进化风险。

我通过使用Command模式解决了问题,即使用一个process()方法创build一个基本的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<Command *>(p_vParam) ; c->process() ; } break ; default: { /* call default procedure */} break ; } } 

然后,对于每个可能的命令,而不是在过程中添加代码,并混合(或更糟,复制/粘贴)来自类似命令的代码,我创build了一个新的命令,并从Command对象或其衍生对象:

这导致了层次结构(以树的forms表示):

 [+] Command | +--[+] CommandServer | | | +--[+] CommandServerInitialize | | | +--[+] CommandServerInsert | | | +--[+] CommandServerUpdate | | | +--[+] CommandServerDelete | +--[+] CommandAction | | | +--[+] CommandActionStart | | | +--[+] CommandActionPause | | | +--[+] CommandActionEnd | +--[+] CommandMessage 

现在,我所需要做的就是覆盖每个对象的进程。

简单,易于扩展。

例如,CommandAction应该分三个阶段进行:“之前”,“同时”和“之后”。 它的代码会是这样的:

 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 } } ; 

正如我所说:容易理解(如果评论得当),而且很容易扩展。

交换机减less到最低限度(例如,因为我们仍然需要将Windows命令委托给Windows默认过程),并且不需要RTTI(或者更糟,内部RTTI)。

在一个开关内部的相同的代码将是相当有趣的,我想(如果只是根据我在我们的应用程序在工作中看到的“历史”代码量)。

unit testing面向对象程序意味着将每个类作为一个单元进行testing。 你想学习的一个原则是“开放延伸,closures修改”。 我从Head First Design Patterns中得到了这个。 但它基本上说,你想有能力轻松扩展你的代码,而无需修改现有的testing代码。

多态性通过消除这些条件语句使得这成为可能。 考虑这个例子:

假设你有一个携带武器的angular色对象。 你可以写这样的攻击方法:

 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 

等等

使用多态性,angular色不需要简单地“知道”武器的types

 weapon.attack() 

会工作。 如果发明了新武器会发生什么? 没有多态性,你将不得不修改你的条件语句。 有了多态性,你将不得不添加一个新的类,而只保留被testing的Character类。

我有点怀疑:我认为inheritance往往会增加复杂性而不是消除。

不过,我想你是在问一个很好的问题,我考虑的一件事是这样的:

你是否因为处理不同的事情而分裂成多个class级? 或者,是不是以同样的方式行事

如果它真的是一个新的types ,那么继续创build一个新的类。 但如果只是一种select,我通常会把它放在同一个class级。

我相信默认的解决scheme是单一的解决scheme,程序员提出inheritance来certificate他们的情况。

不是testing用例影响的专家,而是从软件开发的angular度来看:

  • 开放原则 – 课程应该closures,但可以延期。 如果您通过条件构造来pipe理条件操作,那么如果添加新条件,则需要更改类。 如果你使用多态,基类不需要改变。

  • 不要重复自己 – 指南的一个重要部分是“ 同样的条件”。 这表明你的class级有一些独特的操作模式,可以被分解成一个class级。 然后,当您为该模式实例化对象时,该条件出现在您的代码中的一个位置。 再一次,如果有新的,你只需要改变一个代码。

多态性是面向对象的angular色之一,当然是非常有用的。 通过将关注点分为多个类别,您可以创build独立和可testing的单元。 所以,而不是做一个开关…的情况下,你调用方法在几个不同的types或实现你创build一个统一的接口,有多个实现。 当你需要添加一个实现时,你不需要修改客户端,就像switch … case一样。 非常重要,因为这有助于避免回归。

您也可以通过处理一种types来简化客户端algorithm:接口。

对我来说非常重要的是,多态性最好用于纯粹的接口/实现模式(如古老的Shape < – Circle等)。 您也可以使用模板方法(也称为钩子)在具体类中使用多态,但随着复杂性的增加,其有效性会降低。

多态性是我们公司的代码库build立的基础,所以我认为它非常实用。

开关和多态性做同样的事情。

在多态性(以及一般的基于类的编程)中,按照types对函数进行分组。 使用开关时,可以按function分组。 决定哪个视angular对你有好处。

所以如果你的界面是固定的,你只能添加新的types,多态是你的朋友。 但是,如果你添加新的function到你的界面,你将需要更新所有的实现。

在某些情况下,你可能有一个固定数量的types,新的function可以来,然后交换机更好。 但是添加新的types可以让您更新每个开关。

使用开关,您正在复制子types列表。 使用多态性,您正在复制操作列表。 你交换了一个问题来得到一个不同的问题。 这就是所谓的expression式问题 ,我不知道任何编程范例。 问题的根源在于用来表示代码的文本的一维性质。

由于亲多态性点在这里很好地讨论,让我提供一个亲切换点。

面向对象的devise模式,以避免常见的陷阱。 程序化编程也有devise模式(但是还没有人把它写下来,但是AFAIK,我们需要另外一个新的“帮派”编写一本畅销书)。 一种devise模式可能总是包括一个默认情况

开关可以正确完成:

 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); } 

这段代码会将您最喜欢的debugging器指向您忘记处理案例的位置。 编译器可以强制你实现你的接口,但这迫使你彻底地testing你的代码(至less看到新的情况被注意到)。

当然,如果一个特定的开关将被用于多个地方,它将被切换成一个function( 不要重复自己 )。

如果你想扩展这些开关,只需要grep 'case[ ]*T_BAR' rn . (在Linux上),它会吐出值得一看的位置。 既然你需要看代码,你会看到一些上下文帮助你正确地添加新的情况。 当你使用多态时,调用站点被隐藏在系统内部,并且你依赖于文档的正确性(如果它存在的话)。

扩展交换机也不会破坏OCP,因为您不会改变现有的情况,只需添加一个新的情况即可。

交换机也帮助下一个试图习惯和理解代码的人:

  • 可能的情况在眼前。 阅读代码时,这是一件好事(less跳跃)。
  • 但是虚拟方法调用就像普通的方法调用一样。 人们永远不会知道一个电话是虚拟的还是正常的(不需要查看课程)。 那很糟。
  • 但是如果调用是虚拟的,可能的情况并不明显(没有find所有的派生类)。 这也不好。

当你向第三方提供接口时,他们可以将行为和用户数据添加到系统中,那么这是另一回事。 (他们可以设置callback和指向用户数据,并给他们处理)

进一步的辩论可以在这里find: http : //c2.com/cgi/wiki?SwitchStatementsSmell

恐怕我的“C黑客综合症”和反OOP主义最终将在这里烧毁我所有的声望。 但是无论何时我需要或者不得不将某些东西插入到程序化的C系统中,我都觉得这很容易,缺less约束,强制封装和更less的抽象层让我“做到了”。 但是在一个C ++ / C#/ Java系统中,在软件的整个生命周期中,有数十个抽象层堆叠在一起,我需要花费很多时间来研究如何正确解决其他程序员的所有约束和限制build立在他们的系统,以避免他人“搞乱他们的课堂”。

这主要是与知识的封装有关。 让我们从一个非常明显的例子开始 – toString()。 这是Java,但很容易转移到C ++。 假设你想打印一个人性化的对象版本来进行debugging。 你可以这样做:

 switch(obj.type): { case 1: cout << "Type 1" << obj.foo <<...; break; case 2: cout << "Type 2" << ... 

这显然是愚蠢的。 为什么某个方法需要知道如何打印一切。 对象本身往往会更好地知道如何打印自己,例如:

 cout << object.toString(); 

这样的toString()可以访问成员字段,而不需要强制转换。 他们可以独立testing。 他们可以很容易地改变。

然而,你可能会争辩说,一个对象的打印不应该与一个对象相关联,它应该与打印方法相关联。 在这种情况下,另一种devise模式会有所帮助,这就是Visitor模式,用于伪造Double Dispatch。 完全描述这个答案太长,但你可以在这里阅读一个很好的描述 。

如果你在任何地方使用switch语句,你可能会遇到这样的可能性:在升级的时候,你错过了一个需要更新的地方。

如果你明白它的话,它会工作的很好。

也有2种多态的口味。 第一个在java-esque中很容易理解:

 interface A{ int foo(); } final class B implements A{ int foo(){ print("B"); } } final class C implements A{ int foo(){ print("C"); } } 

B和C共享一个通用接口。 B和C在这种情况下不能被扩展,所以你总是确定你打的是哪个foo()。 C ++也一样,只需要A :: foo纯虚拟。

其次,更棘手的是运行时多态性。 在伪代码中看起来不错。

 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(); } 

但是这是一个棘手的问题。 特别是如果你在C ++中工作,其中一些foo声明可能是虚拟的,而一些inheritance可能是虚拟的。 还有这个答案:

 A* a = new Z; A a2 = *a; a->foo(); a2.foo(); 

可能不是你所期望的。

只要敏锐地意识到你所做的事情,不知道你是否使用了运行时多态。 不要过度自信,如果你不确定在运行时会做什么,那么testing一下。

我必须重申,在一个成熟的代码库中查找所有的开关语句可能是一个不平凡的过程。 如果你错过了任何一个应用程序可能会崩溃,因为一个不匹配的情况下,除非你有默认设置。

还请看“重构”一书中的“Martin Fowlers”
使用开关而不是多态是一种代码味道。

这真的取决于你的编程风格。 虽然这在Java或C#中可能是正确的,但我不同意自动决定使用多态是正确的。 你可以把你的代码分成许多小函数,并用函数指针(在编译时初始化)执行数组查找。 在C ++中,多态性和类经常被滥用 – 可能是人们从强大的OOP语言进入C ++所犯的最大的devise错误是,一切都进入了一个类 – 这是不正确的。 一个类应该只包含最小的一组东西,使它作为一个整体工作。 如果一个子类或朋友是必要的,那就这样吧,但是他们不应该成为规范。 类中的任何其他操作都应该是相同名称空间中的自由函数; ADL将允许这些函数在不查找的情况下使用。

C ++不是OOP语言,不要做成一个。 这跟在C ++中编程C一样糟糕。