Apprendre Rust: Comment j'ai fait le chat UDP avec Azul



Je continue d'apprendre Rust. Je ne sais toujours pas grand-chose, donc je fais beaucoup d’erreurs. La dernière fois, j'ai essayé de faire un jeu Snake . J'ai essayé des cycles, des collections, travailler avec 3D Three.rs . En savoir plus sur ggez et améthyste . Cette fois, j'ai essayé de créer un client et un serveur pour le chat. Pour GUI utilisé Azul . A également regardé Conrod , Yew et Orbtk . J'ai essayé le multithreading, les canaux et le réseautage. J'ai pris en compte les erreurs de l'article précédent et j'ai essayé de rendre cela plus détaillé. Pour plus de détails, bienvenue au chat.

Sources, fonctionne sur Windows 10 x64

Pour le réseautage, j'ai utilisé UDP parce que je veux faire mon prochain projet en utilisant ce protocole et je voulais m'entraîner avec lui ici. Pour l'interface graphique, j'ai rapidement parcouru des projets Google sur Rust, regardé les exemples de base pour eux, et Azul m'a accroché parce qu'il utilise un modèle d'objet de document et un moteur de style CSS, et j'étais engagé dans le développement Web depuis longtemps. En général, j'ai choisi le Cadre de manière subjective. Il est, jusqu'à présent, en alpha profond: le défilement ne fonctionne pas, le focus d'entrée ne fonctionne pas, il n'y a pas de curseur. Pour saisir des données dans un champ de texte, vous devez passer la souris dessus et la maintenir directement au-dessus pendant que vous tapez. Plus de détails ...

En fait, la plupart de l'article sont des commentaires de code.

Azul


Framework GUI utilisant un style fonctionnel, DOM, CSS. Votre interface se compose d'un élément racine, qui a de nombreux descendants, qui peuvent avoir leurs propres descendants, comme en HTML et XML. L'interface entière est créée à partir des données d'un seul DataModel. Dans ce document, toutes les données sont transférées à la présentation en général. Si quelqu'un connaît ASP.NET, Azul et son DataModel sont comme Razor et son ViewModel. Comme en HTML, vous pouvez lier des fonctions aux événements des éléments DOM. Vous pouvez styliser des éléments à l'aide du cadre CSS. Ce n'est pas le même CSS que HTML, mais très similaire. Il existe également une liaison bidirectionnelle comme dans Angular ou MVVM dans WPF, UWP. Plus d'informations sur le site .

Un bref aperçu du reste des cadres


  • Orbtk - Presque le même que Azul et aussi en alpha profond
  • Conrod - Vidéo Vous pouvez créer des applications de bureau multiplateforme.
  • Yew est WebAssembly et similaire à React. Pour le développement web.

Client


La structure dans laquelle les fonctions auxiliaires de lecture et d'écriture sur un socket sont regroupées


struct ChatService {} impl ChatService { //1 fn read_data(socket: &Option<UdpSocket>) -> Option<String> { //2 let mut buf = [0u8; 4096]; match socket { Some(s) => { //3 match s.recv(&mut buf) { //4 Ok(count) => Some(String::from_utf8(buf[..count].into()) .expect("can't parse to String")), Err(e) => { //5 println!("Error {}", e); None } } } _ => None, } } //6 fn send_to_socket(message: String, socket: &Option<UdpSocket>) { match socket { //7 Some(s) => { s.send(message.as_bytes()).expect("can't send"); } _ => return, } } } 

  1. Lire les données du socket
  2. Tampon pour les données à lire à partir du socket.
  3. Appel bloquant. Ici, le flux d'exécution s'arrête jusqu'à ce que les données soient lues ou qu'un délai d'attente se produise.
  4. Nous obtenons une chaîne d'un tableau d'octets dans le codage UTF8.
  5. Nous arrivons ici si la connexion a été interrompue par un timeout ou une autre erreur s'est produite.
  6. Envoie une chaîne à une socket.
  7. Convertissez la chaîne en octets dans le codage UTF8 et envoyez les données au socket. L'écriture de données sur un socket n'est pas bloquante, c'est-à-dire le fil d'exécution continuera son travail. Si les données n'ont pas pu être envoyées, nous interrompons le programme avec le message «impossible d'envoyer».

Une structure qui regroupe des fonctions pour gérer les événements de l'utilisateur et modifier notre DataModel


