简体   繁体   中英

C# replacing huge if-else statement

let's say i have Item object witch mostly holds enum properties like this

public enum Shape
{
    square = 1,
    trangle = 2,
    any = 3
}

public enum Color
{
    blue = 1,
    red = 2,
    yellow = 3,
    green = 4
}


public enum Material
{
    glass = 1,
    wood = 2,
    metal = 3
}


public class Item
{
    public Shape ItemShape { get; set; }
    public Color ItemColor { get; set; }
    public Material ItemMaterial { get; set; }
}

What am trying to do is depends of combination of whole three properties i need to do some action later;

I was thinking to use if-else combination like:

if(item.ItemShape == Shape.square && item.ItemColor == Color.blue && item.ItemMaterial == Material.glass)
        {
            //call Action1
        }
        else if(item.ItemShape == Shape.square && item.ItemColor == Color.blue && item.ItemMaterial == Material.wood)
        {
            // action2
        }
        ......

Problem is that i have around 16 of combinations, so it will be huge if-else method to resolve what method should i call later.

在此输入图像描述

Maybe is there any other way to replace if-else statements more readable code, design patter or something more efficient?

I was thinking to combine whole possible states as flag enum values, but not sure if i can create enum value from object property later.

It sounds like you want some rule set to check an item against. I think the simplest form to make this more readable, is to pass the item, the properties of the rule, and the action to a separate method:

    public bool RunActionIf(Item item, Shape shape, Color color, Material material, Action action)
    {
        if(item.ItemShape == shape && item.ItemColor == color && item.ItemMaterial == material)
        {
            action();
            return true;
        }

        return false;
    }

    public void RunAction(Item item)
    {
        var result =
            RunActionIf(item, Shape.square, Color.blue, Material.glass, Action1) ||
            RunActionIf(item, Shape.square, Color.blue, Material.wood, Action2) ||
            /* Implement your whole table like this */;

        if(!result)
        {
            throw new ArgumentException("No matching rule found", nameof(item));
        }
    }

The main advantage of that method is that it's shorter and with less 'overhead' in the declaration. You can easily see: shape X + color Y + material Z= that action.

Another advantage is that it's easier to implement certain exceptions, for instance by allowing one of the rule parameters to be null to indicate any color, or to use Color.any to support that, although I think it's confusing to have any in the enum with other colors... Anyway, I digress. The point is, if you want to implement that, you have to do that only in one place without having to copy it 16 times.

You could abstract this away a bit further by making such a rule a separate object, which you can put in a list or dictionary, but for a definition like this, it doesn't make it that much more readable, although it does add some benefits of testibility, as well as the possibility to add different kinds of rules, without messing up your clean list again.

I think your best bet would be to ceate a Dictionary that would map your values to methods.
Now there are several options what could be the key in your dictionary - see below.

Disclaimer Please also note that a big if statement is only an issue, if it is spread / duplicated across the codebase, and changes frequently - putting things in a dictionary does not really reduce complexity . Having methods in a dictionary also changes the semantics of your code. The first question should be - am I changing the mappings at runtime? Should they really be dynamic?

  1. Dictionary with key ValueTuple struct you could use syntax (Shape, Color, Material) - this is the easiest one. Please note - not the Tuple class, but ValueTuple struct.
  2. Dictionary with key Item class itself, but then you would need to take care about proper equality comparison in Item . You could make Item a struct to get that for free (but with slower performance, that comes from equality comparisons in System.ValueType that uses reflection in general case), or leave it as a class (or struct) and implement IEquatable<Item> , Equals and GetHashCode .
    Without proper equality comparison, your dictionary lookup will not work (as suggested by @ckuri)
  3. Use ValueTuple without a dictionary to simply condense your code.
  4. A variation of the State pattern where you have a shared interface / base class for your handler. Handler is a separate class that contains one action for a specific set of values, like AnyBlueGlassHandler : Handler . Each handler checks the If condition, and when true, runs the actions. You could then put the handlers in a List<T> and apply them for an item like handlers.Foreach(x=>x.Handle(item))

The code when Item is the key could look like:

    public static class ItemExtensions
    {
    static Dictionary<Item, Action<Item>>
        methods = new Dictionary<Item, Action<Item>>()
        {
            { new Item(Shape.any, Color.blue, Material.glass), x=> { /*do something*/ } }
        };


    public static bool TryApply(this Item item)
    {
        if (methods.TryGetValue(item, out var action))
        {
            action(item);
            return true;
        }
        return false;
    }
}

the code when ValueTuple is the key could look like

 public static class ItemExtensionsUsingValueTuple
 {
    static Dictionary<(Shape, Color, Material), Action<Item>>
        methods = new Dictionary<(Shape, Color, Material), Action<Item>>()
        {
            { (Shape.any, Color.blue, Material.glass), x=> { /*do something*/ } }
        };


    public static bool TryApply(this Item item)
    {
        if (methods.TryGetValue((item.ItemShape, item.ItemColor, item.ItemMaterial), out var action))
        {
            action(item);
            return true;
        }
        return false;
    }
}

a more condensed version of your code with ifs, could look like:

  • declare Key property on your Item class
    public (Shape, Color, Material) Key => (ItemShape, ItemColor, ItemMaterial);
  • use more elegant if statement
    if ( item.Key == (Shape.any, Color.blue, Material.glass)) { }

Try Dictionary<T> , for instance:

static Dictionary<Tuple<Shape, Color, Material>, Action> s_Actions = new
  Dictionary<Tuple<Shape, Color, Material>, Action>() {
    {Tuple.Create(Shape.square, Color.blue, Material.glass), () => { ... } },
     ...  
    {Tuple.Create(Shape.any, Color.red, Material.metal), () => { ... } },
     ...
  };

private static void RunAction(MyItem item) {
  Action action;

  // Either exact match or match with Any 
  if (!s_Actions.TryGetValue(Tuple.Create(item.ItemShape, item.ItemColor, item.ItemMaterial), 
                             out action))
    action = s_Actions.FirstOrDefault(pair => pair.Key.Item1 == Color.any &&
                                              pair.Key.Item2 == item.ItemColor &&
                                              pair.Key.Item3 == item.ItemMaterial) 

  // Execute action if it's found
  if (action != null) 
    action();  
}
void RunAction((Shape shape, Color color, Material material) item)
{
    switch(item)
    {
        case var i1 when i1.color == Color.red && i1.shape == Shape.square:
        case var i2 when i2.color == Color.blue:
            // Do action...
            break;
        case var i1 when i1.shape == Shape.trangle && i1.material == Material.metal:
            // Do action...
            break;
        default:
            // Do action...
            break;
    }
}

This solution uses Value Tuples , but primarily uses C#7 pattern matching in switch statements.

I don't think it totally solves your problem cleanly, but I think the multi-line cases gives you extra readability over the if statement approach and it's easy to implement.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM