重构类以摆脱开关大小写

本文关键字:开关 大小写 重构 | 更新日期: 2023-09-27 18:36:07

假设我有这样的类来计算使用不同交通方式旅行不同距离的成本:

public class TransportationCostCalculator
{
    public double DistanceToDestination { get; set; }
    public decimal CostOfTravel(string transportMethod)
    {
        switch (transportMethod)
        {
            case "Bicycle":
                return (decimal)(DistanceToDestination * 1);
            case "Bus":
                return (decimal)(DistanceToDestination * 2);
            case "Car":
                return (decimal)(DistanceToDestination * 3);
            default:
                throw new ArgumentOutOfRangeException();
        }
    }

这很好,但是开关盒对于维护来说可能是一场噩梦,如果我想以后使用飞机或火车怎么办?然后我必须更改上面的类。我可以在这里使用开关盒的什么替代方案以及如何使用任何提示?

我正在想象在这样的控制台应用程序中使用它,该应用程序将从命令行运行,并带有您想要使用哪种运输车辆以及您想要行驶的距离的参数:

class Program
{
    static void Main(string[] args)
    {
        if(args.Length < 2)
        {
            Console.WriteLine("Not enough arguments to run this program");
            Console.ReadLine();
        }
        else
        {
            var transportMethod = args[0];
            var distance = args[1];
            var calculator = new TransportCostCalculator { DistanceToDestination = double.Parse(distance) };
            var result = calculator.CostOfTravel(transportMethod);
            Console.WriteLine(result);
            Console.ReadLine();
        }
    }
}

任何提示非常感谢!

重构类以摆脱开关大小写

你可以做这样的事情:

public class TransportationCostCalculator {
    Dictionary<string,double> _travelModifier;
    TransportationCostCalculator()
    {
        _travelModifier = new Dictionary<string,double> ();
        _travelModifier.Add("bicycle", 1);
        _travelModifier.Add("bus", 2);
        _travelModifier.Add("car", 3);
    }

    public decimal CostOfTravel(string transportationMethod) =>
       (decimal) _travelModifier[transportationMethod] * DistanceToDestination;
}

然后,可以在配置文件中加载传输类型及其修饰符,而不是使用 switch 语句。 我把它放在构造函数中以显示示例,但它可以从任何地方加载。我也可能会使字典静态并且只加载一次。 无需在每次创建新TransportationCostCalculator时都继续填充它,尤其是在运行时不会更改的情况下。

如上所述,以下是通过配置文件加载它的方法:

void Main()
{
  // By Hard coding. 
  /*
    TransportationCostCalculator.AddTravelModifier("bicycle", 1);
    TransportationCostCalculator.AddTravelModifier("bus", 2);
    TransportationCostCalculator.AddTravelModifier("car", 3);
  */
    //By File 
    //assuming file is: name,value
    System.IO.File.ReadAllLines("C:''temp''modifiers.txt")
    .ToList().ForEach(line =>
        {
           var parts = line.Split(',');
        TransportationCostCalculator.AddTravelModifier
            (parts[0], Double.Parse(parts[1]));
        }
    );
    
}
public class TransportationCostCalculator {
    static Dictionary<string,double> _travelModifier = 
         new Dictionary<string,double> ();
    public static void AddTravelModifier(string name, double modifier)
    {
        if (_travelModifier.ContainsKey(name))
        {
            throw new Exception($"{name} already exists in dictionary.");
        }
        
        _travelModifier.Add(name, modifier);
    }
    
    public double DistanceToDestination { get; set; }
    TransportationCostCalculator()
    {
        _travelModifier = new Dictionary<string,double> ();
    }

    public decimal CostOfTravel(string transportationMethod) =>
       (decimal)( _travelModifier[transportationMethod] * DistanceToDestination);
}

编辑:评论中提到,如果需要在不更新代码的情况下更改方程式,这将不允许修改等式,所以我在这里写了一篇关于如何做到这一点的文章:https://kemiller2002.github.io/2016/03/07/Configuring-Logic.html。

在我看来

,任何基于您当前方法的解决方案都存在一个关键方面的缺陷:无论您如何切片,您都将数据放入代码中。 这意味着每次你想改变这些数字中的任何一个,添加新的车辆类型等,你必须编辑代码,然后重新编译,分发补丁等。

您真正应该做的是将数据放在它所属的位置 - 在一个单独的非编译文件中。 您可以使用XML,JSON,某种形式的数据库,甚至只是一个简单的配置文件。 如果需要,请对其进行加密,不一定需要。

然后,您只需编写一个解析器来读取文件并创建车辆类型地图以成本乘数或要保存的任何其他属性。 添加新车辆就像更新数据文件一样简单。 无需编辑代码或重新编译等。 如果您计划将来添加内容,则更健壮且更易于维护。

听起来像是依赖注入的一个很好的候选者:

interface ITransportation {
    decimal CalcCosts(double distance);
}
class Bus : ITransportation { 
    decimal CalcCosts(double distance) { return (decimal)(distance * 2); }
}
class Bicycle : ITransportation { 
    decimal CalcCosts(double distance) { return (decimal)(distance * 1); }
}
class Car: ITransportation {
    decimal CalcCosts(double distance) { return (decimal)(distance * 3); }
}

现在,您可以轻松创建一个新的类Plane

class Plane : ITransportation {
    decimal CalcCosts(double distance) { return (decimal)(distance * 4); }
}

现在为您的计算器创建一个需要 ITransportation 实例的构造器。在您的CostOfTravel方法中,您现在可以调用ITransportation.CalcCosts(DistanceToDestination)

var calculator = new TransportationCostCalculator(new Plane());

这样做的好处是,您可以交换实际的传输实例,而无需对TransportationCostCalculator类进行任何代码更改。

若要完成此设计,还可以创建如下所示的TransportationFactory

class TransportationFactory {
    ITransportation Create(string type) {
        switch case "Bus": return new Bus(); break
        // ...
}

你叫哪个像

ITransportation t = myFactory.Create("Bus");
TransportationCostCalculator calculator = new TransportationCostCalculator(t);
var result = myCalculator.CostOfTravel(50);

你可以像这样定义一个抽象类,并让每个TransportationMethod扩展抽象类:

abstract class TransportationMethod {
    public TransportationMethod() {
        // constructor logic
    }
    abstract public double travelCost(double distance);
}
class Bicycle : TransportationMethod {
    public Bicycle() : base() { }
    override public double travelCost(double distance) {
        return distance * 1;
    }
}
class Bus : TransportationMethod {
    public Bus() : base() { }
    override public double travelCost(double distance) {
        return distance * 2;
    }
}
class Car : TransportationMethod {
    public Car() : base() { }
    override public double travelCost(double distance) {
        return distance * 3;
    }
}

因此,在您的实际方法调用中,可以像这样重写它:

public decimal CostOfTravel(TransportationMethod t) {
    return t.travelCost(DistanceToDestination);
}

您可以为每种类型的旅行使用策略类。但是,您可能需要一个工厂来基于传输方法创建策略,该传输方法可能有一个switch语句来返回适当的计算器。

