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:
- exercises/10_modules/ (Starten Sie hier mit der Strukturierung Ihres ersten Cargo-Workspaces)
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, dieinventory_libals 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.
- Halle 1 (Library Crate
- 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(ohnepub): Dieses Feld bleibt privat. Niemand von außerhalb kannproduct.pricedirekt lesen oder verändern. Der Zugriff ist nur über die öffentliche Methodeget_priceerlaubt.
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 Dateiproducts.rseingelesen werden soll. Das Voranstellen vonpubsorgt dafür, dass externe Crates, dieinventory_libnutzen, direkt aufinventory_lib::productszugreifen dürfen.products.rs(Zeile 7):price: f64– Da hier keinpubsteht, ist das Feld privat. Von außerhalb des Modulsproductskann 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 Setterset_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 Modulorders. Um das Produkt aus dem Modulproductszu nutzen, verwenden wir den Pfad-Präfixcrate::. Das Schlüsselwortcrateverweist immer auf die Wurzel der aktuellen Crate (in diesem Falllib.rs). Von dort navigieren wir über das öffentliche Modulproductszur StrukturProduct.main.rs(Zeilen 4–5):use inventory_lib::products::Product;use inventory_lib::orders::Order;- Da
store_appeine eigenständige Crate ist, verweistcrate::auf die Wurzel vonmain.rs. Um auf die Bibliothek zuzugreifen, müssen wir den Namen der Bibliothek-Crateinventory_libals Pfadanfang verwenden. Dieser Name wird in derCargo.tomlder Bibliothek deklariert.
main.rs(Zeilen 39–40):order.add_item(p1.clone());– Da die Funktionadd_itemdas Produkt per Value (product: Product) entgegennimmt, findet ein Ownership-Move statt. Würden wirp1nicht klonen, könnten wir es danach inmain()nicht mehr für die Ausgabe in Zeile 46 verwenden. Daher nutzen wir.clone(), was eine exakte Tiefenkopie der Strukturdaten im Speicher anlegt.