可以替换链式开关/ goto的设计模式?

时间:2010-10-27 19:36:43

标签: c# design-patterns goto

我有一个代码,用于将我的应用程序资源更新为当前应用程序版本。 应用程序更新后调用此代码。

int version = 1002;   // current app version

switch(version)
{
   case 1001:
      updateTo1002();
      goto case 1002;

   case 1002:
      updateTo1003();
      goto case 1003;

   case 1003:
      updateTo1004();
      goto case 1004;
      break;

   case 1004:
      updateTo1005();
      break;
}

这里我们通过跳转到指定的case块来调用cascade方法。 我想知道 - 在这种情况下,使用go(通常被视为这种不良做法!)是一种好习惯吗? 我不想一个接一个地调用方法 - 像这样:

updateTo1002()
{
   // do the job
   updateTo1003();
}
updateTo1003()
{
   // do the job
   updateTo1004();
}

有没有设计模式描述这样的问题?

13 个答案:

答案 0 :(得分:57)

好吧,如果我们想成为“面向对象”,为什么不让对象做对话?

var updates = availableUpdates.Where(u => u.version > ver).OrderBy(u => u.version);
foreach (var update in updates) {
  update.apply();
}

答案 1 :(得分:39)

在示例中,版本正在增加,并且始终按顺序调用较早的版本。我认为一组if语句可能更合适

if (version == 1001 ) { 
  updateTo1002();
}

if (version <= 1002) {
  updateTo1003();
}

if (version <= 1003) {
  updateTo1004(); 
}

if (version <= 1004) {
  updateTo1005();
}

有些人评论说,随着版本数量的增加(约50左右),这种方法是不可维护的。在这种情况下,这是一个更容易维护的版本

private List<Tuple<int, Action>> m_upgradeList;

public Program()
{
    m_upgradeList = new List<Tuple<int, Action>> {
        Tuple.Create(1001, new Action(updateTo1002)),
        Tuple.Create(1002, new Action(updateTo1003)),
        Tuple.Create(1003, new Action(updateTo1004)),
        Tuple.Create(1004, new Action(updateTo1005)),
    };
}

public void Upgrade(int version)
{
    foreach (var tuple in m_upgradeList)
    {
        if (version <= tuple.Item1)
        {
            tuple.Item2();
        }
    }
}

答案 2 :(得分:6)

我讨厌不提供支持信息的空白语句,但goto相当普遍地被淘汰(有充分理由)并且有更好的方法来实现相同的结果。您可以尝试Chain of Responsibility模式,这将获得相同的结果,而没有goto实现可以变成的“spaghetti-ish”goo。

Chain of Responsibility模式。

答案 3 :(得分:5)

goto一直被认为是不好的做法。如果你使用goto,通常很难阅读代码,你总是可以用不同的方式编写代码。

例如,您可以使用链接列表创建一系列方法和一些处理链的处理器类。 (请参阅pst的答案以获得良好的例子。)它更加面向对象和可维护。或者,如果您必须在1003和案例1004之间再添加一个方法调用,该怎么办?

当然,请看this问题。

alt text

答案 4 :(得分:2)

我建议修改命令模式,每个命令都是自我验证的:

interface IUpgradeCommand<TApp>()
{
    bool UpgradeApplies(TApp app);
    void ApplyUpgrade(TApp app);
}

class UpgradeTo1002 : IUpgradeCommand<App>
{
    bool UpgradeApplies(App app) { return app.Version < 1002; }

    void ApplyUpgrade(App app) {
        // ...
        app.Version = 1002;
    }
}

class App
{
    public int Version { get; set; }

    IUpgradeCommand<App>[] upgrades = new[] {
        new UpgradeTo1001(),
        new UpgradeTo1002(),
        new UpgradeTo1003(),
    }

    void Upgrade()
    {
        foreach(var u in upgrades)
            if(u.UpgradeApplies(this))
                u.ApplyUpgrade(this);
    }
}

答案 5 :(得分:2)

为什么不:

int version = 1001;

upgrade(int from_version){
  switch (from_version){
    case 1000:
      upgrade_1000();
      break;
    case 1001:
      upgrade_1001();
      break;
    .
    .
    .
    .
    case 4232:
      upgrade_4232();
      break;
  }
  version++;
  upgrade(version);
 }

