我开始为 SKU 实现一个实用程序,即一个产品需要多个单位。基本逻辑是:
let five = "5kg".parse::<Amount>()?;
let two = "2g".parse::<Amount>()?;
let sum = five + two;
let result = sum * 3;
let result = Weight.reduce(result)?;
assert_eq!(result, Amount::new(15006, Weight.base_unit()));
let result = Weight.split(result)?.into_iter().collect::<Vec<_>>();
assert_eq!(result, [Amount::new(15, kg()), Amount::new(6, g())]);
代码设计如下:
use std::ops::{Add, Mul};
trait Exchanger {
type Err;
fn rate(&self, unit: &Unit) -> Result<u32, Self::Err>;
fn reduce<E>(&self, expr: E) -> Result<Amount, Self::Err>
where
for<'a> E: Reduce<&'a Self>,
Self: Sized,
{
expr.reduce(self)
}
fn split<E>(&self, expr: E) -> Result<Split, Self::Err>
where
for<'a> E: Reduce<&'a Self>,
Self: Sized,
{
let base = expr.reduce(self)?;
todo!()
}
}
impl<T: Exchanger> Exchanger for &T {
type Err = T::Err;
fn rate(&self, unit: &Unit) -> Result<u32, Self::Err> {
(**self).rate(unit)
}
}
trait Reduce<E: Exchanger> {
fn reduce(self, e: E) -> Result<Amount, E::Err>;
}
trait Expression<E: Exchanger, Rhs, T>: Reduce<E> + Add<Rhs> + Mul<T> {}
// impls Expression<E, Rhs, T>
struct Amount(u32, Unit);
struct Unit(String);
// impls Expression<E, Rhs, T>
struct Sum<L, R>(L, R);
// impls Expression<E, Rhs, T>
struct Split(Vec<Amount>);
我觉得上面的设计有问题,不够优雅。
第一个是我需要的 定义一个额外的结构体
Split
,因为Exchanger::split
的返回类型可能是:Amount
或Sum
,但这在oop中很好,只需引入像Expression
这样的接口。我尝试使用 Box<dyn Expression>
但它需要通用参数,我无法在编译时提供它们。
我需要为新类型
Expression
重新实现Split
。
每当我添加实现
T
的新类型 Expression
时,我还必须为所有已实现 Add<T>
的类型实现 Expression
,
这样的组合是爆炸性的,例如:
struct NewType;
impl Add<NewType> for NewType {}
impl Add<Amount> for NewType {}
impl Add<Sum> for NewType {}
impl Add<Split> for NewType {}
impl Add<NewType> for Amount {}
impl Add<NewType> for Sum {}
impl Add<NewType> for Split {}
// impl all reference types, and so on.
impl Add<NewType> for &Amount {}
我还需要实现引用类型的所有特征,例如:
trait Trait {}
impl <T: Trait> Trait for &T{}
你能告诉我更好的方法来实现当前的设计吗? 您还可以参考github存储库查看完整代码。
如果您只需要执行您所描述的基本逻辑,我会做这样的事情:
use std::{error::Error, fmt::Display, num::ParseIntError, ops::Add, str::FromStr};
/// A type representing all units of a certain quantity (e.g kg, g, t, etc.)
pub trait Unit: Sized {
/// The base unit. All others units are a multiples of it.
fn base() -> Self;
/// The magnitude of this unit with respect to the base unit. The base unit itself hasµ
/// magnitude 1.
///
/// For example, if the base unit is the gram, then the kilogram would have a magnitude of
/// 1000.
fn magnitude(&self) -> u32;
/// All units of this quantity, in decreasing order of magnitude.
///
/// For example: [t, kg, g]
fn sorted_units() -> Vec<Self>;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Quantity<U> {
pub amount: u32,
pub unit: U,
}
impl<U> Quantity<U> {
pub fn new(amount: u32, unit: U) -> Self {
Self { amount, unit }
}
}
impl<U: Unit> Quantity<U> {
/// Splits this quantity into different units
pub fn split(&self) -> Vec<Self> {
let mut amount = self.amount * self.unit.magnitude();
let mut split = vec![];
for unit in U::sorted_units() {
let mag = unit.magnitude();
let whole = amount / mag;
if whole != 0 {
split.push(Self {
amount: whole,
unit,
})
}
amount -= whole * mag;
}
split
}
}
impl<U: Unit> Add for Quantity<U> {
type Output = Self;
/// Output always in terms of the "base unit"
fn add(self, rhs: Self) -> Self::Output {
Self {
amount: self.amount * self.unit.magnitude() + rhs.amount * rhs.unit.magnitude(),
unit: U::base(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseQuantityError<UErr> {
InvalidAmount(ParseIntError),
InvalidUnit(UErr),
}
impl<U: FromStr> FromStr for Quantity<U> {
type Err = ParseQuantityError<<U as FromStr>::Err>;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let first_non_digit_idx = s
.char_indices()
.find(|(_, c)| !c.is_ascii_digit())
.map(|(idx, _)| idx)
.unwrap_or(s.len());
let amount = s[..first_non_digit_idx]
.parse::<u32>()
.map_err(ParseQuantityError::InvalidAmount)?;
let unit = s[first_non_digit_idx..]
.parse::<U>()
.map_err(ParseQuantityError::InvalidUnit)?;
Ok(Self { amount, unit })
}
}
impl<UErr: Display> Display for ParseQuantityError<UErr> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidAmount(err) => write!(f, "failed to parse `Quantity` amount: {err}"),
Self::InvalidUnit(err) => write!(f, "failed to parse `Quantity` unit: {err}"),
}
}
}
impl<UErr: Error> Error for ParseQuantityError<UErr> {}
这种设计有多重优点:
Unit
以及可能的标准 FromStr
特征例如,用多个单位表示质量:
use std::{error::Error, fmt::Display, str::FromStr};
pub type Mass = Quantity<MassUnit>;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MassUnit {
G,
Kg,
}
impl Unit for MassUnit {
fn base() -> Self {
Self::G
}
fn magnitude(&self) -> u32 {
match self {
Self::G => 1,
Self::Kg => 1000,
}
}
fn sorted_units() -> Vec<Self> {
vec![Self::Kg, Self::G]
}
}
#[derive(Debug)]
pub struct InvalidMassUnit;
impl FromStr for MassUnit {
type Err = InvalidMassUnit;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"g" => Ok(Self::G),
"kg" => Ok(Self::Kg),
_ => Err(InvalidMassUnit),
}
}
}
impl Display for InvalidMassUnit {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "invalid mass unit")
}
}
impl Error for InvalidMassUnit {}
可以这样使用:
let five_kg = "5kg".parse::<Mass>()?;
let two_g = "2g".parse::<Mass>()?;
let sum = five_kg + two_g;
assert_eq!(sum, Mass::new(5002, MassUnit::base()));
let split = sum.split();
assert_eq!(
split,
[Mass::new(5, MassUnit::Kg), Mass::new(2, MassUnit::G)]
);
我冒昧地删除了
Sum
类型,但如果您认为需要它作为计算中的额外层,那么添加它应该不会太难。
我还假设您确实需要能够用不同的单位表示相似的数量;否则,问题几乎消失了,你可以用
newtype替换每个
Quantity<U>
类型,如下所示:
pub struct Grams(u32);
pub struct Millimeters(u32);
您可能希望保留
Quantity<U>
作为 split
的返回值,如果它不能只存在于 Display
impl 中。