重构类摆脱开关案例

时间:2016-02-25 13:31:46

标签: c#

假设我有一个这样的课程来计算不同距离的不同交通方式的旅行费用:

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();
        }
    }
}

任何提示都非常感谢!

11 个答案:

答案 0 :(得分:37)

你可以这样做:

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);
}

编辑:评论中提到,如果在不更新代码的情况下需要更改,则不允许修改等式,因此我写了一篇关于如何在此处执行此操作的帖子:{{ 3}}

答案 1 :(得分:31)

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

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

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

答案 2 :(得分:9)

听起来像是依赖注入的好候选人:

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);

答案 3 :(得分:7)

您可以像这样定义一个抽象类,并让每个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);
}

答案 4 :(得分:4)

您可以为每种旅行使用策略类。但是,您可能需要一个工厂来根据传输方法创建策略,该传输方法可能会有一个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;
    }
}
....

答案 5 :(得分:4)

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

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);       
    }

答案 6 :(得分:2)

我认为答案是某种数据库。

如果您使用某些,则TransportCostCalculator会向数据库询问指定传输方法的多人游戏。

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

如果你想使用仅代码,那么 - tmo - 无法避免从transportmethod到多人游戏(或成本)的转换。所以需要一些swicht。

使用数据库,您可以将代码放在代码之外,并且不得更改代码以应用新的传输方法或更改值。

答案 7 :(得分:1)

这是战略设计模式的一个案例。创建一个基类,比如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]);
    }
}

答案 8 :(得分:1)

我更喜欢这样使用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;
}

请注意,上述方法区分大小写,因此您可以capitalize first char;

Related Answer

答案 9 :(得分:0)

之前有人说,但我想给相关话题另一个镜头。

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

通过使用反射,如果想要添加其他开关类型(如train),则将避免编译代码。您将使用配置文件即时解决问题。

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

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

答案 10 :(得分:-1)

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