2. Návrh a architektura

Cíl kapitoly: Umět rozdělit systém na části se zdůvodněnými hranicemi, zdokumentovat rozhodnutí a využít typový systém Rustu jako architektonický nástroj.

2.1 Co je architektura

Pragmatická definice: architektura jsou rozhodnutí, která je drahé změnit později. Jestli funkce vrací Vec nebo iterátor, změníš za pět minut — to není architektura. Jestli je storage za rozhraním, nebo je Vec natvrdo prošitý celým kódem, to určuje, kolik bude stát budoucí výměna úložiště — to architektura je.

Z toho plyne priorita návrhu: hranice před obsahem. Vnitřek modulu můžeš přepsat kdykoliv; jeho veřejné rozhraní přepisuješ za cenu úprav u všech konzumentů.

2.2 Architektonické styly

Nauč se tři základní styly a hlavně kdy který použít:

Vrstvená architektura (layered) — handler → service → repository → storage. Volání jde jen směrem dolů. Jednoduchá na pochopení, dobrá výchozí volba pro CRUD služby. Riziko: vrstvy zdegradují na „průtokový ohřívač" (každá vrstva jen deleguje níž) — pak je jich moc.

Hexagonální architektura (ports & adapters) — doménová logika uprostřed, veškerá komunikace se světem (HTTP, DB, hodiny, fronty) přes porty (rozhraní definovaná doménou) a adaptéry (implementace portů pro konkrétní technologii). Klíčová vlastnost: doména nezná HTTP ani SQL, takže se testuje bez infrastruktury. V Rustu: port = trait, adaptér = impl.

