Шаблон проектирования, который может заменить цепной переключатель / переход? - PullRequest
27 голосов
/ 27 октября 2010

У меня есть код для обновления ресурсов моего приложения до текущей версии приложения. Этот код вызывается после обновления приложения.

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

Есть ли какой-нибудь шаблон проектирования, описывающий такую ​​проблему?

Ответы [ 13 ]

58 голосов
/ 27 октября 2010

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

var updates = availableUpdates.Where(u => u.version > ver).OrderBy(u => u.version);
foreach (var update in updates) {
  update.apply();
}
39 голосов
/ 27 октября 2010

В этом примере версия увеличивается и всегда вызывает более ранние из них по порядку. Я думаю, что набор 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();
        }
    }
}
6 голосов
/ 27 октября 2010

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

Цепочка ответственности шаблон.

5 голосов
/ 27 октября 2010

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

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

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

alt text

2 голосов
/ 28 октября 2010

почему бы и нет:

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), и все это упаковано доgo.

Заметьте, я не возражаю против того, что здесь есть 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;
  }

}
2 голосов
/ 28 октября 2010

Я бы предложил вариант шаблона команды, при котором каждая команда была бы самопроверяющейся:

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);
    }
}
1 голос
/ 28 октября 2011

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

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

Поэтому создайте иерархию классов:

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

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

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

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

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

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

Редактировать: из комментария @ user470379

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

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

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

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

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

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

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

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

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

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

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

Несколько различий:

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

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

Объедините ПРОСТО отмените и инициализируйте, и у вас будет 4 цепочки если. Просто лучше определить проблемы, прежде чем начать.

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

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

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

1 голос
/ 27 октября 2010

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

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

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

0 голосов
/ 28 октября 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...