marclee44 / me

1 stars 0 forks source link

替代switch/case之C#篇 #13

Open marclee44 opened 3 years ago

marclee44 commented 3 years ago

switch/caseif/else一样,是常用的条件判断/流程控制方法。 想象一个场景:公司给员工考评有A/B/C/D/E这5个评级,A级能得到升职、B级可以加薪、C级合格也就没什么变化、D级不太合格需要降薪、E级很不合格基本等着被解雇。写程序进行处理时,通常的switch/case 写法:

    public class Employee {
        public string Name;
        public string Evaluation;
    }

    class Program {
        static void Main(string[] args) {
            List<Employee> employees = GetEmployeesByArgs(args);
            foreach (var employee in employees) {
                switch (employee.Evaluation) {
                    case "A":
                        LevelUp(employee);
                        break;
                    case "B":
                        Raise(employee);
                        break;
                    case "C":
                        NothingHappened(employee);
                        break;
                    case "D":
                        PayCut(employee);
                        break;
                    case "E":
                        Fired(employee);
                        break;
                    default:
                        break;
                }
            }
        }

        static void LevelUp(Employee employee) {
            Console.WriteLine("{0} level up", employee.Name);
        }

        static void Raise(Employee employee) {
            Console.WriteLine("{0} got a raise", employee.Name);
        }

        static void NothingHappened(Employee employee) {
            Console.WriteLine("{0} nothing happened", employee.Name);
        }

        static void PayCut(Employee employee) {
            Console.WriteLine("{0} got a pay cut", employee.Name);
        }

        static void Fired(Employee employee) {
            Console.WriteLine("{0} is fired", employee.Name);
        }
    }

可以看到,随着判断条件的增多,switch/caseif/else if/else一样,代码的可读性、可维护性严重下降。那么有什么办法可以替代?今天我们就简单来聊聊3种常用的。

字典+代理

字典+代理是比较简单的一种实现:

...
    class Program {
        static void Main(string[] args) {
            List<Employee> employees = GetEmployeesByArgs(args);
            var actionMap = new Dictionary<string, Action<Employee>> {
                { "A", LevelUp },
                { "B", Raise },
                { "C", NothingHappened },
                { "D", PayCut },
                { "E", Fired }
            };
            foreach (var employee in employees) {
                if (actionMap.ContainsKey(employee.Evaluation)) {
                    actionMap[employee.Evaluation](employee);
                }
            }
        }
    }
...

根据字典的key来进行查找,当需要增加新的case分支的时候,只需要编写新的处理方法,并且在字典中增加一对key/value就可以了。代码简洁美观,没有一长串令人厌烦的case了。

继承接口/基类+反射

这个只是稍微复杂一点点。 先建个接口/基类,然后建些类实现这个接口/基类:

    public interface IPay {
        void DoAction(Employee employee);
    }

    public class A : IPay {
        public void DoAction(Employee employee) {
            Console.WriteLine("{0} level up", employee.Name);
        }
    }

    public class B : IPay {
        public void DoAction(Employee employee) {
            Console.WriteLine("{0} got a raise", employee.Name);
        }
    }

    public class C : IPay {
        public void DoAction(Employee employee) {
            Console.WriteLine("{0} nothing happened", employee.Name);
        }
    }

    public class D : IPay {
        public void DoAction(Employee employee) {
            Console.WriteLine("{0} got a pay cut", employee.Name);
        }
    }

    public class E : IPay {
        public void DoAction(Employee employee) {
            Console.WriteLine("{0} is fired", employee.Name);
        }
    }

然后就是调用:

    class Program {
        static void Main(string[] args) {
            List<Employee> employees = GetEmployeesByArgs(args);
            foreach (var employee in employees) {
                try {
                    Assembly assembly = Assembly.GetExecutingAssembly(); //获取当前程序集
                    dynamic obj = assembly.CreateInstance(string.Format("{0}.{1}", MethodBase.GetCurrentMethod().DeclaringType.Namespace, employee.Evaluation)); //用带namespace的类全名创建类实例
                    obj.DoAction(employee); //执行实例中的DoAction方法
                } catch { }
            }
        }
    }

构建稍微复杂的结果,就是在需要新增case分支时,只用新增一个继承了接口/基类的子类,实现内部的处理方法,就完成了。

继承接口/基类+重载方法

这个其实不太适合当前场景,更适合可以抽象出接口/基类的对象作为switch的判断条件时。这里我们权当评级是这样的一个对象。 改造Employee类及相关Evaluation类:

    public abstract class Evaluation { }

    public class Employee2 {
        public string Name;
        public Evaluation Evaluation;
    }

    public class LevelA : Evaluation { }

    public class LevelB : Evaluation { }

    public class LevelC : Evaluation { }

    public class LevelD : Evaluation { }

    public class LevelE : Evaluation { }

调用及实现:

    class Program {
        static void Main(string[] args) {
            List<Employee2> employees = GetEmployeesByArgs2(args);
            foreach (var employee in employees) {
                DoAction(employee.Evaluation, employee.Name);
            }
        }

        static void DoAction(dynamic evaluation, string name) {
            try {
                DoAction(evaluation, name);
            } catch { }
        }

        static void DoAction(LevelA evaluation, string name) {
            Console.WriteLine("{0} level up", name);
        }

        static void DoAction(LevelB evaluation, string name) {
            Console.WriteLine("{0} got a raise", name);
        }

        static void DoAction(LevelC evaluation, string name) {
            Console.WriteLine("{0} nothing happened", name);
        }

        static void DoAction(LevelD evaluation, string name) {
            Console.WriteLine("{0} got a pay cut", name);
        }

        static void DoAction(LevelE evaluation, string name) {
            Console.WriteLine("{0} is fired", name);
        }
    }

这里必须要一个DoAction(dynamic evaluation, string name)的动态方法进行中转。如果没有该中转,由于调用方法的参数类型是基类,下面没有匹配的方法可直接进入,编译无法通过;如果是以基类类型作为中转方法的参数,编译倒是能通过,但你将因无限套娃调用,而见到著名的StackOverFlow异常。 这里,我们也可以看到,增加case时的工作量,也仅仅是实现一个Evaluation的子类,以及相应处理方法的事。

下一篇将是 替代switch/case之Android篇