 struct Controller {} //1 const TIMEOUT_IN_MILLIS: u64 = 2000; impl Controller { //2 fn send_pressed(app_state: &mut azul::prelude::AppState<ChatDataModel>, _event: azul::prelude::WindowEvent<ChatDataModel>) -> azul::prelude::UpdateScreen { //3 let data = app_state.data.lock().unwrap(); //4 let message = data.messaging_model.text_input_state.text.clone(); data.messaging_model.text_input_state.text = "".into(); //5 ChatService::send_to_socket(message, &data.messaging_model.socket); //6 azul::prelude::UpdateScreen::Redraw } //7 fn login_pressed(app_state: &mut azul::prelude::AppState<ChatDataModel>, _event: azul::prelude::WindowEvent<ChatDataModel>) -> azul::prelude::UpdateScreen { //8 use std::time::Duration; //9 if let Some(ref _s) = app_state.data.clone().lock().unwrap().messaging_model.socket { return azul::prelude::UpdateScreen::DontRedraw; } //10 app_state.add_task(Controller::read_from_socket_async, &[]); //11 app_state.add_daemon(azul::prelude::Daemon::unique(azul::prelude::DaemonCallback(Controller::redraw_daemon))); //12 let mut data = app_state.data.lock().unwrap(); //13 let local_address = format!("127.0.0.1:{}", data.login_model.port_input.text.clone().trim()); //14 let socket = UdpSocket::bind(&local_address) .expect(format!("can't bind socket to {}", local_address).as_str()); //15 let remote_address = data.login_model.address_input.text.clone().trim().to_string(); //16 socket.connect(&remote_address) .expect(format!("can't connect to {}", &remote_address).as_str()); //17 socket.set_read_timeout(Some(Duration::from_millis(TIMEOUT_IN_MILLIS))) .expect("can't set time out to read"); // 18 data.logged_in = true; // 19 data.messaging_model.socket = Option::Some(socket); //20 azul::prelude::UpdateScreen::Redraw } //21 fn read_from_socket_async(app_data: Arc<Mutex<ChatDataModel>>, _: Arc<()>) { //22 let socket = Controller::get_socket(app_data.clone()); loop { //23 if let Some(message) = ChatService::read_data(&socket) { //24 app_data.modify(|state| { //25 state.messaging_model.has_new_message = true; //26 state.messaging_model.messages.push(message); }); } } } //27 fn redraw_daemon(state: &mut ChatDataModel, _repres: &mut azul::prelude::Apprepres) -> (azul::prelude::UpdateScreen, azul::prelude::TerminateDaemon) { //28 if state.messaging_model.has_new_message { state.messaging_model.has_new_message = false; (azul::prelude::UpdateScreen::Redraw, azul::prelude::TerminateDaemon::Continue) } else { (azul::prelude::UpdateScreen::DontRedraw, azul::prelude::TerminateDaemon::Continue) } } //29 fn get_socket(app_data: Arc<Mutex<ChatDataModel>>) -> Option<UdpSocket> { //30 let ref_model = &(app_data.lock().unwrap().messaging_model.socket); //31 match ref_model { Some(s) => Some(s.try_clone().unwrap()), _ => None } } } 

