Entwicklung & Code

Cross-Plattform-Applikationen mit Rust 3: Fachlichkeiten und Shell-Integration


close notice

This article is also available in
English.

It was translated with technical assistance and editorially reviewed before publication.

Nach einer Einführung in das Framework Crux geht es im dritten und letzten Teil der Artikelreihe nun um fortgeschrittene Konzepte und die praktische Integration in realen Anwendungen. Die folgenden Punkte zu fachlichen Typen, Aufteilung in mehrere Apps und dem Einsatz in konkreten Shell-Technologien zeigen einen detaillierten Blick aus der Praxis.

Weiterlesen nach der Anzeige




Marcel Koch berät mit seinem siebenköpfigen Team kleine und mittelständische Unternehmen und entwickelt branchenübergreifend Cross-Platform-Apps für Desktop und Mobile sowie Webapplikationen – bevorzugt mit TypeScript, Rust, Flutter oder Java, gestützt auf CI/CD und IaC. Dabei setzt er auf pragmatische, passgenaue Lösungen, denn Software ist kein Selbstzweck. Neben soliden technischen Kenntnissen schult er in Gewaltfreier Kommunikation, Transaktionsanalyse sowie Agilität und fördert einen kritischen Blick auf Cloud Hypes. Marcel ist Speaker, Autor von Fachartikeln und Büchern und regelmäßig in Podcasts zu hören.

In langlebigen Softwareprojekten ist es ratsam, fachliche Typen unabhängig vom eingesetzten Framework zu definieren. Im Rahmen der in den vorherigen Teilen gezeigten E-Mail-App bietet sich zum Beispiel der Typ EmailAddress an: Dieser Typ kapselt die Validierung und Repräsentation einer E-Mail-Adresse und stellt sicher, dass nur gültige Werte im System verwendet werden. Die Logik zur Prüfung – etwa auf das Vorhandensein des Zeichens @ – ist direkt im Typ verankert und nicht Teil der Crux-App.

Dadurch bleibt die fachliche Logik klar von technischen Details getrennt. Sie kann unabhängig von Crux, anderen Frameworks oder der konkreten Plattform getestet, wiederverwendet und weiterentwickelt werden. Sollte sich die technische Umgebung ändern, bleibt die Kernlogik erhalten und muss nicht neu geschrieben werden.

Fachliche Typen wie EmailAddress erhöhen die Wartbarkeit und Verständlichkeit des Codes und schützen vor Fehlern, indem sie ungültige Zustände bereits beim Erstellen verhindern.

Weiterlesen nach der Anzeige

Listing 1: Fachlicher Typ EmailAddress


#[derive(Clone, PartialEq, Eq, Hash, Debug, serde::Serialize, serde::Deserialize)]
pub struct EmailAddress(String);

#[derive(thiserror::Error, Debug)]
pub enum EmailError {
    #[error("missing @")]
    MissingAt,
}

impl EmailAddress {
    /// Der Smart-Constructor garantiert eine gültige Adresse
    pub fn parse(s: impl Into) -> Result {
        let s = s.into();
        if s.contains('@') { Ok(Self(s)) } else { Err(EmailError::MissingAt) }
    }

    pub fn as_str(&self) -> &str { &self.0 }
}


Fachliche Typen wie EmailAddress lassen sich unabhängig vom Framework testen.

Listing 2: Tests für EmailAddress


#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn valid_email_is_accepted() {
        let email = EmailAddress::parse("marcel.koch@example.org");
        assert!(email.is_ok());
        assert_eq!(email.unwrap().as_str(), "marcel.koch@example.org");
    }

    #[test]
    fn invalid_email_is_rejected() {
        let email = EmailAddress::parse("marcel.kochexample.org");
        assert!(email.is_err());
        assert_eq!(email.unwrap_err(), EmailError::MissingAt);
    }
}


Der erste Test prüft, dass eine gültige E-Mail-Adresse akzeptiert und korrekt gespeichert wird. Der zweite Test stellt sicher, dass eine ungültige E-Mail-Adresse (ohne @) abgelehnt und der passende Fehler (MissingAt) zurückgegeben wird.

