Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Praxisteil & Übungen: Module, Pfade und Crates in der Praxis

Herzlich willkommen zum Praxisteil von Kapitel 13! In größeren Softwareprojekten reicht es nicht mehr aus, den gesamten Code in einer einzigen Datei zu speichern. Wir müssen unseren Code strukturieren, logisch aufteilen und steuern, welche Teile für andere Entwickler sichtbar sind. Rust bietet uns hierfür ein mächtiges Modulsystem, Crates und sogenannte Cargo-Workspaces an.

In diesem Praxisteil bauen wir ein modulares Multi-Crate-Warenwirtschaftssystem. Wir trennen die Geschäftslogik der Lagerverwaltung (als wiederverwendbare Bibliothek / Library Crate) sauber von der eigentlichen Anwendung (als ausführbares Programm / Binary Crate).

Die Übungsaufgabe befindet sich im Verzeichnis:


1. Das Praxis-Szenario: Das modulare Warenwirtschaftssystem

Wir wollen ein System entwerfen, das Produkte verwaltet und Bestellungen abwickelt. Um den Code sauber zu halten, strukturieren wir das Projekt wie folgt:

  • inventory_lib (Library Crate): Enthält die Logik. Sie ist eine Bibliothek, die keinen Einstiegspunkt (main.rs) hat, sondern von anderen Programmen eingebunden werden kann. Sie enthält zwei Module:
    • products: Verwaltet Produktdaten (Product-Struktur).
    • orders: Verwaltet Kundenbestellungen.
  • store_app (Binary Crate): Das eigentliche ausführbare Programm, das eine Benutzerschnittstelle hat, die inventory_lib als Abhängigkeit importiert und die Funktionen aufruft.

Beide Crates organisieren wir in einem Cargo-Workspace.

Die Alltagsanalogie: Das Logistikzentrum

Wie können wir uns Module, Crates und Workspaces vorstellen? Denken Sie an ein großes Logistikzentrum:

  • Der Cargo-Workspace (Das Logistikzentrum): Das gesamte Firmengelände, das alle Hallen, Parkplätze und Zufahrten umfasst.
  • Die Crates (Die Hallen):
    • Halle 1 (Library Crate inventory_lib): Das eigentliche Hochregallager. Hier werden Waren einsortiert, erfasst und verwaltet. Es gibt keine Verkaufsstellen, sondern nur logistische Arbeitsprozesse.
    • Halle 2 (Binary Crate store_app): Der Verkaufs- und Kundenbereich. Hier kommen Kunden hinein, bestellen Waren an Bildschirmen und zahlen. Diese Halle greift auf die Regale in Halle 1 zu.
  • Die Module (Die Regale und Kisten): Innerhalb des Hochregallagers (Halle 1) gibt es getrennte Zonen: Zone A für Frischwaren (products) und Zone B für den Versandkarton-Packbereich (orders). Module strukturieren den Raum intern.
  • Die Sichtbarkeit (pub, privat):
    • Privat (Standard): Ein Mitarbeiter-Schließfach oder die interne Kaffeemaschine der Logistiker. Kunden aus Halle 2 haben hierzu absolut keinen Zutritt.
    • Öffentlich (pub): Die Laderampe. Hier können LKWs anfahren und Waren abholen. Dies ist die Schnittstelle nach außen.

2. Strukturierte Praxis-Einheiten

2.1 Get Started: Die Struktur des Workspaces

Ein Cargo-Workspace wird über eine übergeordnete Cargo.toml im Hauptverzeichnis gesteuert. Diese sagt Cargo, welche Unterverzeichnisse zum Projekt gehören.

# Cargo.toml im Workspace-Hauptverzeichnis
[workspace]
members = [
    "exercises/10_modules/inventory_lib",
    "exercises/10_modules/store_app",
]

2.2 Die Library Crate: Module und Dateien aufteilen

In Rust deklarieren wir Module mit dem Schlüsselwort mod. Wir können Module in separate Dateien auslagern.

Unsere Datei inventory_lib/src/lib.rs dient als Eingangstor der Bibliothek:

#![allow(unused)]
fn main() {
// lib.rs
// Wir deklarieren, dass es zwei öffentliche Untermodule gibt.
// Der Compiler sucht automatisch nach den Dateien 'products.rs' und 'orders.rs'.
pub mod products;
pub mod orders;
}

In inventory_lib/src/products.rs definieren wir die Datenstrukturen für Produkte:

#![allow(unused)]
fn main() {
// products.rs
pub struct Product {
    pub id: u32,
    pub name: String,
    price: f64, // ACHTUNG: Preis ist privat!
}

impl Product {
    pub fn new(id: u32, name: String, price: f64) -> Self {
        Self { id, name, price }
    }

    pub fn get_price(&self) -> f64 {
        self.price
    }
}
}
  • pub struct Product: Macht die Struktur außerhalb des Moduls zugänglich.
  • pub id / pub name: Macht diese Felder öffentlich.
  • price (ohne pub): Dieses Feld bleibt privat. Niemand von außerhalb kann product.price direkt lesen oder verändern. Der Zugriff ist nur über die öffentliche Methode get_price erlaubt.

