Подтвердить что ты не робот

Шаблон проектирования, который может заменить цепной переключатель/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. Интересно, стоит ли использовать хорошую практику (часто считается такой плохой практикой!) В этом случае? Я не хочу вызывать метод один за другим - вот так:

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

В ней какой-либо шаблон проектирования описывает такую ​​проблему?

4b9b3361

Ответ 1

В примере версия увеличивается и всегда вызывает более ранние последовательности. Я думаю, что набор операторов 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

Ну, если мы хотим быть "объектно-ориентированными", почему бы не позволить объектам делать-говорить?

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

Ответ 3

Я ненавижу пустые утверждения, которые не предоставляют вспомогательную информацию, но goto довольно универсально подправлен (по уважительной причине), и есть лучшие способы достижения тех же результатов. Вы можете попробовать шаблон Chain of Responsibility, который достигнет тех же результатов без "спагетти-иша", к которому может перейти реализация goto.

Шаблон последовательности ответственности.

Ответ 4

goto всегда считается плохой практикой. Если вы используете goto, вам обычно сложнее читать код, и вы всегда можете написать свой код по-разному.

Например, вы могли бы использовать связанный список для создания цепочки методов и некоторого класса процессора, который обрабатывает цепочку. (См. Pst ответ на хороший пример.). Он гораздо более ориентирован на объект и поддерживается. Или что, если вам нужно добавить еще один вызов метода между 1003 и case 1004?

И, конечно, посмотрите этот вопрос.

alt text

Ответ 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);
    }
}

Ответ 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);
 }

Конечно, вся эта рекурсия создает накладные расходы, но не так много (с вызовом сборщику карбюратора только контекст и int), и все это упаковано до конца.

Обратите внимание: я не очень сильно отношусь к goto, и вариации кортежа (int: action) также имеют свои достоинства.

EDIT:

Для тех, кто не любит рекурсию:

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

}

Ответ 7

Я бы сказал, что это очень подходящая причина использовать функцию GOTO.

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

Фактически, оператор switch() в С# является довольно красивым лицом для коллекции меток и скрытой операции goto. case 'Foo': - это еще один способ определения типа метки внутри области switch().

Ответ 8

Я думаю, что, возможно, логика здесь несколько обратная и вызывает проблему. Что делать, если ваши методы выглядели так:

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

Я не знаю вашего конкретного случая использования, но мне кажется, что чаще всего вы хотите обновить самую последнюю версию, но при необходимости установите дополнительные обновления по мере необходимости. Я думаю, что делать это так лучше, чтобы лучше понять эту логику.

Изменить: from @user470379 комментарий

В этом случае, в основном, это указывает на то, что у вас есть шаблон копирования/вставки и его редактирование.

Проблема связи, в данном случае, едва ли проблема, но может быть. Я расскажу вам несколько вещей, которые могут возникнуть в вашем сценарии, который будет сложно скопировать, если это будет сделано следующим образом:

  • Для каждого обновления теперь требуется дополнительный этап очистки, поэтому после очистки UpdateTo1001()() и т.д.
  • Вы должны быть в состоянии отступить, чтобы проверить старые версии
  • Вам нужно вставить обновление между 1001 и 1002

Давайте возьмем комбинацию этих двух, выполненных по вашему шаблону. Сначала добавьте "undoUpgradeXXXX()", чтобы отменить каждое обновление и сделать шаг назад. Теперь вам понадобится второй, параллельный набор операторов if для отмены.

Теперь добавьте к этому "insert 1002.5". Внезапно вы переписываете две потенциально длинные цепочки операторов if.

Ключевым признаком того, что у вас возникнут такие проблемы, является то, что вы кодируете шаблон. Посмотрите на такие шаблоны - на самом деле, одно из моих первых указаний обычно возникает, когда я просматриваю чье-то плечо в своем коде, если я вижу шаблон, даже не имея возможности читать что-либо, написанное так:

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

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

тогда я знаю, что у меня будут проблемы с их кодом.

Самое простое решение - это, как правило, удалить отличия от каждой "группы" и поместить их в данные (часто это массив, не обязательно внешний файл), свернуть группы в цикл и перебрать этот массив.

В вашем случае простым решением является создание каждого из объектов обновления с помощью одного метода обновления. Создайте массив этих объектов, и когда придет время для обновления, перейдите по ним. Возможно, вам также понадобится какой-то способ их упорядочить. В настоящее время вы используете число, которое может работать - или дата может быть лучше - таким образом вы можете легко "перейти" к данной дате.

