28

アプリケーションリソースを現在のアプリケーションバージョンに更新するためのコードがあります。このコードは、アプリケーションの更新後に呼び出されます。

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

ここでは、指定されたケースブロックにジャンプして呼び出すカスケードメソッドがあります。私は疑問に思います-この場合、使用するのに良い習慣は(しばしばそのような悪い習慣と見なされます!)に行きますか?メソッドを1つずつ呼び出したくない-次のように:

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

そのような問題を説明するデザインパターンはありますか?

4

13 に答える 13

58

「オブジェクト指向」になりたいのなら、オブジェクトに話させてみませんか?

var updates = availableUpdates.Where(u => u.version > ver).OrderBy(u => u.version);
foreach (var update in updates) {
  update.apply();
}
于 2010-10-27T19:50:02.583 に答える
40

この例では、バージョンが増加しており、常に以前のバージョンを順番に呼び出しています。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();
        }
    }
}
于 2010-10-27T19:40:26.727 に答える
6

裏付けとなる情報を提供しない空白のステートメントは嫌いですが、gotoは(正当な理由で)かなり普遍的にパンされており、同じ結果を達成するためのより良い方法があります。gotoの実装が変換できる「スパゲッティっぽい」グーなしで同じ結果を達成するChainofResponsibilityパターンを試すことができます。

責任の連鎖パターン。

于 2010-10-27T19:43:05.687 に答える
5

goto常に悪い習慣と見なされます。gotoを使用する場合、通常はコードを読むのが難しく、常に別の方法でコードを書くことができます。

たとえば、リンクリストを使用して、メソッドのチェーンと、チェーンを処理するプロセッサクラスを作成できます。(良い例については、pstの回答を参照してください。)それははるかにオブジェクト指向であり、保守可能です。1003または、caseの間にもう1つのメソッド呼び出しを追加する必要がある場合はどうなります1004か?

そしてもちろん、この質問を参照してください。

代替テキスト

于 2010-10-27T19:37:46.910 に答える
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);
    }
}
于 2010-10-27T20:11:48.750 に答える
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);
 }

確かに、このすべての再帰はオーバーヘッドを作成しますが、それほど多くはありません (コンテキストと 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;
  }

}
于 2010-10-28T18:09:28.040 に答える
1

これがGOTO機能を使用する非常に適切な理由だと思います。

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

実際、switch()C#のステートメントは、事実上、ラベルのコレクションと非表示のgoto操作の見栄えがします。case 'Foo':のスコープ内でラベルのタイプを定義するもう1つの方法ですswitch()

于 2010-10-27T19:38:13.140 に答える
1

これを行う正しい方法は、次のように継承とポリモーフィズムを使用することです。

まず、さまざまな場合に実行されるコード間には明確な階層関係があることに注意してください。I.e。最初のケースは2番目のケースのすべてを実行し、次にさらにいくつかのケースを実行します。2番目のケースは、3番目のケースのすべてを実行し、さらにいくつかのケースを実行します。

したがって、クラス階層を作成します。

// 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. gotosの代わりに、destination-neutral super.update()を使用して、クラス階層「Version1002extendsVersion1001」で接続を確立します。
  2. これはバージョン番号間の算術関係に依存しないため(上記の一般的な回答とは異なり)、「VersionHeliosextendsVersionGalileo」のようなことをエレガントに行うことができます。
  3. このクラスは、次のような他のバージョン固有の機能を一元化できます。@Override String getVersionName () { return "v1003"; }
于 2011-10-28T05:18:41.260 に答える
1

おそらく、ロジックがやや逆向きであり、問​​題を引き起こしていると思います。メソッドが次のようになったらどうなるでしょうか。

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

正確なユースケースはわかりませんが、ほとんどの場合、最新バージョンに更新したいが、途中で必要に応じて増分更新をインストールするように思えます。このようにすると、そのロジックをよりよく捉えることができると思います。

編集:@ user470379のコメントから

この場合、ほとんどの場合、コピー/貼り付けパターンがあり、それを編集しているという事実を特定しています。

この場合、カップリングの問題はほとんど問題ではありませんが、問題になる可能性があります。この方法で行うとコーディングが難しいシナリオで発生する可能性のあるいくつかのことを紹介します。

  • すべての更新で追加のクリーンアップ手順が必要になるため、 updateTo1001() の後に cleanup() などを呼び出します。
  • 古いバージョンをテストするには、前に戻る必要があります
  • 1001 と 1002 の間に更新を挿入する必要があります

あなたのパターンに従って行われたこれら2つの組み合わせを見てみましょう. まず、「undoUpgradeXXXX()」を追加して、各アップグレードを元に戻し、前に戻ることができるようにします。次に、取り消しを行うための 2 つ目の並列の if ステートメントのセットが必要です。

では、その「insert 1002.5」に追加してみましょう。突然、長い可能性のある 2 つの if ステートメントのチェーンを書き直しています。

この種の問題が発生することを示す重要な兆候は、パターンでコーディングしているということです。このようなパターンに注意してください。実際、私の最初の兆候の 1 つは、通常、誰かの肩越しにコードを見ているときに、次のように書かれたものを読むことさえできなくてもパターンを見つけることができる場合です。

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

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