2.3 CDD Deep Dive: Der unsichtbare Code (Sichtbarkeitsfehler)

Einer der häufigsten Fehler beim Arbeiten mit Modulen in Rust ist das Vergessen von pub bei Modulen, Strukturen oder einzelnen Strukturfeldern.

Der fehlerhafte Code:

Stellen wir uns vor, wir versuchen in unserer Binary Crate store_app/src/main.rs, auf die Produkte zuzugreifen, haben aber in lib.rs vergessen, das Modul products als öffentlich (pub mod products; statt mod products;) zu markieren.

// store_app/src/main.rs
use inventory_lib::products::Product; // FEHLER!

fn main() {
    let p = Product::new(1, String::from("Rust Lehrbuch"), 49.99);
}

Die Reaktion des Compilers:

Wenn wir das Projekt kompilieren, meldet sich der Compiler mit folgender Fehlermeldung:

error[E0603]: module `products` is private
 --> store_app/src/main.rs:2:20
  |
2 | use inventory_lib::products::Product;
  |                    ^^^^^^^^ private module
  |
note: the module `products` is defined here
 --> inventory_lib/src/lib.rs:3:1
  |
3 | mod products;
  | ^^^^^^^^^^^^^

Warum lehnt der Compiler das ab?

In Rust ist standardmäßig alles privat. Das bedeutet, dass ein Modul, eine Funktion oder eine Struktur nur innerhalb des unmittelbar übergeordneten Moduls sichtbar ist. Da wir in lib.rs lediglich mod products; geschrieben haben, darf nur die Bibliothek selbst das Modul nutzen. Die externe Binärdatei store_app hat keinen Zugriff.

Wie beheben wir das?

Wir müssen die Sichtbarkeit explizit erweitern. In inventory_lib/src/lib.rs ändern wir die Deklaration zu:

#![allow(unused)]
fn main() {
pub mod products; // Nun ist das Modul für die Außenwelt sichtbar!
}

Gleiches gilt für Strukturfelder. Wenn wir versuchen würden, p.price = 10.0 aufzurufen, würde uns der Compiler stoppen mit:

error[E0616]: field `price` of struct `Product` is private

Hier beheben wir den Fehler, indem wir entweder das Feld öffentlich machen (pub price) oder den Zustand über einen Getter/Setter manipulieren (was oft besser ist, um Invarianten zu wahren!).


3. Die vollständige Musterlösung

Das System besteht aus drei Hauptdateien im Workspace.

Datei 1: Die Bibliotheks-Wurzel inventory_lib/src/lib.rs

#![allow(unused)]
fn main() {
1:  // lib.rs - Das Einstiegstor unserer Lagerhaltungs-Bibliothek
2:  
3:  // Wir deklarieren die Untermodule und machen sie öffentlich verfügbar
4:  pub mod products;
5:  pub mod orders;
}

Datei 2: Das Produkt-Modul inventory_lib/src/products.rs

#![allow(unused)]
fn main() {
1:  // products.rs - Datenkapselung für Produkte
2:  
3:  #[derive(Debug, Clone)]
4:  pub struct Product {
5:      pub id: u32,
6:      pub name: String,
7:      price: f64, // Kapselung: Nur intern lesbar
8:  }
9:  
10: impl Product {
11:     // Öffentlicher Konstruktor
12:     pub fn new(id: u32, name: String, price: f64) -> Result<Self, String> {
13:         if price < 0.0 {
14:             return Err(String::from("Der Preis darf nicht negativ sein!"));
15:         }
16:         Ok(Self { id, name, price })
17:     }
18: 
19:     // Getter für den privaten Preis
20:     pub fn get_price(&self) -> f64 {
21:         self.price
22:     }
23: 
24:     // Setter zur kontrollierten Preisänderung
25:     pub fn set_price(&mut self, new_price: f64) -> Result<(), String> {
26:         if new_price < 0.0 {
27:             return Err(String::from("Ungültiger Preis!"));
28:         }
29:         self.price = new_price;
30:         Ok(())
31:     }
32: }
}

Datei 3: Das Bestell-Modul inventory_lib/src/orders.rs

