【问题标题】:Design pattern that can replace chained switch/goto?可以替代链式开关/转到的设计模式?
【发布时间】:2010-10-27 19:36:43
【问题描述】:

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

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

这里我们有一个级联方法通过跳转到指定的案例块来调用。 我想知道 - 在这种情况下使用 go to 是一种好的做法(通常被认为是这样的坏做法!)? 我不想一个接一个地调用方法——像这样:

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

是否有任何设计模式描述了这样的问题?

【问题讨论】:

  • 啊!这么多死猫!更严肃地说,为什么你反对链接方法调用但 goto 似乎没问题?我猜这个例子并不能反映你的代码的真实性质......
  • 没用过C#,但它不允许你做case:fallthroughs?
  • @Nick C# 不允许有目的的 case fallthroughs,因为 大多数 case fallthrough 是一个难以发现的错误,而少数情况下它实际上是可取的有合适的替代品。
  • @Michael:嗯,C#可以让你明确地失败。不幸的是,它也不允许这样做。
  • 这实际上是一种简洁的自我记录,但你会从那些看不到他们在学校学到的“goto”这个词是邪恶的人那里得到无穷无尽的废话。我的观点?即使你能找到一个非常有效的 goto 用法,也不值得你花这么多废话。

标签: c# design-patterns goto


【解决方案1】:

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

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

【讨论】:

  • 最后,一个开始有意义的答案!所有这些ifswitchgoto 的废话都吓到我了!
  • Nitpick:这确实比面向对象更实用。当然,这正是正确的做法。
  • +1。我相信这是我见过的最好的 linq 案例。很棒。
  • 在实践中检查 - 工作正常,虽然比之前的 (@JarePad) 示例更简洁,但它仍然很容易维护,就像逻辑的东西一样。
  • 您依赖于版本之间的数字关系,既可以选择要应用的版本,也可以对列表进行排序。这只是其余解决方案的语法糖版本
【解决方案2】:

在示例中,版本正在增加,并且总是按顺序调用较早的版本。我认为这里用一组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();
        }
    }
}

【讨论】:

  • 在我的情况下,必须增量调用更新方法 - 以前的方法不涵盖当前的功能 - 所以无论如何我都必须为每种情况分别设置 if(version == number)
  • @UGEEN,这个解决方案确实会逐步调用它们。如果版本以 1002 开头,它将依次调用 updateTo1003updateTo1004updateTo1005
  • @UGEEN,除非你每次都输入一个大的while循环或更新版本,否则这将不起作用
  • 我仍然不喜欢这个,原因与我评论另一个答案(已被删除)相同。随着我们获得越来越多需要支持的版本号,这将变得非常难以维护。恕我直言,责任链模式是一种更适合应用于此问题的设计模式。
  • @Dave,可能是最新版本。我对漂亮的类型循环、委托和元组有个人风格的亲和力,所以它很吸引我。它还具有最少的前进开销(升级函数 + 表中的 1 个条目)。
【解决方案3】:

我讨厌不提供支持信息的空白语句,但是 goto 相当普遍(有充分的理由)并且有更好的方法来实现相同的结果。您可以尝试 Chain of Responsibility 模式,该模式将获得相同的结果,而无需 goto 实现可能变成的“意大利面条式”粘性。

Chain of Responsibility 模式。

【讨论】:

  • +1(尽管留给 OO 进行 2 行流控制,并将其转换为 5-10 行类,乘以您需要执行的补丁数量 :)。即便如此,“updateTo2002()”必须去某个地方......)
  • 链接是在运行时完成的,需要运行时测试是否为 null - 如果您可以在编译时使用 vanilla 层次结构进行链接,则没有必要
【解决方案4】:

goto 总是被认为是不好的做法。如果您使用 goto,通常会更难阅读代码,而且您总是可以以不同的方式编写代码。

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

当然还有this问题。