Event-driven — komponenty komunikují zprávami/událostmi místo přímých volání. Odděluje producenty od konzumentů, přirozeně škáluje, ale za cenu ztráty přehlednosti toku („kdo tohle vlastně zpracuje?") a složitějšího debugování. Sáhni po něm, když potřebuješ, aby na jednu událost reagovalo víc nezávislých částí, ne jako výchozí styl.

Pro malé a střední služby je hexagonál obvykle nejlepší poměr cena/výkon: stojí pár traitů navíc a kupuje testovatelnost. Zadání paste serveru předepisuje totéž v měkčí, vrstvené podobě: routes.rs (HTTP adaptér) → store.rs/render.rs (logika bez axum typů) — viz sekce Separation of concerns v zadání.

2.3 Dokumentace architektury: C4 model

C4 dokumentuje systém ve čtyřech úrovních přiblížení:

  1. Context — systém jako černá skříňka + uživatelé a okolní systémy
  2. Container — nasaditelné jednotky (binárka, DB, proxy) a komunikace mezi nimi
  3. Component — hlavní moduly uvnitř jedné binárky a jejich závislosti
  4. Code — třídy/structy; tuhle úroveň většinou nekresli, zastarává okamžitě a generuje ji IDE

Pro malý projekt stačí úrovně 1–3, každá jako jeden diagram (Mermaid v markdownu přímo v repu). Pravidlo: diagram, který nikdo neaktualizuje, je horší než žádný — proto málo diagramů, blízko kódu, v gitu. Konkrétní příklady pro paste server jsou v následující sekci.

2.4 Diagramy v praxi: paste server

Tři diagramy, které pro projekt velikosti paste serveru stojí za nakreslení. První vzniká už při analýze, druhé dva při návrhu. Všechny jsou Mermaid — text v gitu, verzovaný a diffovatelný jako kód.

Diagram případů užití (use case)

Odpovídá na otázku „kdo se systémem co dělá" — je to vizuální souhrn funkčních požadavků ze zadání (sekce 1 a 2). Mermaid nemá nativní use case notaci, flowchart s aktérem a elipsami je ustálená náhrada:

Paste serverinclude: při plné kapacitěincludeUživatelZobrazit formulářGET /Vytvořit pastePOST /paste — form i JSONZobrazit pasteGET /paste/:uuidProvést decrement-LRUevikciRenderovat dle mimetypeplain/html/md/binární

Všimni si, co diagram dělá: evikce a rendering nejsou samostatné akce uživatele — jsou to include vztahy, tedy chování, které systém vykoná uvnitř jiného případu užití. Přesně tohle rozlišení („uživatel evikci nevolá, evikce se děje") je informace, kterou seznam požadavků nese hůř než obrázek.

ERD (datový model)

Paste server má triviální model — o to lépe se na něm ukazuje, co do ERD patří: entity, atributy s typy, klíče a kardinality vztahů. Model odpovídá structu Paste ze zadání:

obsahuje (0 az MAX_PASTES)PASTE_STOREusizecapacityMAX_PASTES, konfiguraceu64tickglobalni generace pristupuPASTEUuididPKUUID v4bytescontenttextovy i binarni obsahMimeKindmimetypePlainText / Html / Markdown / OctetStreamu32hitspocitadlo pro decrement-LRUu64last_seen_tickgenerace posledniho pristupu

Dvě věci k povšimnutí. Zaprvé tick žije ve store, ne v paste — je to globální čítač, jehož hodnota se při přístupu kopíruje do last_seen_tick položky; ERD tuhle zodpovědnost ukazuje na první pohled. Zadruhé ERD není obrázek databázových tabulek — tady žádná databáze není (úložiště je Vec v paměti), a diagram je přesto užitečný: modeluje pojmy a vztahy, ne persistence technologii.

Komponentový diagram (C4 úroveň 3)

Moduly binárky a směr závislostí — odpovídá struktuře src/ navržené v zadání:

paste-server binárkaHTTPKlientprohlížeč / curlroutes.rsaxum handlery: HTTP <-> doménastore.rsPasteStore + decrement-LRUrender.rspulldown-cmark, syntect, mermaid→SVGmodel.rsPaste, MimeKind, AppError

Kontrolní otázky nad tímhle diagramem: Vedou všechny šipky správným směrem? (store.rs nesmí ukazovat na routes.rs — doména nezná HTTP.) Nechybí šipka, která v kódu reálně existuje? Diagram, který lže, je horší než žádný — proto ho drž malý a při změně závislostí aktualizuj v tomtéž PR.

2.5 Architecture Decision Records (ADR)

Každé netriviální rozhodnutí zapiš jako krátký ADR — jeden markdown soubor (docs/adr/0002-vec-only-storage.md). Příklad nad reálným rozhodnutím ze zadání paste serveru:

# ADR-0002: Úložiště pouze nad Vec, lineární vyhledávání

## Status: accepted

## Kontext
Zadání zakazuje asociativní kolekce (HashMap, BTreeMap,
VecDeque…) — cílem cvičení je algoritmizace a práce
s indexy a vlastnictvím (zadání, sekce 3, akceptační
kritérium 5).

## Rozhodnutí
PasteStore drží Vec<Paste>; vyhledávání podle UUID je
lineární O(n), kandidát evikce se hledá průchodem
přes last_seen_tick.

## Důsledky
+ Splňuje výukový cíl a AK-5
+ Jednoduchá implementace, snadno testovatelná
- O(n) lookup — přijatelné pro MAX_PASTES v řádu stovek
- Při zvednutí kapacity o řády by bylo nutné rozhodnutí
  revidovat (index, HashMap) → mimo scope cvičení

ADR odpovídá na otázku, kterou si za půl roku položíš ty sám: „Proč jsme to sakra udělali takhle?" Kontext se zapomíná, kód ho nenese. Všimni si vazby na požadavky — rozhodnutí bez vazby na požadavek je podezřelé (buď chybí požadavek, nebo je rozhodnutí zbytečné).

2.6 Ownership jako architektonický nástroj

V jiných jazycích je „kdo vlastní tahle data" konvence v hlavě autora. V Rustu je to kompilátorem vynucený kontrakt — a to z ownership dělá návrhový nástroj:

  • Jeden vlastník = jedno místo zodpovědnosti. Když navrhuješ modul, začni otázkou „kdo vlastní stav?" Struktura vlastnictví je kostra architektury.
  • &T vs &mut T vs T v signatuře je architektonická výpověď: &self = jen čtu, &mut self = měním stav (a kompilátor vynutí exkluzivitu), self = konzumuji (volající to už nemá). Signatury čti a piš jako smlouvy.
  • Sdílený mutabilní stav je explicitní a drahý (Arc<Mutex<T>>) — a to je dobře. Cena v syntaxi tě nutí ptát se, jestli sdílení opravdu potřebuješ, nebo jestli stav nemá vlastnit jedna task a ostatní s ní mluvit přes kanál (message passing).

V paste serveru je vlastnická mapa jednoduchá: main vytvoří Arc<Mutex<PasteStore>> a předá ho axumu přes State — jediná instance, sdílený přístup, žádný globál (zadání to popisuje jako idiomatickou náhradu Singletonu). Zadání zároveň chce, abys uměl zdůvodnit Mutex vs. RwLock: tady i „čtení" (GET) zapisuje (hits, last_seen_tick), takže RwLock by nic nepřinesl — přesně ten typ úvahy, který patří do ADR nebo komentáře.

2.7 Moduly, crates a viditelnost

Hranice v Rustu vynucuješ třemi mechanismy, od nejměkčí po nejtvrdší:

  1. Modul (mod) — organizace uvnitř crate; pub(crate) vs pub řídí, co je vidět ven
  2. Crate v workspace — tvrdá hranice: závislosti jsou explicitní v Cargo.toml, cyklus je nemožný (kompilátor ho zakáže — architektonická výhra zadarmo)
  3. Publikovaná knihovna — hranice se semver kontraktem

Pro začátek: jeden binary crate s dobře řezanými moduly — přesně struktura main.rs / routes.rs / store.rs / render.rs / model.rs ze zadání (a z komponentového diagramu výše). Na workspace s více crates přejdi, až když chceš hranici vynutit (např. doména nesmí záviset na axum — dej doménu do vlastního crate bez té závislosti a kompilátor hlídá za tebe).

Návrh veřejného API modulu: exportuj minimum. Všechno pub je závazek; všechno pub(crate) můžeš zítra beztrestně přepsat.

2.8 Typový systém jako dokumentace a validace

Zásada: make invalid states unrepresentable — navrhni typy tak, aby neplatný stav nešel ani zkonstruovat.

Newtype místo primitivů. fn get(id: String) přijme cokoliv včetně prázdného řetězce a umožní prohodit argumenty. Newtype validuje na hranici:

pub struct PasteId(Uuid);

impl PasteId {
    pub fn parse(s: &str) -> Result<Self, InvalidId> {
        // validace tady, jednou, na hranici systému
    }
}

Zbytek kódu pracuje s PasteId a nemusí nic kontrolovat — existence hodnoty je důkaz platnosti. Tomu se říká parse, don't validate: nevaliduj opakovaně, jednou naparsuj do typu, který neplatnost neumí vyjádřit. (Zadání tenhle vzor doporučuje výslovně — sekce Newtype pattern.)

Enum místo stringů a bool kombinací. Zadání to dělá vzorově: mimetype není String, ale enum MimeKind { PlainText, Html, Markdown, OctetStream } — neplatný mimetype neprojde přes hranici parsování a match nad enumem vynutí, že žádná renderovací větev nechybí (viz Strategy pattern v zadání: strategie přes match, ne nutně přes dyn Trait).

Typestate — pokročilejší vzor, kdy přechody stavů hlídá typ (Connection<Open>Connection<Closed>), zmíněn pro úplnost; sáhni po něm, až budeš mít vzor v ruce.

2.9 Error handling jako součást návrhu

Chyby jsou API. Navrhni je vědomě:

  • Knihovní/doménový kód: konkrétní error enum přes thiserror. Volající potřebuje na varianty reagovat — v paste serveru AppError { NotFound, InvalidMimeType, PayloadTooLarge, … }.
  • Binárka/aplikační vrstva: anyhow pro chyby, na které se nereaguje, jen se propagují a logují (selhání startu, config).
  • Hranice: na okraji systému se doménové chyby mapují na protokol — v axum idiomaticky přes impl IntoResponse for AppError (NotFound → 404, PayloadTooLarge → 413). Doména nezná HTTP — mapování žije v routes.rs, ne v store.rs.
  • panic!/unwrap patří jen tam, kde je stav skutečně nemožný (a napiš do expect("...") proč) — nikdy na vstupy zvenku. Přesně to vynucuje akceptační kritérium 6 zadání: žádný panic při běžném provozu.

2.10 Trait jako port: hexagonál v Rustu

Spojení předchozích bodů do vzoru, který budeš používat pořád:

// Port — definuje ho doména, mluví jazykem domény
pub trait PasteRepository {
    fn get(&self, id: &PasteId) -> Option<Paste>;
    fn insert(&self, paste: Paste) -> Result<(), StorageError>;
}

Doménová logika závisí na traitu. VecStore (ten ze zadání) je adaptér pro produkci, testovací fake je adaptér pro testy. HTTP vrstva je taky jen adaptér — překládá requesty na volání domény. Výsledek: doménu otestuješ bez serveru, storage vyměníš bez zásahu do logiky, a závislosti tečou jedním směrem (dovnitř k doméně). Zadání to formuluje jako DIP: handlery závisí na veřejném API PasteStore, ne na detailu, že uvnitř je Vec.

Pozor na přestřelení: trait zaváděj tam, kde hranici potřebuješ (IO, čas, náhodnost, externí služby), ne kolem každé funkce. V paste serveru stačí, aby PasteStore byl konkrétní typ s čistým veřejným API — trait přidáš, až budou implementace dvě. A nezapomeň: čas je závislost — decrement-LRU používá tick (logické hodiny) místo SystemTime, což je mimochodem přesně to, co dělá evikci deterministicky testovatelnou.

2.11 Návrh veřejného API

Až budeš navrhovat rozhraní (modulu, crate, HTTP), projdi si Rust API Guidelines — checklist konvencí: pojmenování (as_/to_/into_), kdy brát &str vs String vs impl AsRef<str>, implementace standardních traitů (Debug, Clone, Default…), dokumentace každé veřejné položky. Konvence nejsou byrokracie — API, které vypadá jako std, uživatel (i ty za půl roku) použije správně bez čtení dokumentace.

Zdroje