用多态替换条件 - 理论上很好但不实用

时间:2011-02-01 19:08:45

标签: refactoring polymorphism conditional

“使用多态替换条件”仅在您正在为您选择切换/ if语句的对象类型时才是优雅的。作为一个例子,我有一个Web应用程序,它读取一个名为“action”的查询字符串参数。动作可以具有“视图”,“编辑”,“排序”等值。那么如何用多态实现呢?好吧,我可以创建一个名为BaseAction的抽象类,并从中派生ViewAction,EditAction和SortAction。但是,我不需要条件来决定实例化哪种类型的BaseAction?我不知道如何用多态完全替换条件。如果有的话,条件只会被推到链的顶端。

编辑:

public abstract class BaseAction
{
    public abstract void doSomething();
}

public class ViewAction : BaseAction
{
    public override void doSomething() { // perform a view action here... }
}

public class EditAction : BaseAction
{
    public override void doSomething() { // perform an edit action here... }
}

public class SortAction : BaseAction
{
    public override void doSomething() { // perform a sort action here... }
}


string action = "view";  // suppose user can pass either "view", "edit", or "sort" strings to you.
BaseAction theAction = null;

switch (action)
{
    case "view":
        theAction = new ViewAction();
        break;

    case "edit":
        theAction = new EditAction();
        break;

    case "sort":
        theAction = new SortAction();
        break;
}

theAction.doSomething();    // So I don't need conditionals here, but I still need it to decide which BaseAction type to instantiate first. There's no way to completely get rid of the conditionals.

9 个答案:

答案 0 :(得分:28)

你是对的 - “条件被推到了链条的顶端” - 但是没有“只是”它。它非常强大。正如@thkala所说,你只需选择一次;从那以后,对象知道如何开展业务。您描述的方法--BaseAction,ViewAction和其他方法 - 是一种很好的方法。试一试,看看你的代码变得多清晰。

当你有一个工厂方法接受像“View”这样的字符串并返回一个Action,并且你调用它时,你已经隔离了你的条件。那很棒。并且你无法正确地理解力量,直到你尝试过 - 所以试一试!

答案 1 :(得分:9)

即使最后一个答案是一年前,我也想对这个主题做一些评论/评论。

回答评论

我同意@CarlManaster关于编码switch语句一次以避免处理重复代码的所有众所周知的问题,在这种情况下涉及条件(其中一些是@thkala提到的)。

我不相信@KonradSzałwiński或@AlexanderKogtenkov提出的方法适合这种情况有两个原因:

首先,根据您所描述的问题,您无需动态更改操作名称与处理操作的操作实例之间的映射。

请注意,这些解决方案允许这样做(通过简单地将操作名称分配给新的操作实例),而基于静态交换机的解决方案则不允许(映射是硬编码的)。 此外,您还需要一个条件来检查映射表中是否定义了给定的键,如果不是,则应该采取操作(switch语句的default部分)。

其次,在这个特定的例子中,dictionaries实际上是switch语句的隐藏实现。更重要的是,使用default子句读取/理解switch语句可能比从映射表中精神上执行返回处理对象的代码更容易,包括处理未定义的键。

有一种方法可以摆脱所有条件,包括switch语句:

删除switch语句(根本不使用条件)

如何从动作名称创建正确的动作对象?

我将与语言无关,所以这个答案不会那个很长,但诀窍是实现类也是对象

如果您已经定义了一个polimorphic层次结构,那么引用BaseAction的具体子类是没有意义的:为什么不让它返回通过其名称处理动作的正确实例?

这通常是通过您编写的相同switch语句(例如,工厂方法)实现的......但是这样做:

public class BaseAction {

