Cél és előfeltétel

Több mezőt ritkán kapunk meg ugyanabban a pillanatban. Egy űrlap, parancssori kapcsolók vagy konfigurációs sorok fokozatosan állítják elő az adatot. A builder ezt az átmeneti állapotot képviseli, a kész objektum pedig csak ellenőrzés után jön létre. A konverziós gyakorlatra építve most teljes objektumok érvényességi határát tervezzük meg.

Az átmeneti hiány nem kész adat

A kötelező mezőt a builderben Option<T> jelölheti. A None jelentése az, hogy a hívó még nem adta meg az értéket. Ez különbözik a megadott, de hibás szövegtől vagy számtól. A kész típusban már ne szerepeljen ugyanez az opcionális mező, ha használat közben mindig szükség van rá. Így nem kell az összes későbbi műveletben a hiány lehetőségét újra kezelni.

A build visszatérési típusa Result<Kesz, Hiba>. Sikernél minden kötelező mező rendelkezésre áll, és az összefüggő szabályok is teljesülnek. Hibánál a félkész objektum nem szivárog ki sikeres eredményként. A példák a buildert érték szerint fogyasztják el; ez világossá teszi, hogy egy konkrét összeállítás befejeződött. Javítható, megőrzött builder is tervezhető, de az már más tulajdonjogi szerződés volna.

Alapérték és ellenőrzési sorrend

Az alapérték csak valóban opcionális döntéshez tartozzon. A feladat neve nélkül nem indítunk feladatot, az ismétlésszám azonban kaphat alapértéket. A nullával vagy üres szöveggel helyettesített kötelező mező összekeverné a hiányt egy tényleges felhasználói választással. Az Option dokumentációjában az ok_or alkalmas a hiány névvel ellátott hibává alakítására.

Előre rögzítsd azt is, hogy több hiba közül melyiket jelzed. Egy kezdő időpont nélkül az időablak végét még nem tudjuk értelmesen összevetni a kezdettel. Ezért a példák előbb a mezők meglétét, utána a helyi értékeket, végül a mezők kapcsolatát ellenőrzik. A hibák prioritása így ismételt futáskor is kiszámítható.

A settereink self értéket vesznek át és visszaadják a buildert. Ez lehetővé teszi a láncolást, de a metódushívás eredményét használni kell. Ha ugyanazt a mezőt kétszer állítod, a bemutatott szabály szerint az utolsó érték számít. Ezt se hagyd puszta megvalósítási véletlennek: a teszt rögzítheti, hogy az ismételt beállítás felülírás vagy hiba legyen.

A builder akkor hasznos, ha az átmeneti állapot önálló jelentést kap. Egy egyszerű, két kötelező argumentumú függvény helyére nem feltétlenül kell új építőobjektumot tenni. Gondold át, hogy a mezők neve, a sok opcionális beállítás vagy a fokozatos összeállítás teszi-e könnyebbé a hívást. A kész objektumot ezután kezeld stabil értékként: ne tedd ismét opcionálissá azt a mezőt, amelynek meglétét a build már bizonyította. Ha később bővül a konfiguráció, az új alapérték és az új ellenőrzés sorrendje is része lesz a felület viselkedésének. Ezt érdemes néhány célzott teszttel rögzíteni.

1. feladat: kötelező feladatnév

Készíts JobBuilder típust egy kötelező névvel. A build vágja le a név szélső whitespace karaktereit, de az üres eredményt utasítsa el. A kész Job már saját String nevet tartalmazzon. Külön hibaszöveg jelölje a meg nem adott és a megadott, de üres nevet. A jó név tisztított alakját is ellenőrizd.

