Типизация километров и миль для корректности расчётов
Никак не могу поймать концепт.
Есть система учета расстояния перевозки грузов
void savePathToDb(stping cargold, int milesPath, int KmPath)
{
}
var id = "test";
var miles = 109;
var kilosl = 46;
var kilos2 = 86;
SavePathToDB(id, kilosl, miles + kilos2);
И у нас возникла проблема. Из за того что часть товаров возят по дорогам и меряют километрами, а часть по морю и меряют морскими милями, часто получается ситуация когда разработчики по ошибке путают мили с километрами и получают неверные данные.
Как нам используя знания о конструировании типов создать типизацию при которой такая ошибка будет невозможна?
Была мысль написать отдельные структуры для мили и километра как единицы. Но в этом случае всё равно можно будет присвоить некорректное значение. Буду рад любым подсказкам!
Ответы (1 шт):
У меня в проекте в аналогичных случаях делается как-то так:
Определяем типизированную длину. Чтобы не возникало соблазна сконструировать непонятно из каких единиц, прячем конструктор, вместо него выставляем наружу так называемые именованные конструкторы.
struct Distance : IComparable<Distance>, IEquatable<Distance>
{
readonly double meters;
private Distance(double meters) => this.meters = meters;
const double metersInKm = 1000.0;
const double metersInMile = 1609.344;
public static Distance FromKilometers(double km) => new Distance(km * metersInKm);
public static Distance FromMiles(double miles) => new Distance(miles * metersInMile);
public double Meters => meters;
public static Distance operator + (Distance d1, Distance d2) =>
new Distance(d1.meters + d2.meters);
#region Equals, GetHashCode and operator ==
public override bool Equals(object? obj) => obj is Distance other && Equals(other);
public bool Equals(Distance other) => meters == other.meters;
public override int GetHashCode() => HashCode.Combine(meters);
public static bool operator == (Distance left, Distance right) => left.Equals(right);
public static bool operator != (Distance left, Distance right) => !(left == right);
#endregion
#region CompareTo and operator <
public int CompareTo(Distance other) => meters.CompareTo(other.meters);
public static bool operator < (Distance left, Distance right) =>
left.CompareTo(right) < 0;
public static bool operator <= (Distance left, Distance right) =>
left.CompareTo(right) <= 0;
public static bool operator > (Distance left, Distance right) =>
left.CompareTo(right) > 0;
public static bool operator >= (Distance left, Distance right) =>
left.CompareTo(right) >= 0;
#endregion
}
Теперь ваш метод SavePathToDB должен принимать на вход не int, а Distance.
При этом Distance.FromKilometers(2) + Distance.FromMiles(1.5) вполне работает и даёт верный результат, а вот Distance.FromKilometers(2) + Weight.FromKilograms(3) не компилируется.
Кода получилось много, так что если у вас в проекте много таких вот структур, имеет смысл воспользоваться кодогенерацией.
Кстати, в братском F# единицы длины с аналогичными свойствами (включая автоматическое создание производных единиц наподобие m/s^2) встроены прямо в язык.