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

Как вы пишете код, чья логика защищена от будущих дополнительных перечислений?

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

// original code
enum Fruit
{ 
    Apple,
    Orange,
    Banana,
}

...

Fruit fruit = acquireFruit();
if (fruit != Fruit.Orange && fruit != Fruit.Banana)
    coreFruit();
else
    pealFruit();
eatFruit();

Теперь делайте вид, что годы развития проходят с этими тремя типами. Различные варианты вышеуказанной логики распространяются во всех хранимых процедурах, пакетах SSIS, приложениях для Windows, веб-приложениях, java-приложениях, perl-скриптах и ​​т.д....

Наконец:

// new code
enum Fruit
{ 
    Apple,
    Orange,
    Banana,
    Grape,
}

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

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

Я придумал выстрел в темноте:

# 1 Избегайте "Не в логике", например

// select fruit that needs to be cored
select Fruit from FruitBasket where FruitType not in(Orange, Banana)

# 2 При необходимости используйте тщательно разработанные методы NotIn()

internal static class EnumSafetyExtensions
{
    /* By adding enums to these methods, you certify that 1.) ALL the logic inside this assembly is aware of the
     * new enum value and 2.) ALL the new scenarios introduced with this new enum have been accounted for.
     * Adding new enums to an IsNot() method without without carefully examining every reference will result in failure. */

    public static bool IsNot(this SalesOrderType target, params SalesOrderType[] setb)
    {
        // SetA = known values - SetB

        List<SalesOrderType> seta = new List<SalesOrderType>
        {
            SalesOrderType.Allowance,
            SalesOrderType.NonAllowance,
            SalesOrderType.CompanyOrder,
            SalesOrderType.PersonalPurchase,
            SalesOrderType.Allotment,
        };
        setb.ForEach(o => seta.Remove(o));

        // if target is in SetA, target is not in SetB
        if (seta.Contains(target))
            return true;

        // if target is in SetB, target is not not in SetB
        if (setb.Contains(target))
            return false;
        // if the target is not in seta (the considered values minus the query values) and the target isn't in setb
        // (the query values), then we've got a problem.  We've encountered a value that this assembly does not support.

        throw new InvalidOperationException("Unconsidered Value detected: SalesOrderType." + target.ToString());
    }
}

Теперь я могу безопасно использовать код следующим образом:

bool needsCoring = fruit.IsNot(Fruit.Orange, Fruit.Banana);

Если этот код будет распространяться по всей системе, будут выбрасываться исключения, когда Виноград скачет в город (qa поймает их все).

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

Как вы справляетесь с этим?

UPDATE:

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

Если у С# не было есть переключатель... по умолчанию, мы могли бы изменить приведенный выше код следующим образом:

Fruit fruit = acquireFruit();
if (fruit != Fruit.Orange && fruit != Fruit.Banana)
    coreFruit();
else if(fruit == Fruit.Apple)
    pealFruit();
else
    throw new NotSupportedException("Unknown Fruit:" + fruit)
eatFruit();

ОТКАЗ:

Вы действительно не должны использовать какой-либо из вышеуказанных псевдокодов. Он может (?) Компилировать или даже работать, но это ужасный код, действительно. Я видел много хороших решений в этом потоке, если вы ищете подход на основе ООП. Хорошее решение, конечно, помещает все переключения и проверки в централизованный метод (метод factory - это то, что меня поражает). Кроме того, потребуется также просмотр кода коллегиального обзора.

4b9b3361

Ответ 1

Если я правильно понял ваш вопрос, наиболее распространенной практикой является выброс NotSupportedException или NotImplementedException.

switch (fruit.Kind) {
case Fruit.Apple:
    Bite(fruit);
    break;
case Fruit.Banana:
    FeedToMonkey(fruit);
    break;
default: throw new NotSupportedException("Unknown fruit.");
}

Что касается добавления новых значений enum, которые нарушали бы существующую логику if-not-is, я считаю, что использование перечисления - это плохой выбор в этом случае. У ваших предметов явно своеобразное поведение, они не похожи, например. цвета. Возможно, лучше сделать варианты ответственными за принятие решения о том, как их следует лечить. Затем вы должны заменить enums на полиморфизм.

Ответ 2

Я бы использовал типы, а не перечисления, для структур данных... E.G. Создайте интерфейс IFruit, который имеет следующее:

interface IFruit
{
     bool NeedsCoring();
     void GetEaten(Person by);
     // etc.
}

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

Ответ 3

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

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

public abstract class Fruit {
    public abstract T Match(Func<Apple, T> f, Func<Banana, T> g, Func<Grape, T> h);