【讨论】:

  • 必须同意。这是教条的,不是真的。我承认自从我 27 年前第一次学习 Pascal 以来我没有使用过 goto,但这仍然不是一个好的答案。
  • 如果你从不使用 goto 的理由是 Velociraptor 攻击,我认为你的论点需要工作。
  • @Nathan 这不是无知 - 你不能看到 Velociraptor吗??
  • 我们把这个问题扩大到100个版本怎么样?这段代码现在看起来如何?对于这个例子,我们需要停止查看非常小的样本数据集,而要着眼于全局,并提供一种设计模式来更彻底地解决问题,而不是这个非常特殊的问题实例。
  • @Andrew: C# goto 并不像其他语言中的goto 那样危险,因为它不允许您从块外跳到块中。虽然 goto 很少适用于 C#,但 goto 并不是普遍邪恶的;有一些问题,使用goto 会产生比不使用它更简洁的代码。请注意,我在自己的代码中没有注意到这种情况的任何实例。
【解决方案5】:

我会建议一种命令模式的变体,每个命令都是自我验证的:

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

【讨论】:

  • 我觉得这个解决方案很有趣,但在实际使用中我觉得它有点形式胜过内容
  • 取决于您的需求和其他实现细节。各个升级实现易于测试,具有单一职责,并且您可以配置 IoC 容器以自动注入 IUpgradeCommand 的所有实现者,因此您不必手动管理列表。您确实要求提供 OO 解决方案... ;)
  • 依赖于版本号之间的数字关系(本质上做同样的事情,但方式更复杂:common_sense &gt; patterns
  • bool UpgradeApplies(App) 取决于版本号呢?我给App 提供了一个Version 属性以与原始示例保持一致,但是您可以同样轻松地检测当前环境的特征。 composition &gt; inheritance
【解决方案6】:

为什么不:

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

}

【讨论】:

  • 即使使用尾调用,这也会比安全使用 goto 产生更多的开销。
  • 也依赖于版本号之间的数字关系
  • 我相信这是相当普遍的。我不知道您必须编写从版本 3 升级到版本 2 的代码的实际用例。
【解决方案7】:

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

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

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

【讨论】:

  • 我对那篇文章的一个问题是他在结尾处对权威的呼吁:“在您不同意在 C# 语言中包含 goto 之前,请记住您不同意创建语言。你也不同意“代码完成”的作者史蒂夫麦康奈尔。 - 不仅仅是一个逻辑谬误,而且我已经看到证明框架和语言设计者并不总是喜欢他们自己的决定(例如,他们决定将数据添加到每个对象,只是为了支持锁定,并不总是遵循命名约定/并不总是支持 tryparse 等)
  • @Merlyn - 那篇文章还有一个问题if (Step1() == true)
  • @Merlyn 和 @Ishtar 射杀作者,而不是复制粘贴者! :p
  • 鉴于有更好的解决方案,我认为不需要使用 goto “功能”(功能!?)
【解决方案8】:

我认为这里的逻辑可能有些倒退并导致了问题。如果您的方法如下所示:

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

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

编辑:来自@user470379 的评论

在这种情况下,主要是确定您有复制/粘贴模式并对其进行编辑。

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

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

让我们按照您的模式将这两者结合起来。首先让我们添加一个“undoUpgradeXXXX()”来撤消每次升级并能够后退。现在您需要第二组并行的 if 语句来执行撤消操作。

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

您将遇到此类问题的关键迹象是您正在以某种模式进行编码。寻找这样的模式——事实上,我的第一个迹象通常是当我从别人的肩膀上看他们的代码时,如果我能看到一个模式,甚至无法阅读这样写的任何东西:

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

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

那么我知道他们的代码会出现问题。

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

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

现在有一些不同之处:

  • 向每次迭代 (cleanup()) 添加新行为将是对循环的单行修改。
  • 重新排序将本地化为修改您的对象 - 可能更简单。
  • 将升级分成多个必须按顺序调用的步骤很容易。

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

将 JUST undo & initialize 结合起来,你就有了 4 个 if 链。最好在开始之前发现问题。

我也可以说,消除这样的代码可能很困难(取决于您的语言,这非常困难)。在 Ruby 中它实际上很容易,在 java 中需要一些练习,而且许多人似乎无法做到,所以他们称 Java 不灵活且困难。

在这里和那里花费一个小时思考如何减少这样的代码对我的编程能力的帮助比我读过的任何书籍或接受过的任何培训都要多。

这也是一个挑战,让您有事可做,而不是编辑巨大的 if 链来寻找您忘记将 8898 更改为 8899 的复制/粘贴错误。老实说,它使编程变得有趣(这就是为什么我花了这么多时间关于这个答案)