Megoldás
#[derive(Default)]
struct JobBuilder { name: Option<String> }
#[derive(Debug)]
struct Job { name: String }
impl JobBuilder {
    fn name(mut self, name: &str) -> Self { self.name = Some(name.to_owned()); self }
    fn build(self) -> Result<Job, &'static str> {
        let name = self.name.ok_or("hiányzó név")?;
        let name = name.trim();
        if name.is_empty() { return Err("üres név"); }
        Ok(Job { name: name.to_owned() })
    }
}
fn main() {
    assert_eq!(JobBuilder::default().build().unwrap_err(), "hiányzó név");
    assert_eq!(JobBuilder::default().name("  ").build().unwrap_err(), "üres név");
    assert_eq!(JobBuilder::default().name(" Mentés ").build().unwrap().name, "Mentés");
}

A kész példányban nincs Option<String>. A hiányt a létrehozási határon oldottuk fel, ezért a használó már nevet kap, nem újabb ellenőrzési feladatot.

2. feladat: megnevezett alapérték

A Retries builder alapértelmezésben két újrapróbálkozást engedjen, és csak nulla és három közötti explicit értéket fogadjon el. A nulla itt tudatos választás: nincs újrapróbálkozás. A kétszer meghívott setter utolsó értéke maradjon meg. A program csak beállítást készít, sem hálózati kérést, sem tényleges ismétlést nem hajt végre.

Megoldás
struct Retries { count: u8 }
impl Default for Retries {
    fn default() -> Self { Self { count: 2 } }
}
impl Retries {
    fn count(mut self, n: u8) -> Self { self.count = n; self }
    fn build(self) -> Result<u8, &'static str> {
        if self.count <= 3 { Ok(self.count) } else { Err("túl sok próba") }
    }
}
fn main() {
    assert_eq!(Retries::default().build(), Ok(2));
    assert_eq!(Retries::default().count(0).build(), Ok(0));
    assert_eq!(Retries::default().count(1).count(3).build(), Ok(3));
    assert!(Retries::default().count(4).build().is_err());
}

Az alapérték és a megengedett tartomány külön döntés. A felhasználó által megadott nullát nem írjuk felül azzal az indokkal, hogy „üresnek” látszik.

3. feladat: összefüggő időablak

A kezdő és záró egész időpont is legyen kötelező. Azonos időpontok érvényes, nulla hosszúságú ablakot alkotnak; a vég nem előzheti meg a kezdetet. Rögzítsd a hiányzó mezők hibasorrendjét is. Nem valódi órát olvasunk: a számok egy alkalmazás saját egységeit képviselik, ezért az ellenőrzés teljesen determinisztikus.

Megoldás
#[derive(Default)]
struct WindowBuilder { start: Option<u32>, end: Option<u32> }
impl WindowBuilder {
    fn start(mut self, n: u32) -> Self { self.start = Some(n); self }
    fn end(mut self, n: u32) -> Self { self.end = Some(n); self }
    fn build(self) -> Result<(u32, u32), &'static str> {
        let start = self.start.ok_or("kezdet hiányzik")?;
        let end = self.end.ok_or("vég hiányzik")?;
        if end < start { return Err("fordított ablak"); }
        Ok((start, end))
    }
}
fn main() {
    assert_eq!(WindowBuilder::default().build(), Err("kezdet hiányzik"));
    assert_eq!(WindowBuilder::default().start(2).build(), Err("vég hiányzik"));
    assert_eq!(WindowBuilder::default().start(2).end(2).build(), Ok((2, 2)));
    assert!(WindowBuilder::default().start(3).end(2).build().is_err());
}

Elfogadási feltételek

  1. A hiányzó és az üres név megkülönböztethető, a kész feladat pedig tisztított saját szöveget tárol.
  2. Az alapérték két újrapróbálkozás, az explicit nulla megmarad, az ismételt setter viselkedését állítás ellenőrzi.
  3. Az időablak hiányzó mezői a megadott sorrendben hibáznak, az azonos végpontok elfogadottak, a fordított ablak hibás.

Továbblépés

A builder átmeneti szabadságot enged, a kész típus viszont szűkebb állapotot képvisel. A hibák és forrásláncok leckében a rövid példahibákból olyan hibafelületet építünk, amely a jelentést és az eredeti okot is megőrzi.