当然,所有这些递归都会产生开销,但并不是那么多(通过调用carbage收集器只有一个上下文和一个int),并且它们都打包了。

注意,我不介意这里的goto很多,而且元组(int:action)变体也有其优点。

编辑:

对于那些不喜欢递归的人:

int version = 1001;
int LAST_VERSION = 4233;

While (version < LAST_VERSION){
  upgrade(version);
  version++;
}

upgrade(int from_version){
  switch (from_version){
    case 1000:
      upgrade_1000();
      break;
    case 1001:
      upgrade_1001();
      break;
    .
    .
    .
    .
    case 4232:
      upgrade_4232();
      break;
  }

}

答案 6 :(得分:1)

我想说这是使用GOTO功能的一个非常合格的理由。

http://weblogs.asp.net/stevewellens/archive/2009/06/01/why-goto-still-exists-in-c.aspx

事实上,C#中的switch()语句实际上是一组标签和隐藏的goto操作的漂亮面孔。 case 'Foo':只是在switch()范围内定义标签类型的另一种方式。

答案 7 :(得分:1)

我想也许逻辑在某种程度上是倒退的并导致问题。如果你的方法看起来像这样:

updateTo1002() 
{ 
   if (version != 1001) {
       updateTo1001();
   }
   // do the job     
} 
updateTo1003() 
{ 
   if (version != 1002) {
       updateTo1002();
   }
   // do the job     
} 

我不知道您的确切用例,但在我看来,您通常希望更新到最新版本,但在此过程中根据需要安装增量更新。我认为这样做可以更好地捕捉逻辑。

编辑:来自@ user470379的评论

在这种情况下,主要是识别出你有复制/粘贴模式并进行编辑的事实。

在这种情况下,耦合问题几乎不是问题,但可能是。我会给你一些可能出现在你的场景中的东西,如果这样做会很难编码:

  • 现在每次更新都需要额外的清理步骤,所以在updateTo1001()之后调用cleanup()等等。
  • 您需要退后一步才能测试旧版本
  • 您需要在1001和1002之间插入更新

让我们按照你的模式完成这两项的组合。首先,让我们添加一个“undoUpgradeXXXX()”来撤销每个升级,并能够向后退一步。现在你需要第二个并行的if语句来做undos。

现在,让我们添加“插入1002.5”。突然之间,你正在重写两个可能很长的if语句链。

您将遇到这些问题的关键迹象是您正在编码模式。注意这样的模式 - 实际上,我的第一个迹象之一通常是当我在他们的代码中查看别人的肩膀时,如果我能看到一个模式甚至无法读取这样的任何东西:

********
   ***
   *****

********
   ***
   *****
...

然后我知道我的代码会有问题。

最简单的解决方案通常是删除每个“组”的差异并将它们放入数据(通常是数组,不一定是外部文件),将组折叠成循环并迭代该数组。

在您的情况下,简单的解决方案是使用单个升级方法制作每个升级对象。创建这些对象的数组,在升级时,迭代它们。您可能还需要一些方法对它们进行排序 - 您当前正在使用可能有效的数字 - 或者日期可能更好 - 这样您就可以轻松地“转到”给定日期。

现在有一些差异:

  • 为每个迭代添加一个新行为(cleanup())将对您的循环进行单行修改。
  • 重新排序将被本地化为修改您的对象 - 可能更简单。
  • 将升级分解为必须按顺序调用的多个步骤非常简单。

让我举一个最后一个例子。假设在运行所有升级之后,您需要对每个升级执行初始化步骤(在每种情况下都不同)。如果为每个对象添加一个initialize方法,那么对初始循环的修改是微不足道的(只需在循环中添加第二次迭代)。在您的原始设计中,您必须复制,粘贴和放大编辑整个if链。

结合JUST撤消&amp;初始化,你有4个链。在开始之前找出问题会更好。

我还可以说,消除这样的代码可能很困难(根据您的语言而言,非常严厉)。在Ruby中它实际上非常简单,在java中它可能需要一些练习,许多人似乎无法这样做,因此他们称Java不灵活且困难。

花一个小时在这里和那里考虑如何减少这样的代码对我的编程能力做了比我阅读或训练过的任何书籍更多。