  1. Délai d'expiration en millisecondes après lequel l'opération de blocage de la lecture à partir du socket sera interrompue.
  2. La fonction est remplie lorsque l'utilisateur souhaite envoyer un nouveau message au serveur.
  3. Nous prenons possession du mutex avec notre modèle de données. Cela bloque le thread de redessin d'interface jusqu'à ce que le mutex soit libéré.
  4. Nous faisons une copie du texte entré par l'utilisateur pour le transférer plus loin et effaçons le champ de saisie de texte.
  5. Nous envoyons un message.
  6. Nous informons le Framework qu'après avoir traité cet événement, nous devons redessiner l'interface.
  7. La fonction fonctionne lorsque l'utilisateur souhaite se connecter au serveur.
  8. Nous connectons la structure pour représenter la durée de la bibliothèque standard.
  9. Si nous sommes déjà connectés au serveur, nous interrompons l'exécution de la fonction et informons le Framework qu'il n'est pas nécessaire de redessiner l'interface.
  10. Ajoutez une tâche qui sera exécutée de manière asynchrone dans le thread à partir du pool de threads du cadre Azul. L'accès au mutex avec le modèle de données bloque la mise à jour de l'interface utilisateur jusqu'à ce que le mutex soit libéré.
  11. Ajoutez une tâche récurrente qui s'exécute dans le thread principal. Tous les calculs longs dans ce démon sont bloqués par la mise à jour de l'interface.
  12. Nous entrons en possession du mutex.
  13. Nous lisons le port entré par l'utilisateur et créons une adresse locale en fonction de celui-ci, nous l'écouterons.
  14. Créez un socket UDP qui lit les paquets arrivant à l'adresse locale.
  15. Nous lisons l'adresse du serveur saisie par l'utilisateur.
  16. Nous demandons à notre socket UDP de lire les paquets uniquement à partir de ce serveur.
  17. Définissez le délai d'expiration de l'opération de lecture à partir du socket. L'écriture dans le socket se produit sans attendre, c'est-à-dire que nous écrivons simplement des données et n'attendons rien, et l'opération de lecture à partir du socket bloque le flux et attend jusqu'à ce que les données pouvant être lues arrivent. Si vous ne définissez pas de délai d'expiration, l'opération de lecture à partir du socket attendra indéfiniment.
  18. Définissez un indicateur indiquant que l'utilisateur s'est déjà connecté au serveur.
  19. Nous passons le socket créé au modèle de données.
  20. Nous informons le Framework qu'après avoir traité cet événement, nous devons redessiner l'interface.
  21. Une opération asynchrone qui s'exécute dans le pool de threads du cadre Azul.
  22. Obtenez une copie du socket à partir de notre modèle de données.
  23. Essayer de lire des données à partir d'un socket. Si vous ne faites pas de copie du socket et attendez directement ici jusqu'à ce qu'un message arrive du socket qui se trouve dans le mutex dans notre modèle de données, alors l'interface entière cessera d'être mise à jour jusqu'à ce que nous libérions le mutex.
  24. Si nous obtenons une sorte de message, puis en modifiant notre modèle de données, modifier fait la même chose que lock (). Déballer () en passant le résultat au lambda et en libérant le mutex après la fin du code lambda.
  25. Définissez un indicateur pour indiquer que nous avons un nouveau message.
  26. Ajoutez un message à la liste de tous les messages de discussion.
  27. Une opération synchrone répétitive s'exécutant dans le thread principal.
  28. Si nous avons un nouveau message, nous informons le Framework que nous devons redessiner l'interface à partir de zéro et continuer à travailler avec ce démon; sinon, nous ne dessinerons pas l'interface depuis le début, mais nous appellerons toujours cette fonction dans le cycle suivant.
  29. Crée une copie de notre socket afin de ne pas verrouiller le mutex avec notre modèle de données.
  30. Nous obtenons le mutex et obtenons un lien vers la socket.
  31. Créez une copie du socket. Mutex sera libéré automatiquement à la sortie d'une fonction.

Traitement des données et démons asynchrones à Azul


 // Problem - blocks UI :( fn start_connection(app_state: &mut AppState<MyDataModel>, _event: WindowEvent<MyDataModel>) -> UpdateScreen { //   app_state.add_task(start_async_task, &[]); //  app_state.add_daemon(Daemon::unique(DaemonCallback(start_daemon))); UpdateScreen::Redraw } fn start_daemon(state: &mut MyDataModel, _repres: &mut Apprepres) -> (UpdateScreen, TerminateDaemon) { // UI    thread::sleep(Duration::from_secs(10)); state.counter += 10000; (UpdateScreen::Redraw, TerminateDaemon::Continue) } fn start_async_task(app_data: Arc<Mutex<MyDataModel>>, _: Arc<()>) { // simulate slow load app_data.modify(|state| { // UI    thread::sleep(Duration::from_secs(10)); state.counter += 10000; }); } 

Le démon est toujours exécuté dans le thread principal, donc le blocage y est inévitable. Avec une tâche asynchrone, si vous le faites, par exemple, comme ceci, il n'y aura pas de verrouillage pendant 10 secondes.