    public class CalculatorFactory {
        public static ICalculator CreateCalculator(string transportType) {
            switch (transportType) {
                case "car":
                    return new CarCalculator();
                ...
public class CarCalculator : ICalculator {
    public decimal Calc(double distance) {
        return distance * 1;
    }
}
....

您可以创建一个基于传输返回乘数的字典。

public class TransportationCostCalculator
{
    Dictionary<string, int> multiplierDictionary;
    TransportationCostCalculator () 
    {
         var multiplierDictionary= new Dictionary<string, int> (); 
         dictionary.Add ("Bicycle", 1);
         dictionary.Add ("Bus", 2);
         ....
    }
    public decimal CostOfTravel(string transportMethod)
    {
         return  (decimal) (multiplierDictionary[transportMethod] * DistanceToDestination);       
    }
我认为

答案是某种数据库。

如果您使用某些,则运输成本计算器会向数据库询问给定传输方法的多人游戏。

数据库可以是文本文件、xml或SQL服务器。只是一个键值对。

如果你想只使用代码,有 - tmo - 没有办法避免从传输方法到多人游戏的转换(或成本)。所以需要某种手段。

对于数据库,您将字典从代码中剔除,并且不得更改代码以应用新的传输方法或更改值。

这是策略设计模式的一个案例。创建一个基类,比如TravelCostCalculator,然后为您将考虑的每种旅行模式开发类,每种模式都覆盖一个通用方法,Calculate(double) 。然后,您可以根据需要使用工厂模式实例化特定TravelCostCalculator

诀窍在于如何构建工厂(没有 switch 语句)。我这样做的方法是拥有一个静态类构造函数(public static Classname() - 不是实例构造函数),该构造函数将每个策略类注册到Dictionary<string, Type>中的工厂。

由于 C# 不能确定性地运行类构造函数(就像C++在大多数情况下那样),因此必须显式运行它们以确保它们能够运行。这可以在主程序或工厂构造函数中完成。缺点是,如果添加策略类,还必须将其添加到要运行的构造函数列表中。您可以创建必须运行的静态方法(TouchRegister),也可以使用 System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor

class Derived : Base
{
    public static Derived()
    {
        Factory.Register(typeof(Derived));
    }
}
// this could also be done with generics rather than Type class
class Factory
{
    public static Register(Type t)
    {
        RegisteredTypes[t.Name] = t;
    }
    protected Dictionary<string, Type t> RegisteredTypes;
    public static Base Instantiate(string typeName)
    {
        if (!RegisteredTypes.ContainsKey(typeName))
            return null;
        return (Base) Activator.CreateInstance(RegisteredTypes[typeName]);
    }
}

我更喜欢像这样使用 Enum

public enum TransportMethod
{
    Bicycle = 1,
    Bus = 2,
    Car = 3
}

并像这种方法一样使用它:

public decimal CostOfTravel(string transportMethod)
{
    var tmValue = (int)Enum.Parse(typeof(TransportMethod), transportMethod);
    return DistanceToDestination  * tmValue;
}

请注意,上面的方法是区分大小写的,因此您可以将第一个字符大写;

相关答案

之前说过,但我想再给相关话题一个机会。

这是一个很好的反思例子。"反射对象用于在运行时获取类型信息。提供对正在运行的程序的元数据的访问权限的类位于 System.Reflection 命名空间中。

通过使用反射,如果想要添加程序,则可以避免编译代码。您将使用配置文件即时解决问题。

我最近通过使用依赖注入解决了策略模式的类似问题,但我仍然以 switch 语句结束。它不能以这种方式解决您的问题。如果将新类型添加到字典中,泰森建议的方法仍然需要重新编译。

我正在谈论的一个例子:在 C# 中使用反射动态加载自定义配置 XML:http://technico.qnownow.com/dynamic-loading-of-custom-configuration-xml-using-reflection-in-c/

定义查找表数组 3 x 2。在与传输类型相邻的数组单元格中查找速率值。根据费率计算成本。