可以替代链式 switch/goto 的设计模式吗?

发布于 2024-09-29 19:27:28 字数 649 浏览 0 评论 0原文

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

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 块来调用级联方法。 我想知道 - 在这种情况下使用 go to 是一个好的做法(通常被认为是不好的做法!)吗? 我不想一个接一个地调用方法 - 像这样:

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

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

I have a code for updating my application resources to current application version.
This code is called after application update.

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

Here we have a cascade method calling by jumping to specified case block.
I wonder - is that good practice to use go to (often considered as such bad practise!) in this case?
I do not want to call method one by other - like this:

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

It there any design pattern describes such an issue?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(13

巨坚强 2024-10-06 19:27:29

我讨厌不提供支持信息的空白语句,但 goto 受到相当普遍的批评(有充分的理由),并且有更好的方法来实现相同的结果。您可以尝试责任链模式,该模式将获得相同的结果,而无需“意大利面条- goto 实现可以变成“ish” goo。

责任链模式。

I hate blank statements that don't provide supporting information, but goto is fairly universally panned (for good reason) and there are better ways to achieve the same results. You could try the Chain of Responsibility pattern that will achieve the same results without the "spaghetti-ish" goo that a goto implementation can turn into.

Chain of Responsibility pattern.

苦行僧 2024-10-06 19:27:29

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

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

当然,请参阅这个问题。

替代文本

goto is always considered as bad practice. If you use goto it is usually harder to read code and you always can write your code differently.

For example you could used linked list to create a chain of methods and some processor class that processes the chain. (See pst's answer for good example.). It is much more object oriented and maintainable. Or what if you have to add one more method call beetween 1003 and case 1004?

And of course see this question.

alt text

空心↖ 2024-10-06 19:27:29

我建议使用命令模式的变体,每个命令都是自我验证的:

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

I would suggest a variation of the command pattern, with each command being self-validating:

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);
    }
}
少年亿悲伤 2024-10-06 19:27:29

为什么不呢:

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

当然,所有这些递归都会产生开销,但并没有那么多(调用碳水化合物收集器时只有一个上下文和一个 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;
  }

}

why not:

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

Sure, all this recursion creates overhead, but not all that much (with a call to the carbage collector only a context and an int), and it's all packaged up to go.

Note, I don't mind the goto's much here, and the tuple (int:action) variations have their merits too.

EDIT:

For those who don't like recursion:

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

}
眼睛会笑 2024-10-06 19:27:29

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

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

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

I would say that this is a very eligible reason to use the GOTO feature.

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

In fact, the switch() statement in C# is effectively a pretty face for a collection of labels and a hidden goto operation. case 'Foo': is just another way of defining a type of label inside the scope of a switch().

呆头 2024-10-06 19:27:29

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

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 撤消”和“JUST 撤消”初始化并且你有 4 个 if 链。最好在开始之前找出问题。

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

花一个小时到处思考如何减少这样的代码对我的编程能力的提升比我读过的任何书籍或接受过的培训都要多。

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

I think perhaps the logic is somewhat backwards here and causing the problem. What if your methods looked like this:

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

I don't know your exact use case, but it would seem to me like most often you would want to update to the most recent version, but install the incremental updates as needed along the way. I think doing it this way captures that logic better.

Edit: from @user470379's comment

In this case, mostly it's identifying the fact that you have a copy/paste pattern and editing it.

The coupling problem is, in this case, barely an issue but could be. I'll give you a few things that could come up in your scenario that would be hard to code if done this way:

  • Every update now needs an extra cleanup step, so after updateTo1001() call cleanup(), etc.
  • You need to be able to step back in order to test older versions
  • You need to insert an update between 1001 and 1002

Let's take a combination of these two done following your pattern. First let's add an "undoUpgradeXXXX()" to undo each upgrade and be able to step backwards. Now you need a second, parallel set of if statements to do the undos.

Now, let's add to that "insert 1002.5". All of a sudden you are re-writing two potentially long chains of if statements.

The key indication that you are going to have these kind of problems is that you are coding in a pattern. Look out for patterns like this--in fact, one of my first indications is usually when I'm looking over someone's shoulder at their code, if I can see a pattern without even being able to read anything written like this:

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

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

then I know I'm going to have problems with their code.

The easiest solution is generally to remove the differences from each "group" and put them into data (often an array, no necessarily an external file), collapse the groups into a loop and iterate over that array.

In your case, the easy solution is to make each of your upgrade objects with a single upgrade method. Create an array of these objects and when it's time to upgrade, iterate over them. You may also need some way to sequence them--You're currently using a number, that might work--or a date might be better--that way you can "Go to" a given date easily.

A few differences now:

  • Adding a new behavior to each iteration (cleanup()) would be a single line modification to your loop.
  • Reordering would be localized to modifying your objects--possibly even simpler.
  • Breaking your upgrade into multiple steps that must be called in order would be easy.

Let me give you an example of that last one. Suppose after all your upgrades have been run you need to go through an initialize step for each (different in each case). If you add an initialize method to each object then the modification to your initial loop is trivial (simply add a second iteration through the loop). In your original design you'd have to copy, paste & edit the entire if chain.

