Класс рефакторинга, чтобы избавиться от корпуса коммутатора

Скажем, у меня есть класс, подобный этому, для расчета стоимости путешествий на разных расстояниях с различными видами транспорта:

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

Любые советы были высоко оценены!

+34
25 февр. '16 в 13:31
источник поделиться
10 ответов

Вы можете сделать что-то вроде этого:

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

Изменить: в комментариях было упомянуто, что это не позволит изменять уравнение, если это когда-либо понадобилось для изменения без обновления кода, поэтому я написал сообщение о том, как это сделать здесь: http://structuredsight.com/2016/03/07/configuring-logic.

+37
25 февр. '16 в 13:40
источник

Мне кажется, что любое решение, основанное на вашем текущем методе, ошибочно критично: независимо от того, как вы его обрезаете, вы помещаете данные в свой код. Это означает, что каждый раз, когда вы хотите изменить любой из этих номеров, добавьте новый тип транспортного средства и т.д., Вам нужно отредактировать код, а затем перекомпилировать, распространить патч и т.д.

То, что вы действительно должны делать, это поместить эти данные там, где они есть, - в отдельный, не скомпилированный файл. Вы можете использовать XML, JSON, некоторую форму базы данных или даже простой файл конфигурации. Шифруйте его, если хотите, необязательно.

Затем вы просто напишите синтаксический анализатор, который читает файл, и создаст карту типа транспортного средства для множителя стоимости или любых других свойств, которые вы хотите сохранить. Добавление нового автомобиля будет таким же простым, как обновление файла данных. Нет необходимости редактировать код или перекомпилировать и т.д. Гораздо более надежный и простой в обслуживании, если вы планируете добавить материал в будущем.

+31
25 февр. '16 в 17:53
источник

Звучит как хороший кандидат на инъекцию зависимости:

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

Теперь создайте constrcutor для вашего калькулятора, который ожидает экземпляр 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);
+9
25 февр. '16 в 13:35
источник

Вы можете определить абстрактный класс, подобный этому, и каждый 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);
}
+7
25 февр. '16 в 13:40
источник

Вы можете использовать класс стратегии для каждого типа путешествий. Но тогда вам, вероятно, понадобится factory для создания стратегии, основанной на методе транспорта, который, скорее всего, будет иметь оператор 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;
    }
}
....
+4
25 февр. '16 в 13:35
источник

Вы можете сделать 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);       
    }
+4
25 февр. '16 в 13:41
источник

Я думаю, что ответ - это какая-то база данных.

Если вы используете некоторые, TransportCostCalculator запрашивает базу данных для многопользовательского режима данному способу транспорта.

База данных может быть текстовым файлом или xml или SQL-сервером. Просто пара ключ-значение.

Если вы хотите использовать только код, есть - tmo - никоим образом не избежать перевода с транспорта на мультиплеер (или стоимость). Поэтому нужен какой-то свихт.

В базе данных вы помещаете словарь из своего кода, и вы не должны изменять свой код для применения новых методов транспорта или изменения значений.

+2
25 февр. '16 в 13:55
источник

Это пример шаблона проектирования стратегии. Создайте базовый класс, скажем TravelCostCalculator, а затем разработайте классы для каждого вида путешествий, которые вы рассмотрите, каждый из которых переопределяет общий метод, Calculate(double). Затем вы можете создать конкретный TravelCostCalculator при необходимости с помощью шаблона factory.

Фокус в том, как построить factory (без оператора switch). То, как я это делаю, - это создать статический конструктор классов (public static Classname() - не конструктор экземпляра), который регистрирует каждый класс стратегии с помощью factory в Dictionary<string, Type>.

Так как С# детерминированно не запускает конструкторы классов (например, С++ в большинстве случаев), вы должны явно запускать их для обеспечения их запуска. Это можно сделать в основной программе или в конструкторе factory. Недостатком является то, что если вы добавите класс стратегии, вы также должны добавить его в список запускаемых конструкторов. Вы можете создать статический метод, который должен быть запущен (Touch или Register), или вы также можете использовать 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]);
    }
}
+1
25 февр. '16 в 13:59
источник

Я предпочитаю использовать 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;
}

Обратите внимание, что выше метод чувствителен к регистру, поэтому вы можете заглавные буквы char;

Связанный ответ

+1
06 мар. '16 в 7:41
источник

Ранее было сказано, но я хочу, чтобы связать тему с другим выстрелом.

Это хороший пример для размышлений. " Reflection объекты используются для получения информации о типе во время выполнения. Классы, предоставляющие доступ к метаданным запущенной программы, находятся в системе .Reflection namespace."

Используя отражение, вы избежите компиляции кода, если требуется добавить другой тип коммутатора, такой как поезд. Вы решите проблему "на лету" с помощью файла конфигурации.

Недавно я решил аналогичную проблему со стратегией patttern, используя инъекцию зависимостей, но я все еще получаю инструкцию switch. Это не решает вашу проблему таким образом. Метод, предложенный tyson, все еще нуждается в перекомпиляции, если новый словарь добавлен в словарь.

Пример того, о чем я говорю: Динамическая загрузка пользовательской конфигурации XML с использованием Reflection в С#: http://technico.qnownow.com/dynamic-loading-of-custom-configuration-xml-using-reflection-in-c/

0
29 окт. '17 в 16:10
источник

Посмотрите другие вопросы по метке или Задайте вопрос