How to refactor Switch into Dictionary/Factory

2019-01-26 16:52发布

问题:

I am trying run a 'Recipe' read from a text file and parsed line by line to dynamically call a series of methods. I think I need to implement a Factory after doing quite a bit of googling, but I am lacking some key details. This is the closest example I have:

http://simpleprogrammer.com/2010/08/17/pulling-out-the-switch-its-time-for-a-whooping/

The following code is a snippet of what have now.

    internal static void Run(int Thread_ID, List<StringBuilder> InstructionSet, List<double>[] Waveforms)
    {
        //Init
        List<double>[] Register = new List<double>[10];
        for (int i = 0; i < Waveforms.Length; i++) { Register[i] = new List<double>(Waveforms[i]); }
        for (int i = 0; i < Register.Length; i++) { if (Register[i] == null) { Register[i] = new List<double>(); } }

        //Run Recipe Steps
        foreach (var item in InstructionSet)
        {
            Step Op = Step.Parse(item.ToString());
            switch (Op.TaskName)
            {
                case "SimpleMovingAverage":
                    Register[Convert.ToInt32(Op.Args[0])] = Signal_Filters.SimpleMovingAverage(Register[Convert.ToInt32(Op.Args[1])], Convert.ToInt32(Op.Args[2]));
                    break;

                case "RollingSteppedStdDeviation":
                    Register[Convert.ToInt32(Op.Args[0])] = Signal_Filters.RollingSteppedStdDeviation(Register[Convert.ToInt32(Op.Args[1])], Convert.ToInt32(Op.Args[2]), Convert.ToInt32(Op.Args[3]));
                    break;

               //... etc. many, many methods to be called.
            }
        }
    }

... and below is the portion of the example I have questions about:

public static class MoveFactory
{
    private static Dictionary<string, Func<IMove>> moveMap = new Dictionary<string, Func<IMove>>()
    {
        {"Up", () => { return new UpMove(); }},
        {"Down", () => { return new DownMove(); }},
        {"Left", () => { return new LeftMove(); }}
        // ...
    };

    public static IMove CreateMoveFromName(string name)
    {
        return moveMap[name]();
    }
}
  1. Can I generate the Dictionary list automatically? So that whenever I add a new class that implements my Factory Interface (my equivalent of IMove), I don't have to update my dictionary or pretty much any other part of my code. Perhaps this can forced as part of the Interface?

  2. In the above example code, I don't see it passing arguments in and out. Looking at my code I have data I need to mutate progressively... How would I do this using a Factory.

  3. The Factory needs to be thread-safe as I want to pass different initial data to multiple workers each running their own recipe.

回答1:

Let's tackle these one at a time.

Building The Dictionary Dynamically

This is actually pretty easy to do using a combination of Reflection and Custom Attributes.

The creation of an Attribute is pretty trivial, so I'll leave that to you to look up, but let's assume you have one called MoveNameAttribute that can be applied at a class level. You can then decorate your classes that implement IMove like so:

[MoveName("Up")]
class UpMove: IMove{}

[MoveName("Down")]
class DownMove: IMove{}

Now you can use Reflection and a little LINQ to extract these class types into a dictionary, and create new instances of those types on demand using the key specified in your custom attribute.

While the entire Factory itself is pretty short in terms of lines of code, Reflection can be daunting if you have never done it before. I've annotated every line to explain what is going on.

internal static class MoveFactory
{
    private static readonly IDictionary<String, Type> _moveTypes;

    static MoveFactory()
    {
        _moveTypes = LoadAllMoveTypes();
    }

    private static IDictionary<string, Type> LoadAllMoveTypes()
    {
        var asm =
            //Get all types in the current assembly
            from type in Assembly.GetExecutingAssembly().GetTypes()
            //Where the type is a class and implements "IMove"
            where type.IsClass && type.GetInterface("IMove") != null
            //Only select types that are decorated with our custom attribute
            let attr = type.GetCustomAttribute<MoveNameAttribute>()
            where attr != null
            //Return both the Name and the System.Type
            select new
                        {
                            name = attr.Name,
                            type
                        };

        //Convert the results to a Dictionary with the Name as a key
        // and the Type as the value
        return asm.ToDictionary(move => move.name, move => move.type);
    }

    internal static IMove CreateMove(String name)
    {
        Type moveType;

        //Check to see if we have an IMove with the specific key
        if(_moveTypes.TryGetValue(name, out moveType))
        {
            //Use reflection to create a new instance of that IMove
            return (IMove) Activator.CreateInstance(moveType);
        }

        throw new ArgumentException(
           String.Format("Unable to locate move named: {0}", name));
    }
}

Now that you have your factory, you can simply create new instances like this:

var upMove = MoveFactory.CreateMove("Up");
var downMove = MoveFactory.CreateMove("Down");

Since the factory uses a Static Constructor, it will only populate this list once, and will automatically pick up your new classes.

Passing Arguments

I'm not 100% sure what your use case is here, but it doesn't look like you need to pass arguments to your Factory, rather to some method on your IMove. However, you have a variable number of arguments that can be passed in.

If this is the case, then you are simply going to have to live with a bit of ugliness in your design. You need a very generic method on your IMove interface:

public interface IMove
{
   double Compute(double val1, params int[] args);
}

Now your individual move classes are going to have to just be diligent and check to ensure that they get the proper number of parameters. I'll leave this as an exercise for you, but this should give you what you need based on the example above.

Thread Safety

As it stands the factory implementation above is thread safe because it doesn't rely on any shared state, and the underlying dictionary is essentially immutable. Each call to CreateMove returns a brand new IMove instance.

Now whether or not your implementations of IMove are thread safe is up to you :)

Whew! That was a long answer, but hopefully this will help you out.