 fn start_async_task(app_data: Arc<Mutex<MyDataModel>>, _: Arc<()>) { //  UI.  . thread::sleep(Duration::from_secs(10)); app_data.modify(|state| { state.counter += 10000; }); } 

La fonction de modification appelle lock () et le mutex avec le modèle de données bloque donc la mise à jour de l'interface pendant la durée de son exécution.

Nos styles


 const CUSTOM_CSS: &str = " .row { height: 50px; } .orange { background: linear-gradient(to bottom, #f69135, #f37335); font-color: white; border-bottom: 1px solid #8d8d8d; }"; 

En fait, Fonctions pour créer notre DOM à afficher à son utilisateur


 impl azul::prelude::Layout for ChatDataModel { //1 fn layout(&self, info: azul::prelude::WindowInfo<Self>) -> azul::prelude::Dom<Self> { //2 if self.logged_in { self.chat_form(info) } else { self.login_form(info) } } } impl ChatDataModel { //3 fn login_form(&self, info: azul::prelude::WindowInfo<Self>) -> azul::prelude::Dom<Self> { //4 let button = azul::widgets::button::Button::with_label("Login") //5 .dom() //6 .with_class("row") //7 .with_class("orange") //8 .with_callback( azul::prelude::On::MouseUp, azul::prelude::Callback(Controller::login_pressed)); //9 let port_label = azul::widgets::label::Label::new("Enter port to listen:") .dom() .with_class("row"); //10 let port = azul::widgets::text_input::TextInput::new() //11 .bind(info.window, &self.login_model.port_input, &self) .dom(&self.login_model.port_input) .with_class("row"); // 9 let address_label = azul::widgets::label::Label::new("Enter server address:") .dom() .with_class("row"); //10 let address = azul::widgets::text_input::TextInput::new() //11 .bind(info.window, &self.login_model.address_input, &self) .dom(&self.login_model.address_input) .with_class("row"); //12 azul::prelude::Dom::new(azul::prelude::NodeType::Div) .with_child(port_label) .with_child(port) .with_child(address_label) .with_child(address) .with_child(button) } //13 fn chat_form(&self, info: azul::prelude::WindowInfo<Self>) -> azul::prelude::Dom<Self> { //14 let button = azul::widgets::button::Button::with_label("Send") .dom() .with_class("row") .with_class("orange") .with_callback(azul::prelude::On::MouseUp, azul::prelude::Callback(Controller::send_pressed)); //15 let text = azul::widgets::text_input::TextInput::new() .bind(info.window, &self.messaging_model.text_input_state, &self) .dom(&self.messaging_model.text_input_state) .with_class("row"); //12 let mut dom = azul::prelude::Dom::new(azul::prelude::NodeType::Div) .with_child(text) .with_child(button); //16 for i in &self.messaging_model.messages { dom.add_child(azul::widgets::label::Label::new(i.clone()).dom().with_class("row")); } dom } } 

  1. La fonction qui crée le DOM final et est appelée à chaque fois que vous devez redessiner l'interface.
  2. Si nous sommes déjà connectés au serveur, nous affichons alors le formulaire d'envoi et de lecture des messages, sinon nous affichons le formulaire de connexion au serveur.
  3. Crée un formulaire pour entrer les données nécessaires pour se connecter au serveur.
  4. Créez un bouton avec l'inscription de texte Connexion.
  5. Convertissez-le en objet DOM.
  6. Ajoutez-y la classe de ligne.
  7. Ajoutez-y la classe css orange.
  8. Ajoutez un gestionnaire d'événements pour cliquer sur le bouton.
  9. Créez une étiquette de texte avec du texte à afficher sur la ligne utilisateur et la classe css.
  10. Nous créons une zone de texte pour saisir du texte avec du texte à partir de la propriété de notre modèle et de la ligne de classe css.
  11. Liez le champ de texte à la propriété de notre DataModel. Il s'agit d'une liaison bidirectionnelle. Maintenant, l'édition de TextInput modifie automatiquement le texte dans la propriété de notre modèle et l'inverse est également vrai. Si nous modifions le texte de notre modèle, le texte de TextInput changera.
  12. Nous créons un élément DOM racine dans lequel nous plaçons nos éléments d'interface utilisateur.
  13. Crée un formulaire pour envoyer et lire des messages.
  14. Créez un bouton avec le texte «Envoyer» et css avec les classes «ligne», «orange» et un gestionnaire d'événements lorsque vous cliquez dessus.
  15. Nous créons un champ de saisie de texte avec une liaison bidirectionnelle avec la propriété de modèle self.messaging_model.text_input_state et css avec la classe «row».
  16. Ajoutez des étiquettes de texte qui affichent des messages écrits dans le chat.

Notre modèle qui stocke l'état de notre interface


La documentation Azul indique qu'elle devrait stocker toutes les données d'application, y compris la connexion à la base de données, donc j'ai mis un socket UDP dedans.

 //1 #[derive(Debug)] //2 struct ChatDataModel { //3 logged_in: bool, //4 messaging_model: MessagingDataModel, //5 login_model: LoginDataModel, } #[derive(Debug, Default)] struct LoginDataModel { //6 port_input: azul::widgets::text_input::TextInputState, //7 address_input: azul::widgets::text_input::TextInputState, } #[derive(Debug)] struct MessagingDataModel { //8 text_input_state: azul::widgets::text_input::TextInputState, //9 messages: Vec<String>, //10 socket: Option<UdpSocket>, //11 has_new_message: bool, } 

  1. Cela nous permettra d'afficher notre structure sous forme de chaîne dans un modèle de la forme {:?}
  2. Notre modèle de données. Pour qu'il puisse être utilisé à Azul. Elle doit implémenter le trait Layout.
  3. Un drapeau pour vérifier si l'utilisateur est connecté ou non au serveur.
  4. Un modèle pour afficher un formulaire pour envoyer des messages au serveur et enregistrer les messages reçus du serveur.
  5. Modèle d'affichage du formulaire de connexion au serveur.
  6. Le port que l'utilisateur a entré. Nous allons l'écouter avec notre socket.
  7. L'adresse du serveur que l'utilisateur a entrée. Nous nous y connecterons.
  8. Message d'utilisateur. Nous l'enverrons au serveur.
  9. Un tableau de messages provenant du serveur.
  10. La prise par laquelle nous communiquons avec le serveur.
  11. Un indicateur pour vérifier si un nouveau message est arrivé du serveur.

Et enfin, le principal point d'entrée de l'application. Démarre un cycle à partir du dessin GUI et du traitement des entrées utilisateur


 pub fn run() { //1 let app = azul::prelude::App::new(ChatDataModel { logged_in: false, messaging_model: MessagingDataModel { text_input_state: azul::widgets::text_input::TextInputState::new(""), messages: Vec::new(), socket: None, has_new_message: false, }, login_model: LoginDataModel::default(), }, azul::prelude::AppConfig::default()); // 2 let mut style = azul::prelude::css::native(); //3 style.merge(azul::prelude::css::from_str(CUSTOM_CSS).unwrap()); //4 let window = azul::prelude::Window::new(azul::prelude::WindowCreateOptions::default(), style).unwrap(); //5 app.run(window).unwrap(); } 

  1. Nous créons une application avec des données de démarrage.
  2. Les styles utilisés par l'application par défaut.
  3. Ajoutez-y nos propres styles.
  4. Nous créons une fenêtre dans laquelle notre application s'affichera.
  5. Lancez l'application dans cette fenêtre.

Serveur


Le principal point d'entrée de l'application


Ici, nous avons généralement une application console.

 pub fn run() { //1 let socket = create_socket(); //2 let (sx, rx) = mpsc::channel(); //3 start_sender_thread(rx, socket.try_clone().unwrap()); loop { //4 sx.send(read_data(&socket)).unwrap(); } } 

  1. Créez un socket.
  2. Nous créons un canal à sens unique avec un expéditeur de message sx et de nombreux destinataires rx.
  3. Nous commençons à envoyer des messages à tous les destinataires dans un flux séparé.
  4. Nous lisons les données du socket et les envoyons au flux, qui envoie des messages aux clients connectés au serveur.

Fonction pour créer un flux pour envoyer des messages aux clients


 fn start_sender_thread(rx: mpsc::Receiver<(Vec<u8>, SocketAddr)>, socket: UdpSocket) { //1 thread::spawn(move || { //2 let mut addresses = Vec::<SocketAddr>::new(); //3 loop { //4 let (bytes, pre) = rx.recv().unwrap(); // 5 if !addresses.contains(&pre) { println!(" {} connected to server", pre); addresses.push(pre.clone()); } //6 let result = String::from_utf8(bytes) .expect("can't parse to String") .trim() .to_string(); println!("received {} from {}", result, pre); //7 let message = format!("FROM: {} MESSAGE: {}", pre, result); let data_to_send = message.as_bytes(); //8 addresses .iter() .for_each(|s| { //9 socket.send_to(data_to_send, s) //10 .expect(format!("can't send to {}", pre).as_str()); }); } }); } 

  1. Commencez un nouveau fil. déplacer signifie que les variables prennent le lambda et le flux, respectivement. Plus précisément, notre nouveau thread «absorbera» les variables rx et socket.
  2. Une collection d'adresses qui nous sont connectées par les clients. Nous leur enverrons tous nos messages. En général, dans un projet réel, il serait nécessaire de gérer la déconnexion d'un client de nous et la suppression de son adresse de ce tableau.
  3. Nous commençons une boucle infinie.
  4. Nous lisons les données du canal. Ici, le flux sera bloqué jusqu'à l'arrivée de nouvelles données.
  5. S'il n'y a pas une telle adresse dans notre tableau, ajoutez-la là.
  6. Décodez une chaîne UTF8 à partir d'un tableau d'octets.
  7. Nous créons un tableau d'octets que nous allons envoyer à tous nos clients.
  8. Nous passons par la collecte d'adresses et envoyons des données à tout le monde.
  9. L'opération d'écriture sur le socket UDP n'est pas bloquante, donc ici la fonction n'attendra pas que le message arrive au destinataire et s'exécute presque instantanément.
  10. attendre en cas d'erreur fera une sortie d'urgence du programme avec le message donné.

La fonction crée un socket basé sur l'entrée utilisateur


 const TIMEOUT_IN_MILLIS: u64 = 2000; fn create_socket() -> UdpSocket { println!("Enter port to listen"); //1 let local_port: String = read!("{}\n"); let local_address = format!("127.0.0.1:{}", local_port.trim()); println!("server address {}", &local_address); //2 let socket = UdpSocket::bind(&local_address.trim()) .expect(format!("can't bind socket to {}", &local_address).as_str()); //3 socket.set_read_timeout(Some(Duration::from_millis(TIMEOUT_IN_MILLIS))) .expect("can't set time out to read"); //4 socket } 

  1. Nous lisons le port que notre serveur écoutera et créons une adresse de serveur local sur cette base.
  2. Créez un socket UDP en écoutant cette adresse.
  3. Définissez le délai d'expiration de l'opération de lecture. L'opération de lecture bloque et bloquera le flux jusqu'à l'arrivée de nouvelles données ou jusqu'à l'expiration d'un délai.
  4. Nous renvoyons le socket créé à partir de la fonction.
  5. La fonction lit les données du socket et les renvoie avec l'adresse de l'expéditeur.

Fonction pour lire les données d'une socket


 fn read_data(socket: &UdpSocket) -> (Vec<u8>, SocketAddr) { //1 let mut buf = [0u8; 4096]; //2 loop { match socket.recv_from(&mut buf) { //3 Ok((count, address)) => { //4 return (buf[..count].into(), address); } //5 Err(e) => { println!("Error {}", e); continue; } }; } } 

  1. Le tampon est l'endroit où nous lirons les données.
  2. Démarre une boucle qui s'exécutera jusqu'à ce que des données valides soient lues.
  3. Nous obtenons le nombre d'octets lus et l'adresse de l'expéditeur.
  4. Nous coupons le tableau depuis son début au nombre d'octets lus et le convertissons en un vecteur d'octets.
  5. Si un délai d'attente ou une autre erreur se produit, passez à l'itération suivante de la boucle.

À propos des calques dans l'application


Offtopic: Un petit programme éducatif pour deux juin au travail. J'ai décidé de le mettre ici, peut-être que quelqu'un vous sera utile. Les poètes pointus de juin sont des exemples en C # et nous parlons d'ASP.NET
Donc, il n'y avait rien à faire, c'était le soir, et j'ai décidé d'écrire un petit programme pédagogique sur l'architecture pour Artem et Victor. Eh bien, allons-y.

En fait, j'ai ajouté ici parce que le mode est la reconnaissance et je ne peux écrire des articles qu'une fois par semaine, et le matériel est déjà là et la semaine prochaine, je voulais télécharger autre chose sur Habr.

En règle générale, une application est en couches. Dans chaque couche, il y a des objets qui implémentent la caractéristique de comportement de la couche dans laquelle ils se trouvent. Et ainsi. Ce sont les couches.

  1. Couche de présentation.
  2. Logique métier en couches.
  3. Couche d'accès aux données.
  4. Entités (utilisateur, animal, etc.)


Chaque couche peut contenir son propre DTO et des classes complètement arbitraires avec des méthodes arbitraires. L'essentiel est qu'ils exécutent les fonctionnalités associées à la couche dans laquelle ils se trouvent. Dans les applications simples, certaines couches peuvent être manquantes. Par exemple, une vue de couche peut être implémentée via le modèle MVC, MVP, MVVM. Ce qui est complètement facultatif. L'essentiel est que les classes qui se trouvent dans cette couche implémentent les fonctionnalités assignées à la couche. N'oubliez pas que les motifs et l'architecture ne sont que des recommandations et non des directions. Le motif et l'architecture ne sont pas une loi, c'est un conseil.

Et donc, nous considérerons chaque couche sur l'exemple d'une application ASP.NET standard utilisant le standard Entity Framework.

Couche de présentation


Nous avons MVC ici. Il s'agit de la couche qui fournit une interaction avec l'utilisateur. Les commandes viennent ici et les utilisateurs obtiennent des données d'ici. Pas nécessairement des gens, si nous avons une API, alors notre utilisateur est un programme différent. Les voitures communiquent avec les voitures.

Couche logique métier


Ici, généralement, les classes sont appelées Service, par exemple, UserService, bien qu'il puisse s'agir de n'importe quoi. Juste un ensemble de classes avec des méthodes. L'essentiel est que les calculs et les calculs de notre application aient lieu ici. Il s'agit de la couche la plus épaisse et la plus volumineuse. Il y a la plupart du code et diverses classes. C'est en fait notre application.

Couche d'accès aux données


Habituellement, ici EF implémente les modèles d'unité de travail et de référentiel. Donc oui, DbContext est, vous pouvez dire, Unit Of Work, et DB le définit comme Repository. En fait, c'est l'endroit où nous mettons les données et d'où nous les obtenons. Que la source de données soit une base de données, une API d'une autre application, un cache en mémoire ou simplement une sorte de générateur de nombres aléatoires. N'importe quelle source de données.

Entités


Oui, juste toutes sortes d'utilisateurs, d'animaux et plus encore. Un point important - ils peuvent avoir une sorte de comportement caractéristique seulement d'eux. Par exemple:

 class User { public string FirstName { get; set; } public string LastName { get; set; } public string FullName { get { return FirstName + " " + LastName; } } public bool Equal(User user) { return this.FullName == user.FullName; } } 

Eh bien, et un exemple très simple. Shoba était


 using System; using System.Collections.Generic; using System.Text; //Entities class User { public int Id { get; set; } public string Name { get; set; } } //Data Access Layer class UserRepository { private readonly Dictionary<int, User> _db; public UserRepository() { _db = new Dictionary<int, User>(); } public User Get(int id) { return _db[id]; } public void Save(User user) { _db[user.Id] = user; } } //Business Logic Layer class UserService { private readonly UserRepository _repo; private int _currentId = 0; public UserService() { _repo = new UserRepository(); } public void AddNew() { _currentId++; var user = new User { Id = _currentId, Name = _currentId.ToString() }; _repo.Save(user); } public string GetAll() { StringBuilder sb = new StringBuilder(); for (int i = 1; i <= _currentId; i++) { sb.AppendLine($"Id: {i} Name: {_repo.Get(i).Name}"); } return sb.ToString(); } } //presentation Layer aka Application Layer class UserController { private readonly UserService _service; public UserController() { _service = new UserService(); } public string RunExample() { _service.AddNew(); _service.AddNew(); return _service.GetAll(); } } namespace ConsoleApp1 { class Program { static void Main(string[] args) { var controller = new UserController(); Console.WriteLine(controller.RunExample()); Console.ReadLine(); } } } 


PS


Bien sho, je veux dire merci à mon Nastya pour avoir corrigé les erreurs grammaticales dans l'article. Alors oui, Nastya tu n'es pas en vain avec un diplôme rouge et généralement cool. Je t'aime <3.

Source: https://habr.com/ru/post/fr433624/


All Articles