Теперь несколько различий:

  • Добавление нового поведения в каждую итерацию (cleanup()) будет одной строкой для вашего цикла.
  • Переупорядочение будет локализовано для изменения ваших объектов - возможно, даже проще.
  • Ломать свое обновление на несколько шагов, которые нужно вызвать по порядку, было бы легко.

Позвольте мне привести вам пример этого последнего. Предположим, что после всех ваших обновлений вы должны пройти шаг инициализации для каждого (каждый в каждом случае). Если вы добавляете метод инициализации к каждому объекту, то модификация вашего начального цикла тривиальна (просто добавьте вторую итерацию через цикл). В вашем оригинальном дизайне вам придется копировать, вставлять и редактировать целую цепочку if.

Объединить JUST отменить и инициализировать, и у вас есть цепочки 4 if. Это просто лучше определить проблемы, прежде чем вы начнете.

Я также могу сказать, что устранение кода, подобного этому, может быть затруднено (прямо в зависимости от вашего языка). В Ruby это довольно легко, в java это может принести некоторую практику, и многие из них не могут это сделать, поэтому они называют Java негибким и сложным.

Проводя час здесь и там, размышляя над тем, как уменьшить код, подобный этому, сделал больше для моих возможностей программирования, чем любые книги, которые я читал или тренировался у меня.

И это вызов, дает вам что-то делать вместо редактирования огромных if-цепочек, которые ищут ошибку копирования/вставки, когда вы забыли изменить 8898 на 8899. Честно говоря, это делает программирование забавным (вот почему я потратил так много времени на этот ответ)

Ответ 9

Правильный способ сделать это - использовать наследование и полиморфизм следующим образом:

Во-первых, обратите внимание, что существует четкая иерархическая взаимосвязь между кодом, выполняемым в различных случаях. 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

Во-вторых, используйте виртуальную диспетчеризацию, иначе называемую полиморфизмом, вместо коммутационного флага:

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

Обсуждение (для неубежденных):

  • Вместо gotos используйте нейтральный super.update() и нейтралитет назначения, и установите соединение в иерархии классов "Версия 1002 расширяет Версию 1001"
  • Это не зависит от арифметической связи между номерами версий (в отличие от популярного ответа выше), поэтому вы можете элегантно делать такие вещи, как "VersionHelios extends VersionGalileo"
  • Этот класс может централизовать любые другие функциональные возможности, такие как @Override String getVersionName () { return "v1003"; }

Ответ 10

Я думаю, что все в порядке, хотя я сомневаюсь, что это ваш настоящий код. Вы уверены, что вам не нужно инкапсулировать метод 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();

Было бы сложно рекомендовать наиболее подходящий шаблон без дополнительных деталей.

Ответ 11

Мне приходилось иметь дело с такой проблемой (получить файл в этом формате, чтобы его можно было получить в этом другом формате и т.д.), и мне не нравится оператор switch. Версия с проверками "если" может быть хорошей, или может быть полезно рекурсивно иметь что-то вроде:

/* 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, потому что он не учитывает возможность того, что процедура обновления версии может не работать или может обновлять более одного уровня за раз.

Ответ 12

Я бросил в комментарий, что использование goto никогда не стоит того дерьма, который вы возьмете для его использования (даже если это потрясающее, идеальное использование) - слишком много программистов узнают что-то и никогда не могут отторгнуть его от своего мозга.

Я не собирался публиковать ответ, но я не думаю, что было достаточно ясно, что все решение, которое вы подразумеваете, просто неверно. Я предполагал, что это просто для вас, но должно быть ясно: будьте очень осторожны с шаблонами кода - это так же плохо, как код копирования/вставки (на самом деле это код копирования/вставки).

У вас должен быть только набор объектов, каждый из которых содержит код обновления и номер версии.

Вы просто перебираете эту коллекцию, в то время как номер версии равен < вашей целевой версии и вызвать код обновления для этого объекта для каждого из них. Таким образом, чтобы создать новый уровень обновления, вы просто создаете единственный объект "Upgrade" и вставляете его в коллекцию.

Ваша цепочка объектов обновления может быть пройдена как назад, так и вперед с добавлением отмены и очень тривиальным дополнением кода к контроллеру - что-то, что станет кошмаром для поддержки кода примера.

Ответ 13

Вы можете посмотреть шаблон рабочего процесса State Machine. Простым и полезным для вас может быть: Проект без гражданства