    public class Apple {
        // apple properties
        public override T Match(Func<Apple, T> f, Func<Banana, T> g, Func<Grape, T> h) {
            return f(this);
        }
    }
    public class Banana {
        // banana properties
        public override T Match(Func<Apple, T> f, Func<Banana, T> g, Func<Grape, T> h) {
            return g(this);
        }
    }
    public class Grape {
        // grape properties
        public override T Match(Func<Apple, T> f, Func<Banana, T> g, Func<Grape, T> h) {
            return h(this);
        }
    }
}

Использование:

public void EatFruit(Fruit fruit, Person p)
{
    // prepare fruit
    fruit.Match(
        apple => apple.Core(),
        banana => banana.Peel(),
        grape => { } // no steps required to prepare
        );

    p.Eat(fruit);
}

public FruitBasket PartitionFruits(List<Fruit> fruits)
{
    List<Apple> apples = new List<Apple>();
    List<Banana> bananas = new List<Banana>();
    List<Grape> grapes = new List<Grape>();

    foreach(Fruit fruit in fruits)
    {
        // partition by type, 100% type-safe on compile,
        // does not require a run-time type test
        fruit.Match(
            apple => apples.Add(apple),
            banana => bananas.Add(banana),
            grape => grapes.Add(grape));
    }

    return new FruitBasket(apples, bananas, grapes);
}

Этот стиль полезен по трем причинам:

  • Будущая проверка: Допустим, я добавляю тип Pineapple и добавляю его к методу Match: Match(..., Func<Pineapple, T> k);. Теперь у меня есть куча ошибок компиляции, потому что все текущие применения Match проходят в 3 params, но мы ожидаем 4. Код не компилируется до тех пор, пока не исправит все использования Match для обработки вашего нового типа - это делает невозможно ввести новый тип, рискуя не обрабатываться в вашем коде.

  • Тип безопасности. Оператор Match предоставляет вам доступ к определенным свойствам подтипов без тестирования типа времени выполнения.

  • Refactorable. Если вам не нравятся делегаты, как показано выше, или у вас есть несколько десятков типов, и вы не хотите их обрабатывать, их легко переносить a FruitVisitor, поэтому каждый подтип переходит к соответствующему методу FruitVisitor.

Ответ 4

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

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

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

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

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

Ответ 5

В то время как у меня нет тонны опыта С#, была ли эта Java, то я бы вместо этого создал интерфейс IPeelable и еще один ICoreable, и эти классы фруктов реализуют. Затем вместо того, чтобы избегать логики not, вы могли бы просто проверить, есть ли у фрукта, который вы приобрели, на любом из интерфейсов - таким образом, вы можете добавить будущие плоды, которые реализуют как очищаемые, так и корулярные, например, muscmelon.

Ответ 6

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

Ответ 7

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

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

using System;

enum FruitType
{
    Apple,
    Banana,
    Pineapple,
}

interface IFruit
{
    void Prepare();
    void Eat();
}

class Apple : IFruit
{
    public void Prepare()
    {
        // Wash
    }

    public void Eat()
    {
        // Chew and swallow
    }
}

class Banana : IFruit
{
    public void Prepare()
    {
        // Peel
    }

    public void Eat()
    {
        // Feed to your dog?
    }
}

class Pineapple : IFruit
{
    public void Prepare()
    {
        // Core
        // Peel
    }

    public void Eat()
    {
        // Cut up first
        // Then, apply directly to the forehead!
    }
}

class FruitFactory
{
    public IFruit GetInstance(FruitType fruitType)
    {
        switch (fruitType)
        {
            case FruitType.Apple:
                return new Apple();
            case FruitType.Banana:
                return new Banana();
            case FruitType.Pineapple:
                return new Pineapple();
            default:
                throw new NotImplementedException(
                    string.Format("Fruit type not yet supported: {0}"
                        , fruitType
                    ));
        }
    }
}

class Program
{
    static FruitType AcquireFruit()
    {
        // Todo: Read this from somewhere.  A database or config file?
        return FruitType.Pineapple;
    }

    static void Main(string[] args)
    {
        var fruitFactory = new FruitFactory();
        FruitType fruitType = AcquireFruit();
        IFruit fruit = fruitFactory.GetInstance(fruitType);
        fruit.Prepare();
        fruit.Eat();
    }
}

Причина, по которой я пошел на разработку Prepare, а не дизайн Core/Peel/Deseed/Dehusk/Chill/Cut, заключается в том, что каждый фрукт будет нуждаться в другой подготовке. Благодаря дизайну, который отделяет методы подготовки, каждый раз, когда вы добавляете новый класс с разными требованиями, вам необходимо поддерживать весь код вызова (и, возможно, каждую реализацию). Благодаря дизайну, который скрывает конкретные детали подготовки, вы можете поддерживать каждый класс отдельно, а добавление новых фруктов не нарушает существующих.

См. статью, почему мой дизайн предпочтительнее:

С++ FAQ Lite - виртуальные функции

Ответ 8

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

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

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