    //I'm using this notation to write a class method
    public static handlingByName(anActionName) {
        subclasses = this.concreteSubclasses()

        handlingClass = subclasses.detect(x => x.handlesByName(anActionName));

        return new handlingClass();
    }
}

那么,那个方法在做什么?

首先,检索此的所有具体子类(指向BaseAction)。在您的示例中,您将获得一个包含ViewActionEditActionSortAction的集合。

请注意,我说的是具体的子类,而不是所有的子类。如果层次结构更深,具体的子类将始终是层次结构底部的那些(叶子)。那是因为他们是唯一不应该是抽象的并且提供真正实现的人。

其次,获取第一个子类,它回答它是否可以通过其名称处理一个动作(我使用的是lambda / closure风格的表示法)。 handlesByName的{​​{1}}类方法的示例实现如下所示:

ViewAction

第三,我们将消息new发送给处理动作的类,有效地创建它的实例。

当然,当子类没有按名称处理动作时,你必须处理这种情况。许多编程语言(包括Smalltalk和Ruby)允许将detect方法传递给第二个lambda /闭包,只有在没有子类符合条件时才会对其进行求值。 此外,您将不得不处理多个子类通过其名称处理操作的情况(可能,其中一个方法以错误的方式编码)。

<强>结论

这种方法的一个优点是可以通过编写(而不是修改)现有代码来支持新操作:只需创建public static class ViewAction { public static bool handlesByName(anActionName) { return anActionName == 'view' } } 的新子类并正确实现BaseAction类方法。它有效地支持通过添加新概念添加新功能,而无需修改现有的实现。很明显,如果新功能需要将新的polimorphic方法添加到层次结构中,则需要进行更改。

此外,您可以使用系统反馈向开发人员提供:“所提供的操作不由BaseAction的任何子类处理,请创建新的子类并实现抽象方法”。对我而言,模型本身告诉你什么是错的(而不是试图在精神上执行查找表)这一事实增加了价值并明确了解必须完成的工作。

是的,这可能听起来过于设计。请保持开放的心态,并意识到解决方案是否过度设计必须与您正在使用的特定编程语言的开发文化一起进行。例如,.NET人员可能不会使用它,因为.NET不允许您将类视为真实对象,而另一方面,该解决方案用于Smalltalk / Ruby文化。

最后,使用常识和品味来预先确定特定技术在使用之前是否真正解决了您的问题。这是诱人的,但所有的权衡(文化,开发人员的资历,对变革的抵制,开放的思想等)都应该进行评估。

答案 2 :(得分:7)

需要考虑的一些事项:

  • 您只能实例化每个对象。一旦你这样做,就其类型不再需要条件。

  • 即使在一次性实例中,如果你使用了子类,你会摆脱多少个条件?使用像这样的条件的代码很容易一次又一次地完全完全相同的条件......

  • 将来需要foo Action值时会发生什么?您需要修改多少个地方?