#![allow(unused)]
fn main() {
1:  // orders.rs - Abwicklung von Produktbestellungen
2:  
3:  // Wir importieren die Product-Struktur aus dem Nachbarmodul
4:  use crate::products::Product;
5:  
6:  #[derive(Debug)]
7:  pub struct Order {
8:      pub order_id: u32,
9:      pub items: Vec<Product>,
10: }
11: 
12: impl Order {
13:     pub fn new(order_id: u32) -> Self {
14:         Self {
15:             order_id,
16:             items: Vec::new(),
17:         }
18:     }
19: 
20:     pub fn add_item(&mut self, product: Product) {
21:         self.items.push(product);
22:     }
23: 
24:     pub fn calculate_total(&self) -> f64 {
25:         self.items.iter().map(|item| item.get_price()).sum()
26:     }
27: }
}

Datei 4: Die Anwendung store_app/src/main.rs

1:  // main.rs - Ausführbare Anwendung im Workspace
2:  
3:  // Wir importieren die Schnittstellen aus unserer externen Bibliothek Crate
4:  use inventory_lib::products::Product;
5:  use inventory_lib::orders::Order;
6:  
7:  fn main() {
8:      println!("--- Willkommen im modularen Rust-Shop ---");
9:  
10:     // 1. Produkte über den sicheren Konstruktor erstellen
11:     let p1 = match Product::new(101, String::from("Rust Lehrbuch"), 49.99) {
12:         Ok(p) => p,
13:         Err(e) => {
14:             println!("Fehler beim Produkt-Setup: {}", e);
15:             return;
16:         }
17:     };
18: 
19:     let mut p2 = match Product::new(102, String::from("Koffein-Kapseln"), 9.99) {
20:         Ok(p) => p,
21:         Err(e) => {
22:             println!("Fehler beim Produkt-Setup: {}", e);
23:             return;
24:         }
25:     };
26: 
27:     // 2. Preisänderung über Setter demonstrieren
28:     println!("Alter Preis von {}: {} €", p2.name, p2.get_price());
29:     if let Err(e) = p2.set_price(11.49) {
30:         println!("Fehler beim Ändern des Preises: {}", e);
31:     } else {
32:         println!("Neuer Preis von {}: {} €", p2.name, p2.get_price());
33:     }
34: 
35:     // 3. Bestellung anlegen und Produkte hinzufügen
36:     let mut order = Order::new(5001);
37:     
38:     // Wir klonen die Produkte, da wir sie in die Bestellung verschieben (Ownership Move)
39:     order.add_item(p1.clone());
40:     order.add_item(p2.clone());
41: 
42:     // 4. Bestellwert ausgeben
43:     println!("\nBestellungs-Details:");
44:     println!("Bestellnummer: {}", order.order_id);
45:     for item in &order.items {
46:         println!(" - {}: {} €", item.name, item.get_price());
47:     }
48:     println!("Gesamtsumme: {:.2} €", order.calculate_total());
49: }

4. Anatomische Zeilenzerlegung und Detail-Analyse

Lassen Sie uns die Struktur und die Pfade im Detail analysieren:

  • lib.rs (Zeilen 4–5): pub mod products; – Durch dieses Statement teilt die Bibliothek dem Compiler mit, dass die Datei products.rs eingelesen werden soll. Das Voranstellen von pub sorgt dafür, dass externe Crates, die inventory_lib nutzen, direkt auf inventory_lib::products zugreifen dürfen.
  • products.rs (Zeile 7): price: f64 – Da hier kein pub steht, ist das Feld privat. Von außerhalb des Moduls products kann dieses Feld weder direkt gelesen (let x = p.price; schlägt fehl) noch beschrieben werden. Das ist die Grundlage für Datenkapselung. So können wir im Setter set_price (Zeilen 25–31) garantieren, dass niemals ein negativer Preis im Speicher landet.
  • orders.rs (Zeile 4): use crate::products::Product; – Wir befinden uns im Modul orders. Um das Produkt aus dem Modul products zu nutzen, verwenden wir den Pfad-Präfix crate::. Das Schlüsselwort crate verweist immer auf die Wurzel der aktuellen Crate (in diesem Fall lib.rs). Von dort navigieren wir über das öffentliche Modul products zur Struktur Product.
  • main.rs (Zeilen 4–5):
    • use inventory_lib::products::Product;
    • use inventory_lib::orders::Order;
    • Da store_app eine eigenständige Crate ist, verweist crate:: auf die Wurzel von main.rs. Um auf die Bibliothek zuzugreifen, müssen wir den Namen der Bibliothek-Crate inventory_lib als Pfadanfang verwenden. Dieser Name wird in der Cargo.toml der Bibliothek deklariert.
  • main.rs (Zeilen 39–40): order.add_item(p1.clone()); – Da die Funktion add_item das Produkt per Value (product: Product) entgegennimmt, findet ein Ownership-Move statt. Würden wir p1 nicht klonen, könnten wir es danach in main() nicht mehr für die Ausgabe in Zeile 46 verwenden. Daher nutzen wir .clone(), was eine exakte Tiefenkopie der Strukturdaten im Speicher anlegt.