Вместо того, чтобы иметь базовый класс "Фрукты" и подкласс для каждого вида фруктов, просто введите переменную экземпляра для типа - и сделайте это перечислением. В противном случае количество подклассов может выйти из-под контроля. Теперь каждый вид - это "Фрукты". Поле "type" указывает нам, какой тип.

Теперь, когда у нас есть фундаментальная идея класса Fruit, добавьте идею peel/core как другое поле. Если есть только эти два варианта, возможно, поле может быть логическим, "isPeelable". Если там, где, или может быть в будущем, другие варианты, такие как "разбить" или "вырвать", теперь перечисление может быть хорошей идеей, как и для поля типа фруктов. Я полагаю, что поле экземпляра класса можно назвать "prepToEat"?

Отсюда становится интересным. Ваш дизайн должен быть гибким для размещения новых видов фруктов. Кроме того, похоже, что у вас есть метод для каждого значения "prepToEat". И у нас не будет ни одной из этих "кодировок исключений", которые вы описываете в # 1, # 2 выше.

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

КЛЮЧЕВЫЙ ДИЗАЙН-ЭЛЕМЕНТ использует делегат для метода prepToEat. Это снова препятствует нам изменять класс Fruit непосредственно, когда мы добавляем фрукты в наш репитер.

  public class FruitEater
  {
     ArrayList<Fruit> myFruit;
     FruitFactory myFruitMaker;

     public FruitEater()
     {
        this.myFruit = new Arraylist();
        this.myFruitMaker = new FruitFactory();
     }

     public static void Main( args[] stuff )
     {
        myFruit.Add( myFruitMaker.Create( FruitType.Orange ));
        myFruit.Add( myFruitMaker.Create( FruitType.Apple ));

        foreach ( Fruit a in myFruit )
        {
           a.eat(); //FINALLY!!!!
        }
     }

  } //FruitEater class



  public class Fruit
  {
     public delegate void PrepareToEatDelegate();

     protected FruitType type;
     protected PrepType prepType;
     // pretend we have public properties to get each of these attributes


     // a field to hold what our delegate creates.
     private PrepareToEatDelegate prepMethod;

     // a method to set our delegate-created field
     public void PrepareToEatMethod( PrepareToEatDelegate clientMethod )
     {
        prepMethod = clientMethod;
     }

     public void Eat()
     {
        this.prepMethod();
        // do other fruit eating stuff
     }

      public Fruit(FruitType myType )
      {
        this.type = myType;
      }
  }

  public class FruitFactory
  {
     public FruitFactory() { }

     public Fruit Create( FruitType myType )
     {
        Fruit newFruit = new Fruit (myType);

        switch ( myType )
        {
           case FruitType.Orange :
              newFruit.prepType = PrepType.peel;
              newFruit.PrepareToEatMethod(new Fruit.PrepareToEatDelegate(FruitFactory.PrepareOrange));
              break;

           case FruitType.Apple :
              newFruit.prepType = PrepType.core;
              newFruit.PrepareToEatMethod( new Fruit.PrepareToEatDelegate( FruitFactory.PrepareApple ) );
              break;

           default :
              // throw an exception - we don't have that kind defined.
        }
        return newFruit;
     }// Create()

     // we need a prep method for each type of fruit this factory makes
     public static void PrepareOrange()
     {
        // whatever you do
     }

     public static void PrepareApple()
     {
        // apple stuff 
     }
  }// FruitFactory

  public enum FruitType
  {
     Orange
     ,Apple
     ,Grape
  }


  public enum PrepType
  {
     peel
     ,core
     ,pluck
     ,smash
  }

Ответ 9

У многих людей есть хорошие предложения, но позвольте мне добавить еще один, который не требует полной редизайна кода или поддержки объектов/классов в местах (например, sql), которые, очевидно, не поддерживают такие вещи.

Вы заявили:

Fruit fruit = acquireFruit();
if (fruit != Fruit.Orange && fruit != Fruit.Banana)
    coreFruit();
else
    pealFruit();
eatFruit();

Что совершенно неожиданно произойдет, если появится Виноград. Я думаю, что лучший подход:

Fruit fruit = acquireFruit();
Boolean fruitPrepared = false;

if (fruit == Fruit.Orange || fruit == Fruit.Banana) {
    pealFruit();
    fruitPrepared = true;
}

if (fruit == Fruit.Apple) {
    coreFruit();
    fruitPrepared = true;
}

if (!fruitPrepared) 
  throw new exception();

eatFruit();

Третий подход очень похож:

Fruit fruit = acquireFruit();

switch(fruit) {
  case Fruit.Orange:
  case Fruit.Banana:
    pealFruit();
    break;    
  case Fruit.Apple:
    coreFruit();
    break;
  default:
    throw new exception('unknown fruit detected');
    break;
}

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

Ответ 10

Используйте социальный фактор!

enum Fruit
{ 
    Apple,
    Orange,
    Banana,
    // add Grape here, and I'll shoot you
    // not kidding.
}

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