Combine JUST undo & initialize and you have 4 if chains. It's just better to identify problems before you start.

I can also say that eliminating code like this can be difficult (downright tough depending on your language). In Ruby it's actually pretty easy, in java it can take some practice and many can't seem to do it so they call Java inflexible and difficult.

Spending an hour here and there mulling over how to reduce code like this has done more for my programming abilities than any books I've read or training I've had.

Also it's a challenge, gives you something to do instead of editing huge if-chains looking for the copy/paste error where you forgot to change 8898 to 8899. Honestly it makes programming fun (which is why I spent so much time on this answer)

萧瑟寒风 2024-10-06 19:27:29

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

首先,请注意,在各种情况下执行的代码之间存在明确的层次关系。 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扩展Version1001”
  2. 这不依赖于版本号之间的算术关系(与上面的流行答案不同),因此您可以优雅地执行诸如“VersionHelios扩展VersionGalileo”之类的操作
  3. 此类可以集中任何其他特定于版本的功能,例如as @Override String getVersionName () { return "v1003"; } }

The right way to do this is using inheritance and polymorphism as follows:

First, note that there is a clear hierarchical relationship between the code executed in the various cases. I. e. The first case does everything for second case and then some more. The second case does everything for third case and then some more.

Therefore, create a class hierarchy:

// 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

Second, use virtual dispatch, aka polymorphism, instead of the switch-case:

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

Discussion (for the unconvinced):

  1. Instead of gotos, use the destination-neutral super.update() and make the connection in the class hierarchy "Version1002 extends Version1001"
  2. This does not depend on the arithmetic relationship between the version numbers (unlike a popular answer above), so you can elegantly do things like "VersionHelios extends VersionGalileo"
  3. This class can centralize any other version-specific functionality such as @Override String getVersionName () { return "v1003"; }
梅窗月明清似水 2024-10-06 19:27:29

我认为没关系,尽管我怀疑这是您的真实代码。您确定不需要将 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();

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

I think it's alright, although I doubt this is your real code. Are you sure you don't need to encapsulate an Update method inside an AppUpdate class or something? The fact that you have methods named XXX001, XXX002 etc. is not a good sign, IMO.

Anyway. here's an alternative with delegates (not really suggesting you use it, just an idea):

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

It would be difficult to recommend the most appropriate pattern without more detail.

无人问我粥可暖 2024-10-06 19:27:29

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

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

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

您只需在版本号 < 时迭代该集合即可。您的目标版本,并为每个对象调用该对象的升级代码。通过这种方式,要创建新的升级级别,您只需制作一个“升级”对象并将其粘贴到集合中即可。

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

I threw into a comment that using goto is never worth the crap you'll take for using it (even if it's an awesome, perfect use)--too many programmers learn something and can never get it unstuck from their brain.

I wasn't going to post an answer, but I don't think it's been made clear enough that the entire solution you are implying is just wrong. I had assumed it was just to make your point, but it should be made clear: Be very careful of patterns in code--this is just as bad as copy/paste code (in fact, it IS copy/paste code).

You should just have a collection of objects, each with the upgrade code and a version number.

You simply iterate over that collection while the version number is < your target version, and call the upgrade code on that object for each one. That way to create a new upgrade level you are just making a single "Upgrade" object and sticking it into the collection.

Your chain of upgrade objects can even be traversed backwards as well as forwards with the addition of an undo and a very trivial code addition to the controller--something that would become a nightmare to maintain using the example code.

无畏 2024-10-06 19:27:29

您可以查看状态机工作流程模式。对您来说简单且有用的可能是:无状态项目

You may look at State Machine workflow pattern. The simple and usefull for you may be: Stateless project

别挽留 2024-10-06 19:27:29

我不得不处理一些这样的问题(将文件转换为这种格式,以便它可以转换为其他格式等),并且我不喜欢 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”语句,因为它没有考虑版本更新例程可能不起作用或一次可能更新多个级别的可能性。

I've had to deal with some such a problem (get file into this format, so it can be gotten into this other format, etc.) and I don't like the switch statement. The version with 'if' tests might be good, or it might be good to recursively have something like:

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

Unless you have thousands of versions, stack depth shouldn't be a problem. I think the if-test version, testing specifically for versions that each upgrade routine can handle, reads better; if the version number at the end isn't one the main code can handle, signal an error. Alternatively, lose the "if" statements in the main code and include them in the routines. I don't like the 'case' statement because it doesn't consider the possibility that a version-update routine might not work, or might update more than one level at a time.

故乡的云 2024-10-06 19:27:28

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

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

Well, if we want to be "object oriented", why not let the objects-do-the-talking?

var updates = availableUpdates.Where(u => u.version > ver).OrderBy(u => u.version);
foreach (var update in updates) {
  update.apply();
}
和我恋爱吧 2024-10-06 19:27:28

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

In the example the version is increasing and always calling the earlier ones in sequence. I think that a set of if statements is probably more appropriate here

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

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

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

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

Some have commented that this approach is unmaintainable as the number of versions gets higher (think 50 or so). In that case here is an easier to maintain version

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();
        }
    }
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文