У вас хорошее начало, но, как сказал Джон, в настоящее время оно не безопасно для типов; в преобразователе нет проверки ошибок, чтобы убедиться, что полученное десятичное число является значением в градусах Цельсия.
Итак, чтобы продолжить, я бы начал вводить структурные типы, которые принимают числовое значение и применяют его к единице измерения. В шаблонах архитектуры предприятия (также называемых шаблонами проектирования «бригада четырех») это называется шаблоном «деньги» после наиболее распространенного использования для обозначения количества типа валюты. Шаблон действует для любой числовой суммы, которая требует значимости единицы измерения.
Пример:
public enum TemperatureScale
{
Celsius,
Fahrenheit,
Kelvin
}
public struct Temperature
{
decimal Degrees {get; private set;}
TemperatureScale Scale {get; private set;}
public Temperature(decimal degrees, TemperatureScale scale)
{
Degrees = degrees;
Scale = scale;
}
public Temperature(Temperature toCopy)
{
Degrees = toCopy.Degrees;
Scale = toCopy.Scale;
}
}
Теперь у вас есть простой тип, который вы можете использовать для обеспечения того, чтобы выполняемые вами преобразования принимали температуру надлежащего масштаба и возвращали результат, температура которого, как известно, находится в другом масштабе.
Вашим Funcs понадобится дополнительная строка, чтобы проверить, соответствует ли ввод выводу; вы можете продолжать использовать лямбды или сделать еще один шаг вперед с помощью простого паттерна стратегии:
public interface ITemperatureConverter
{
public Temperature Convert(Temperature input);
}
public class FahrenheitToCelsius:ITemperatureConverter
{
public Temperature Convert(Temperature input)
{
if (input.Scale != TemperatureScale.Fahrenheit)
throw new ArgumentException("Input scale is not Fahrenheit");
return new Temperature(input.Degrees * 5m / 9m - 32, TemperatureScale.Celsius);
}
}
//Implement other conversion methods as ITemperatureConverters
public class TemperatureConverter
{
public Dictionary<Tuple<TemperatureScale, TemperatureScale>, ITemperatureConverter> converters =
new Dictionary<Tuple<TemperatureScale, TemperatureScale>, ITemperatureConverter>
{
{Tuple.Create<TemperatureScale.Fahrenheit, TemperatureScale.Celcius>,
new FahrenheitToCelsius()},
{Tuple.Create<TemperatureScale.Celsius, TemperatureScale.Fahrenheit>,
new CelsiusToFahrenheit()},
...
}
public Temperature Convert(Temperature input, TemperatureScale toScale)
{
if(!converters.ContainsKey(Tuple.Create(input.Scale, toScale))
throw new InvalidOperationException("No converter available for this conversion");
return converters[Tuple.Create(input.Scale, toScale)].Convert(input);
}
}
Поскольку эти типы преобразований являются двусторонними, вы можете рассмотреть возможность настройки интерфейса для обработки в обоих направлениях с помощью метода «ConvertBack» или аналогичного, который будет измерять температуру по шкале Цельсия и преобразовывать в градусы Фаренгейта. Это уменьшает количество ваших классов. Тогда вместо экземпляров классов значения вашего словаря могут быть указателями на методы экземпляров преобразователей. Это несколько усложняет настройку основного средства выбора стратегии TemperatureConverter, но уменьшает количество классов стратегии преобразования, которые вы должны определить.
Также обратите внимание, что проверка ошибок выполняется во время выполнения, когда вы на самом деле пытаетесь выполнить преобразование, что требует тщательного тестирования этого кода во всех случаях, чтобы убедиться, что он всегда корректен. Чтобы избежать этого, вы можете получить базовый класс температуры для создания структур CelsiusTempera и FahrenheitTempera, которые просто определяют их масштаб как постоянное значение. Затем ITemperaConverter можно сделать универсальным для двух типов, обоих температур, что даст вам возможность проверить во время компиляции, что вы указываете преобразование, которое вы считаете нужным. Кроме того, температурный преобразователь можно настроить для динамического поиска преобразователей ITemperaConverter, определения типов, между которыми они будут преобразовываться, и автоматической настройки словаря преобразователей, чтобы вам не приходилось беспокоиться о добавлении новых. Это происходит за счет увеличения количества классов на основе температуры; вам понадобится четыре класса домена (базовый и три производных класса) вместо одного. Это также замедлит создание класса TemperatureConverter, поскольку код для рефлексивной сборки словаря конвертера будет использовать немало отражений.
Вы также можете изменить перечисления единиц измерения, чтобы они стали «классами маркеров»; пустые классы, которые не имеют никакого значения, кроме того, что они принадлежат этому классу и являются производными от других классов. Затем вы можете определить полную иерархию классов «UnitOfMeasure», которые представляют различные единицы измерения и могут использоваться в качестве аргументов и ограничений универсального типа; ITemperaConverter может быть универсальным для двух типов, оба из которых ограничены классами TemperatureScale, а реализация CelsiusFahrenheitConverter закроет универсальный интерфейс для типов CelsiusDegrees и FahrenheitDegrees, полученных из TemperatureScale. Это позволяет вам самим выставлять единицы измерения в качестве ограничений конверсии, что, в свою очередь, допускает конвертации между типами единиц измерения (определенные единицы определенных материалов имеют известные конверсии; 1 британская имперская пинта воды весит 1,25 фунта).
Все это проектные решения, которые упростят один тип изменений в этом проекте, но с некоторыми затратами (либо затрудняя выполнение чего-либо еще, либо снижая производительность алгоритма). Вам решать, что действительно «легко» для вас, в контексте общей среды приложения и кодирования, в которой вы работаете.
РЕДАКТИРОВАТЬ: Использование, которое вы хотите, из вашего редактирования, очень легко для температуры.Однако, если вам нужен универсальный UnitConverter, который может работать с любым UnitofMeasure, вы больше не хотите, чтобы Enums представляли ваши единицы измерения, потому что Enums не может иметь пользовательскую иерархию наследования (они наследуются непосредственно от System.Enum).
Вы можете указать, что конструктор по умолчанию может принимать любой Enum, но тогда вы должны убедиться, что Enum является одним из типов, который является единицей измерения, в противном случае вы можете передать значение DialogResult, и конвертер будет сбиваться.out во время выполнения.
Вместо этого, если вам нужен один UnitConverter, который может конвертировать в любые заданные лямбда-единицы UnitOfMeasure для других единиц измерения, я бы указал единицы измерения как «классы маркеров»;маленькие «токены» без состояния, которые имеют смысл только в том смысле, что они являются их собственным типом и наследуются от их родителей:
//The only functionality any UnitOfMeasure needs is to be semantically equatable
//with any other reference to the same type.
public abstract class UnitOfMeasure:IEquatable<UnitOfMeasure>
{
public override bool Equals(UnitOfMeasure other)
{
return this.ReferenceEquals(other)
|| this.GetType().Name == other.GetType().Name;
}
public override bool Equals(Object other)
{
return other is UnitOfMeasure && this.Equals(other as UnitOfMeasure);
}
public override operator ==(Object other) {return this.Equals(other);}
public override operator !=(Object other) {return this.Equals(other) == false;}
}
public abstract class Temperature:UnitOfMeasure {
public static CelsiusTemperature Celsius {get{return new CelsiusTemperature();}}
public static FahrenheitTemperature Fahrenheit {get{return new CelsiusTemperature();}}
public static KelvinTemperature Kelvin {get{return new CelsiusTemperature();}}
}
public class CelsiusTemperature:Temperature{}
public class FahrenheitTemperature :Temperature{}
public class KelvinTemperature :Temperature{}
...
public class UnitConverter
{
public UnitOfMeasure BaseUnit {get; private set;}
public UnitConverter(UnitOfMeasure baseUnit) {BaseUnit = baseUnit;}
private readonly Dictionary<UnitOfMeasure, Func<decimal, decimal>> converters
= new Dictionary<UnitOfMeasure, Func<decimal, decimal>>();
public void AddConverter(UnitOfMeasure measure, Func<decimal, decimal> conversion)
{ converters.Add(measure, conversion); }
public void Convert(UnitOfMeasure measure, decimal input)
{ return converters[measure](input); }
}
Вы можете включить проверку ошибок (убедитесь, что для единицы ввода задано преобразование, проверьтечто добавляемое преобразование предназначено для UOM с тем же родителем, что и базовый тип, и т. д. и т. д.), как вы считаете нужным.Вы также можете получить UnitConverter для создания TemperatureConverter, позволяя добавлять статические проверки типа во время компиляции и избегать проверок во время выполнения, которые должен использовать UnitConverter.