【讨论】:

  • @DaveWhite 解释一下。如果您担心冗长,可以将其缩短为 if (version != 1001) updateTo1001();
  • 不可维护是苛刻的,但想象一下你的例子有 20 个版本号。 50 个版本号。 100 个版本号。你想维护那个代码吗?将 Unmaintainable 替换为“非常难以维护”。
  • 我认为当前的任何答案都不过分冗长。简洁,不多,但没有什么能让我吃惊的。
  • @Dave 我倾向于同意,但与这里的大多数其他答案相比,我想说的是:每个更新的 goto 与每个更新周围的 if 块与此大致相同,同时在需要时可以灵活地允许多个更新路径。
  • 我也酌情对其他答案发表了类似评论。
【解决方案9】:

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

首先,请注意,在各种情况下执行的代码之间存在清晰的层次关系。 IE。第一种情况为第二种情况做了一切,然后再做更多。第二种情况为第三种情况做了一切,然后更多。

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

// 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() 代替 goto,并在类层次结构“Version1002 extends Version1001”中建立连接
  2. 这不依赖于版本号之间的算术关系(与上面的流行答案不同),因此您可以优雅地执行诸如“VersionHelios extends VersionGalileo”之类的事情
  3. 此类可以集中任何其他特定于版本的功能,例如@Override String getVersionName () { return "v1003"; }

【讨论】:

  • "right"... 直到您使用 4000+ 版本的嵌套类定义。
【解决方案10】:

我认为没关系,尽管我怀疑这是您的真实代码。您确定不需要将Update 方法封装在AppUpdate 类中吗? IMO,您拥有名为 XXX001XXX002 等的方法这一事实并不是一个好兆头。

无论如何。这是代表的替代方案(并不是真的建议您使用它,只是一个想法):

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

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

【讨论】:

  • 我发现该代码比原来的 goto 代码更难阅读。
  • @Holstebroe - 查看包含 100 个版本选择的原始代码。如果您不熟悉,这可能会“看起来”更难,但这是朝着可测试性和可维护性迈出的一步。
  • 代码没有 100 个版本选择,它有 5 个。关键是泛化的成本只有在一定的复杂性水平上才开始得到回报。当然,当达到该级别时,应该有重构的纪律。
【解决方案11】:

我发表了一条评论,即使用 goto 永远不值得你为使用它而付出的废话(即使它是一个很棒的、完美的使用)——太多的程序员学到了一些东西,却永远无法从他们的大脑中解脱出来。

我不打算发布答案,但我认为你所暗示的整个解决方案都是错误的,但我认为这还不够清楚。我以为这只是为了说明你的观点,但应该明确指出:要非常小心代码中的模式——这与复制/粘贴代码一样糟糕(实际上,它是复制/粘贴代码)。

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

您只需在版本号为

您的升级对象链甚至可以通过添加撤消和向控制器添加非常微不足道的代码来向后和向前遍历 - 使用示例代码维护这将成为一场噩梦。

【讨论】:

  • 依赖于版本号之间的数值关系和列表中元素的正确顺序
【解决方案12】:

您可以查看状态机工作流模式。对你来说简单实用的可能是:Stateless project

【讨论】:

    【解决方案13】:

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

    /* 如果可能,至少升级到版本 106。如果代码 无法进行升级,别管它,让外部代码 观察不变的版本号 */ 无效升级到 106(无效) { 如果(版本

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

    【讨论】:

    • 依赖于版本号之间的数字关系和 if 语句的正确顺序——脆弱的你可以很容易地复制一个段而忘记更改版本号并让升级代码运行两次
    • @agksmehx:我编辑修复了上面的代码(应该将版本设置为 106)。已修复,两次调用升级例程应该是无操作的。基本思想是对“策略”模式的简单线性实现。该代码不处理如果文件以 107 或更高版本开始会发生什么的问题;这也许可以通过在一个文件中包含两个版本号来处理:实际版本和可以读取它的最低代码的数量。代码的 106 版本不知道它是否能够读取 107。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-09-19
    • 1970-01-01
    • 2021-03-17
    • 2016-12-15
    • 1970-01-01
    • 2021-12-13
    • 1970-01-01
    相关资源
    最近更新 更多