それから私は彼らのコードに問題があることを知っています.

最も簡単な解決策は、一般に、各「グループ」から違いを取り除き、それらをデータ (多くの場合、配列であり、必ずしも外部ファイルである必要はありません) に入れ、グループをループにまとめて、その配列を反復処理することです。

あなたの場合、簡単な解決策は、単一のアップグレード方法で各アップグレード オブジェクトを作成することです。これらのオブジェクトの配列を作成し、アップグレードするときにそれらを反復します。それらを順序付けする方法も必要になる場合があります-現在、機能する可能性のある番号を使用しています-または日付の方が適切かもしれません-その方法で、特定の日付に簡単に「移動」できます。

いくつかの違いがあります:

  • 各反復 (cleanup()) に新しい動作を追加すると、ループに 1 行の変更が加えられます。
  • 並べ替えは、オブジェクトの変更にローカライズされます。おそらくさらに簡単です。
  • アップグレードを順番に呼び出す必要がある複数のステップに分割するのは簡単です。

その最後の例を挙げましょう。すべてのアップグレードが実行された後、それぞれの初期化手順を実行する必要があるとします (ケースごとに異なります)。各オブジェクトに初期化メソッドを追加すると、最初のループへの変更は簡単です (単純にループに 2 回目の反復を追加するだけです)。元のデザインでは、if チェーン全体をコピー、貼り付け、編集する必要があります。

JUST undo & initialize を組み合わせると、4 つの if チェーンがあります。開始する前に問題を特定することをお勧めします。

また、このようなコードを排除するのは難しい場合があるとも言えます (言語によっては非常に困難です)。Ruby では実際には非常に簡単ですが、Java ではある程度の練習が必要な場合があり、多くの人がそれを行うことができないように見えるため、Java は柔軟性がなく難しいと呼ばれています。

あちこちで 1 時間かけて、このようなコードを削減する方法を熟考することは、これまでに読んだ本やトレーニングしたどの本よりも、私のプログラミング能力に多くの効果をもたらしました。

また、8898 を 8899 に変更するのを忘れたコピー/貼り付けエラーを探して巨大な if チェーンを編集する代わりに、何かをすることができます。 )

于 2010-10-27T19:55:13.887 に答える
0

ステートマシンのワークフローパターンを見ることができます。シンプルで便利なものは次のとおりです。ステートレスプロジェクト

于 2010-10-28T10:35:37.583 に答える
0

これがあなたの本当のコードだとは思えませんが、大丈夫だと思います。Updateクラスなどの中にメソッドをカプセル化する必要はありませんAppUpdateか?XXX001などという名前のメソッドがあるという事実XXX002は、良い兆候ではありません、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();

詳細がなければ、最も適切なパターンを推奨することは困難です。

于 2010-10-27T19:47:39.713 に答える
0

goto を使用することは、それを使用するためのがらくたの使用に値するものではないというコメントを投げかけました (たとえそれが素晴らしく完璧な使用法であったとしても)。

私は答えを投稿するつもりはありませんでしたが、あなたが暗示している解決策全体が間違っているということは十分に明確にされていないと思います. 私はそれがあなたの主張をするためだけだと思っていましたが、明確にする必要があります.コードのパターンには十分注意してください.

オブジェクトのコレクションがあり、それぞれにアップグレード コードとバージョン番号が含まれている必要があります。

バージョン番号がターゲット バージョンよりも小さい間、そのコレクションを繰り返し処理し、それぞれのオブジェクトでアップグレード コードを呼び出すだけです。新しいアップグレード レベルを作成するには、単一の「アップグレード」オブジェクトを作成し、それをコレクションに貼り付けるだけです。

アップグレード オブジェクトのチェーンは、元に戻す操作とコントローラーへの非常に簡単なコードの追加を使用して、前後にトラバースすることもできます。これは、サンプル コードを使用して維持するのは悪夢になります。

于 2010-10-27T21:52:35.313 に答える
0

私はそのような問題に対処しなければなりませんでした (ファイルをこの形式に変換して、別の形式に変換できるようにするなど)、switch ステートメントが好きではありません。「if」テストを含むバージョンが良いかもしれませんし、次のようなものを再帰的に持つのも良いかもしれません:

/* 可能であれば、少なくともバージョン 106 にアップグレードします。コードの場合
   アップグレードを行うことができず、物事を放っておき、コードを外部に任せます
   変更されていないバージョン番号を観察する */

void upgrade_to_106(void)
{
  if (バージョン < 105)
    upgrade_to_105();
  もし (バージョン == 105)
  {
    ...
    バージョン = 106;
  }
}

何千ものバージョンがない限り、スタックの深さは問題になりません。各アップグレード ルーチンが処理できるバージョンを特にテストする if-test バージョンの方が読みやすいと思います。最後のバージョン番号がメイン コードで処理できるものでない場合は、エラーを通知します。または、メイン コードの「if」ステートメントを削除し、ルーチンに含めます。バージョン更新ルーチンが機能しない可能性や、一度に複数のレベルを更新する可能性を考慮していないため、'case' ステートメントは好きではありません。

于 2010-10-27T20:48:18.873 に答える