这也是一个挑战,给你一些事情,而不是编辑巨大的if-chains寻找复制/粘贴错误,你忘了改变8898到8899.老实说它让编程变得有趣(这就是为什么我花了这么多时间在这个答案)

答案 8 :(得分:1)

正确的方法是使用继承和多态,如下所示:

首先,请注意在各种情况下执行的代码之间存在明确的层次关系。 I. e。第一种情况为第二种情况做了一切,然后再进行了一些。第二种情况为第三种情况做了一切,然后又进行了一些。

因此,创建一个类层次结构:

// Java used as a preference; translatable to C#
class Version {
    void update () {
        // do nothing
    }
}

class Version1001 extends Version {
    @Override void update () {
        super.update();
        // code from case update 1001
    }
}

class Version1002 extends Version1001 {
    @Override void update () {
        super.update();
        // code from case update 1002
    }
}

class Version1003 extends Version1002 {
    @Override void update () {
        super.update();
        // code from case update 1003
    }
}

// and so forth

其次,使用虚拟调度,即多态,而不是switch-case:

Version version = new Version1005();
version.update();

讨论(不相信):

  1. 使用目标中立的super.update()而不是gotos,并在类层次结构中建立连接“Version1002 extends Version1001”
  2. 这不依赖于版本号之间的算术关系(与上面的流行答案不同),所以你可以优雅地做“VersionHelios extends VersionGalileo”这样的事情。
  3. 此类可以集中任何其他特定于版本的功能,例如@Override String getVersionName () { return "v1003"; }

答案 9 :(得分:0)

我认为没关系,虽然我怀疑这是你的真正的代码。您确定不需要在Update类或其他内容中封装AppUpdate方法吗?您拥有名为XXX001XXX002等方法的事实并不是一个好兆头,IMO。

反正。这是代表们的另一种选择(并不是真的建议你使用它,只是一个想法):

var updates = new Action[] 
                 {updateTo1002, updateTo1003, updateTo1004, updateTo1005};

if(version < 1001 || version > 1004)
   throw new InvalidOperationException("...");    

foreach(var update in updates.Skip(version - 1001))
   update();

如果没有更多详细信息,很难推荐最合适的模式。

答案 10 :(得分:0)

我不得不处理一些这样的问题(将文件转换为这种格式,因此可以使用其他格式等)并且我不喜欢switch语句。带有'if'测试的版本可能会很好,或者递归地使用类似的东西可能会很好:

/* Upgrade to at least version 106, if possible.  If the code
   can't do the upgrade, leave things alone and let outside code
   observe unchanged version number */

void upgrade_to_106(void)
{
  if (version < 105)
    upgrade_to_105();
  if (version == 105)
  {
    ...
    version = 106;
  }
}

除非您有数千个版本,否则堆栈深度应该不是问题。我认为if-test版本,专门针对每个升级例程可以处理的版本进行测试,读取效果更好;如果最终的版本号不是主代码可以处理的版本号,则发出错误信号。或者,丢失主代码中的“if”语句并将它们包含在例程中。我不喜欢'case'语句,因为它没有考虑版本更新例程可能不起作用的可能性,或者可能一次更新多个级别。

答案 11 :(得分:0)

我发表评论说使用goto永远不值得你使用它的废话(即使它是一个很棒的,完美的用途) - 太多的程序员学到了东西,永远不会让它从他们的大脑中解脱出来。

我不会发表回答,但我认为已经明确表示你所暗示的整个解决方案都是错误的。我认为这只是为了说明问题,但应该明确:在代码中要非常小心 - 这与复制/粘贴代码一样糟糕(实际上,它是复制/粘贴代码)。 / p>

您应该只拥有一组对象,每个对象都有升级代码和版本号。

您只需迭代该集合,而版本号为&lt;您的目标版本,并为每个对象调用该对象的升级代码。通过这种方式创建新的升级级别,您只需创建一个“升级”对象并将其粘贴到集合中。

您的升级对象链甚至可以向后和向前遍历,同时添加一个撤销和一个非常简单的代码添加到控制器 - 这将成为维护使用示例代码的噩梦。

答案 12 :(得分:0)

您可以查看State Machine工作流程模式。对您而言简单实用可能是:Stateless project