  • 如果您需要的barfoo略有不同,该怎么办?对于类,您只需从BarAction继承FooAction,覆盖您需要更改的一件事。

从长远来看,面向对象的代码通常比程序代码更容易维护 - 大师们也不会遇到任何问题,但对于我们其他人来说, 是一个区别。

答案 3 :(得分:4)

有几种方法可以将输入字符串转换为给定类型的对象,而条件肯定是其中之一。根据实现语言,也可以使用switch语句,该语句允许将期望的字符串指定为索引并创建或获取相应类型的对象。还有更好的方法。

查找表可用于将输入字符串映射到所需对象:

action = table.lookup (action_name); // Retrieve an action by its name
if (action == null) ...              // No matching action is found

初始化代码将负责创建所需的对象,例如

table ["edit"] = new EditAction ();
table ["view"] = new ViewAction ();
...

这是可以扩展以涵盖更多细节的基本方案,例如操作对象的其他参数,在使用它们进行表查找之前规范化操作名称,使用整数替换表格而不是使用整数字符串以识别请求的操作等。

答案 4 :(得分:3)

您的示例不需要多态,也可能不建议使用。用多态调度替换条件逻辑的最初想法是合理的。

这就是区别:在你的例子中,你有一套小的固定(和预定)动作。此外,在“排序”和“编辑”操作几乎没有共同点的意义上,这些操作并没有很强的相关性。多态性过度构建了您的解决方案。

另一方面,如果你有许多具有特殊行为的对象用于一个共同的概念,那么多态就是你想要的。例如,在游戏中可能存在许多玩家可以“激活”的对象,但每个对象的反应都不同。你可以用复杂的条件(或者更可能是switch语句)来实现它,但是多态会更好。多态性允许您引入不属于原始设计的新对象和行为(但符合其原则)。

在您的示例中,对于支持视图/编辑/排序操作的对象进行抽象仍然是一个好主意,但可能不会自己抽象这些操作。这是一个测试:您是否想要将这些操作放入集合中?可能不是,但你可能有一个支持它们的对象列表。

答案 5 :(得分:2)

我一直在考虑这个问题,可能比我遇到的其他开发人员更多。他们中的大多数完全没有意识到维护长嵌套if-else语句或切换案例的成本。在你的案例中,我完全理解你在应用名为“用多态性替换条件”的解决方案时遇到的问题。您已成功注意到只要已选择对象,多态就会起作用。在这个步骤中也已经说过,这个问题可以减少到关联[key] - &gt; [类]。以下是解决方案的AS3实现。

private var _mapping:Dictionary;
private function map():void
{
   _mapping["view"] = new ViewAction();
   _mapping["edit"] = new EditAction();
   _mapping["sort"] = new SortAction();
}

private function getAction(key:String):BaseAction
{
    return _mapping[key] as BaseAction;
} 

你喜欢跑步吗?

public function run(action:String):void
{
   var selectedAction:BaseAction = _mapping[action];
   selectedAction.apply();
}

在ActionScript3中有一个名为getDefinitionByName(key:String)的全局函数:Class。我们的想法是使用您的键值来匹配代表您的条件的解决方案的类的名称。在您的情况下,您需要将“view”更改为“ViewAction”,将“edit”更改为“EditAction”并将“sort”更改为“SortAtion”。无需使用查找表记忆任何内容。函数运行将如下所示:

public function run(action:Script):void
{
   var class:Class = getDefintionByName(action);
   var selectedAction:BaseAction = new class();
   selectedAction.apply();
}

不幸的是,您使用此解决方案进行了编译检查,但您可以灵活地添加新操作。如果你创建一个新密钥,你唯一需要做的就是创建一个适当的类来处理它。

即使您不同意我也请发表评论。

答案 6 :(得分:1)

public abstract class BaseAction
{
    public abstract void doSomething();
}

public class ViewAction : BaseAction
{
    public override void doSomething() { // perform a view action here... }
}

public class EditAction : BaseAction
{
    public override void doSomething() { // perform an edit action here... }
}

public class SortAction : BaseAction
{
    public override void doSomething() { // perform a sort action here... }
}


string action = "view"; // suppose user can pass either
                        // "view", "edit", or "sort" strings to you.
BaseAction theAction = null;

switch (action)
{
    case "view":
        theAction = new ViewAction();
        break;

    case "edit":
        theAction = new EditAction();
        break;

    case "sort":
        theAction = new SortAction();
        break;
}

theAction.doSomething();

所以这里我不需要条件,但是我仍然需要它来决定首先实例化哪个BaseAction类型。没有办法完全摆脱条件。

答案 7 :(得分:0)

多态性是一种结合方法。这是被称为“对象模型”的特殊情况。对象模型用于操纵复杂系统,如电路或绘图。考虑存储/编组文本格式的东西:项目“A”,连接到项目“B”和“C”。现在你需要知道什么是连接到A。一个人可能会说我不打算为此创建一个对象模型,因为我可以在解析时计算它,单通道。在这种情况下,你可能是对的,你可能会在没有对象模型的情况下离开。但是,如果您需要使用导入设计进行大量复杂的操作呢?你会以文本格式操作它还是通过调用java方法发送消息并引用java对象更方便?这就是为什么有人提到你只需要翻译一次。

答案 8 :(得分:0)

您可以在哈希映射中的某处存储字符串和相应的操作类型。

public abstract class BaseAction
{
    public abstract void doSomething();
}

public class ViewAction : BaseAction
{
    public override void doSomething() { // perform a view action here... }
}

public class EditAction : BaseAction
{
    public override void doSomething() { // perform an edit action here... }
}

public class SortAction : BaseAction
{
    public override void doSomething() { // perform a sort action here... }
}


string action = "view"; // suppose user can pass either
                        // "view", "edit", or "sort" strings to you.
BaseAction theAction = null;

theAction = actionMap.get(action); // decide at runtime, no conditions
theAction.doSomething();