Wächst das Entwicklungsteam der App, kann es sinnvoll sein, das Team und auch die Anwendung in mehrere eigenständige Apps und Crates aufzuteilen. Ein Kontaktmanagement könnte die bisherige E-Mail-App erweitern und alle eingehenden Kontaktdaten speichern, aktualisieren oder löschen. Dabei bekommt das Kontaktmanagement eine eigene Crux-App, verwaltet in einem separaten Crate. Dieses separate Crate beinhaltet ein abgegrenztes Feature und wird auch als Feature-Crate bezeichnet. Jedes Feature-Crate definiert dabei seine eigenen Events, Models, ViewModels und Effekte – das Model ist also explizit Teil des jeweiligen Features. Die Haupt-App integriert die einzelnen Feature-Crates und koordiniert deren Zusammenspiel.

Listing 3: contacts (Feature-Crate)


// contacts/src/lib.rs
#[derive(Clone, Debug)]
pub struct Contact {
    pub name: String,
    pub email: EmailAddress,
}

pub enum ContactsEvent {
    AddContact(Contact),
    RemoveContact(EmailAddress),
    EditContact(EmailAddress, Contact),
}

#[derive(Default)]
pub struct ContactsModel {
    pub contacts: Vec,
}

pub struct ContactsViewModel {
    pub contacts: Vec,
}

pub enum ContactsEffect {
    ShowContactAdded(EmailAddress),
    ShowContactRemoved(EmailAddress),
}

pub fn update(event: ContactsEvent, model: &mut ContactsModel) -> Vec {
    match event {
        ContactsEvent::AddContact(contact) => {
            model.contacts.push(contact.clone());
            vec![ContactsEffect::ShowContactAdded(contact.email)]
        }
        ContactsEvent::RemoveContact(email) => {
            model.contacts.retain(|c| c.email != email);
            vec![ContactsEffect::ShowContactRemoved(email)]
        }
        ContactsEvent::EditContact(email, new_contact) => {
            if let Some(c) = model.contacts.iter_mut().find(|c| c.email == email) {
                *c = new_contact.clone();
            }
            vec![]
        }
    }
}

pub fn view(model: &ContactsModel) -> ContactsViewModel {
    ContactsViewModel {
        contacts: model.contacts.clone(),
    }
}


Das E-Mail-Feature übernimmt die Funktion der bisherigen E-Mail-App:

Listing 4: email (Feature-Crate)


// email/src/lib.rs
pub enum EmailEvent {
    SendEmail(EmailAddress),
    EmailSent(bool),
}

#[derive(Default)]
pub struct EmailModel {
    pub last_sent: Option,
}

pub struct EmailViewModel {
    pub last_sent: Option,
}

pub enum EmailEffect {
    SendEmailRequest(String),
}


Die Haupt-App aggregiert die Feature-Modelle und koordiniert die Kommunikation:

Listing 5: Haupt-App-Integration (App-Crate)


// app/src/lib.rs
use contacts::{ContactsEvent, ContactsModel, ContactsViewModel, ContactsEffect};
use email::{EmailEvent, EmailModel, EmailViewModel, EmailEffect};

pub enum AppEvent {
    Email(EmailEvent),
    Contacts(ContactsEvent),
}

pub struct AppModel {
    pub email: EmailModel,
    pub contacts: ContactsModel,
}

pub struct AppViewModel {
    pub email: EmailViewModel,
    pub contacts: ContactsViewModel,
}

pub enum AppEffect {
    Email(EmailEffect),
    Contacts(ContactsEffect),
}

pub fn update(event: AppEvent, model: &mut AppModel) -> Vec {
    match event {
        AppEvent::Email(email_event) => {
            email::update(email_event, &mut model.email)
                .into_iter().map(AppEffect::Email).collect()
        }
        AppEvent::Contacts(contacts_event) => {
            contacts::update(contacts_event, &mut model.contacts)
                .into_iter().map(AppEffect::Contacts).collect()
        }
    }
}

pub fn view(model: &AppModel) -> AppViewModel {
    AppViewModel {
        email: email::view(&model.email),
        contacts: contacts::view(&model.contacts),
    }
}


Durch diese Aufteilung bleibt die Anwendung modular, testbar und erweiterbar. Jedes Feature verwaltet seinen eigenen Zustand und lässt sich unabhängig entwickeln, testen und warten. Die Haupt-App sorgt für die Orchestrierung und das Zusammenführen der einzelnen ViewModels zu einer konsistenten Oberfläche. So entsteht eine skalierbare Architektur.



Source link

Beliebt

Die mobile Version verlassen