Cél és előfeltétel

Egy rendelési felületen a darabszám, az összeg és a termékkód könnyen összekeverhető, ha mindegyiket egyszerű számmal vagy szöveggel jelölöd. Ebben a leckében a típus neve is része lesz a szerződésnek. Az adatmodellezési gyakorlatra és a Result használatára építünk. A végére ellenőrzött értéket adsz át a rendszer belsejének, ahol már nem kell minden függvényben ugyanazt a feltételt újra megvizsgálni.

A név önmagában még nem határ

A típusálnevet ugyanúgy használhatod, mint az eredeti típust; egy newtype ezzel szemben külön típus. A mennyiségből nem lesz automatikusan pénzösszeg akkor sem, ha mindkettő mögött egész szám található. Ez a különbség fordításkor észrevehetővé teszi a rosszul összekötött függvényeket. A Rust Book newtype-része ezt az egységek és az elrejtett megvalósítás oldaláról is bemutatja.

Az invariáns egy olyan feltétel, amelynek minden használható példányra igaznak kell lennie. Ha egy mennyiség egy és száz közé esik, a konstruktor a nullát és a túl nagy számot elutasítja. Ehhez nem elég egy ellenőrző függvény: a belső mezőhöz való hozzáférést is korlátoznod kell. A példák ezért külön modulba teszik a típust. A modulon kívüli main csak a nyilvános felületet éri el.

Mit szabad kiengedni?

Az olvasó metódus másolható számot vagy kölcsönzött szöveget adhat vissza. A módosítható belső referencia viszont kikerülhetné a konstruktor ellenőrzését. Egy nyilvános tuple-mező ugyanezt a rést nyitná meg. A kevesebb hozzáférés itt segít abban, hogy a későbbi kód valóban bízhasson a típus jelentésében.

A konstruktor hibája várható eredmény, ezért Result vagy egy egyszerű esetben Option képviselje. A hibás adat ne pánikkal érkezzen vissza a felhasználói határra. A tesztekben használt unwrap más szerepű: azt állítja, hogy az ismert, jó próbaadatot a konstruktor elfogadja. A tulajdonjogot továbbra is a tárolt érték határozza meg; a szöveges kód saját String értéket birtokol.

Érdemes különválasztani az ábrázolást és a szerződést. A Quantity belül ugyanúgy egy kis egész szám, mint korábban, de a hívó számára már azt jelenti: a mennyiség ellenőrzött. Emiatt egy későbbi függvény kevesebb ismétlődő ellenőrzést végezhet. Ez csak akkor igaz, ha valóban minden létrehozási út ugyanazt a szabályt őrzi. Egy új konstruktor, deszerializáló vagy módosító metódus bevezetésekor ezért ugyanazt a kérdést tedd fel: hogyan marad érvényes a típus állítása? A fordító a típushatárok betartását segíti; az üzleti tartomány helyes kiválasztása továbbra is a fejlesztő feladata.

1. feladat: valódi mennyiség

Készíts olyan Quantity típust, amely egy és száz közötti u16 értéket enged meg. Az olvasója az eredeti számot adja vissza. Az ellenőrzést a létrehozáskor végezd el, és a mező legyen privát. A két határérték elfogadott; a mellettük álló nulla és százegy elutasított. Így a teszt nem csak egy kényelmes középső példát igazol.

Megoldás
mod domain {
    #[derive(Debug)]
    pub struct Quantity(u16);
    impl Quantity {
        pub fn new(n: u16) -> Option<Self> {
            (1..=100).contains(&n).then_some(Self(n))
        }
        pub fn value(&self) -> u16 { self.0 }
    }
}
fn main() {
    use domain::Quantity;
    assert_eq!(Quantity::new(1).unwrap().value(), 1);
    assert_eq!(Quantity::new(100).unwrap().value(), 100);
    assert!(Quantity::new(0).is_none());
    assert!(Quantity::new(101).is_none());
}

A konstruktor az egyetlen nyilvános bejárat. A value metódus másolatot ad, ezért a kapott szám átírása nem módosítaná a már létrehozott mennyiséget.

2. feladat: szűk termékkód

A Sku pontosan négy és nyolc közötti ASCII nagybetűt vagy számjegyet fogadjon el. Ne vágjon szóközt, ne alakítson kisbetűt nagybetűvé, és ne fogadjon el ékezetes betűt. Ez egy szándékosan szűk azonosító-formátum, nem személynév-ellenőrző. A hívó így megkülönböztetheti a saját adattisztítását a típus érvényességi szabályától.

Megoldás
mod domain {
    #[derive(Debug)]
    pub struct Sku(String);
    impl Sku {
        pub fn new(s: &str) -> Option<Self> {
            let valid = (4..=8).contains(&s.len())
                && s.bytes().all(|b| b.is_ascii_uppercase() || b.is_ascii_digit());
            valid.then(|| Self(s.to_owned()))
        }
        pub fn as_str(&self) -> &str { &self.0 }
    }
}
fn main() {
    use domain::Sku;
    assert_eq!(Sku::new("AB12").unwrap().as_str(), "AB12");
    assert!(Sku::new("ABC12345").is_some());
    for bad in ["", "abc1", "ÁB12", " AB12", "ABCDEFGHI"] {
        assert!(Sku::new(bad).is_none(), "{bad:?}");
    }
}

A bájthossz ebben a feladatban helyes választás, mert az engedélyezett ábécé ASCII. Általános emberi szövegnél ugyanez a vizsgálat más szerződést jelentene.

3. feladat: az összeadás is őrizze a típust

Adj a centben tárolt összeghez ellenőrzött összeadást. A nulla itt megengedett; az egyetlen tiltott eredmény a u64 tartományán túli összeg. A metódus új, érvényes newtype értéket adjon, túlcsorduláskor pedig None legyen az eredmény. Ne csökkentsd csendben a számot, és ne alakítsd telítődő műveletté az összeadást.

Megoldás
mod domain {
    #[derive(Debug, PartialEq, Eq)]
    pub struct Cents(u64);
    impl Cents {
        pub fn new(n: u64) -> Self { Self(n) }
        pub fn checked_add(&self, other: &Self) -> Option<Self> {
            self.0.checked_add(other.0).map(Self)
        }
    }
}
fn main() {
    use domain::Cents;
    assert_eq!(Cents::new(0).checked_add(&Cents::new(5)), Some(Cents::new(5)));
    assert_eq!(Cents::new(u64::MAX).checked_add(&Cents::new(0)), Some(Cents::new(u64::MAX)));
    assert_eq!(Cents::new(u64::MAX).checked_add(&Cents::new(1)), None);
}

A létrehozás és az összes későbbi művelet együtt őrzi az invariánst. Egy helyes konstruktor mellett egy ellenőrizetlen későbbi művelet ugyanúgy megsérthetné a felület ígéretét.

Elfogadási feltételek

  1. A mennyiség négy határpróbája sikeres, és külső kód csak a nyilvános konstruktoron keresztül hozhat létre értéket.
  2. A termékkód elfogadja az ASCII határokat, de az üres, szóközös, kisbetűs és ékezetes bemenetet elutasítja.
  3. Az összeg összeadása a legnagyobb érték mellett is ellenőrzött, és túlcsorduláskor nem ad sikeres eredményt.

Továbblépés

A newtype akkor hasznos, ha a típus neve, a létrehozás és az engedélyezett műveletek ugyanazt ígérik. A következő konverziós leckében azt tervezzük meg, mikor lehet egy átalakítás feltétlenül sikeres, és mikor kell a hibát a típussal is kimondani.