Rust de aprendizaje: cómo chateé UDP con Azul



Sigo aprendiendo Rust. Todavía no sé mucho, así que cometo muchos errores. La última vez intenté hacer un juego de Snake . Probé ciclos, colecciones, trabajo con 3D Three.rs . Aprendí sobre ggez y amatista . Esta vez intenté hacer un cliente y un servidor para chatear. Para la GUI se usa Azul . También vimos a Conrod , Yew y Orbtk . Traté de subprocesos múltiples, canales y redes. Tomé en cuenta los errores del artículo anterior y traté de hacer esto más detallado. Para más detalles, bienvenido a cat.

Fuentes, funciona en Windows 10 x64

Para las redes, utilicé UDP porque quiero hacer mi próximo proyecto usando este protocolo y quería entrenar con él aquí. Para la GUI, rápidamente busqué proyectos de Google en Rust, miré los ejemplos básicos para ellos, y Azul me enganchó porque usa un Modelo de objetos de documentos y un motor de estilo similar a CSS, y estuve involucrado en el desarrollo web durante mucho tiempo. En general, elegí el Marco subjetivamente. Hasta ahora, está en alfa profundo: el desplazamiento no funciona, el enfoque de entrada no funciona, no hay cursor. Para ingresar datos en un campo de texto, debe desplazarse sobre él y mantenerlo directamente encima mientras escribe. Más detalles ...

En realidad, la mayor parte del artículo son comentarios de código.

Azul


Marco GUI que usa estilo funcional, DOM, CSS. Su interfaz consta de un elemento raíz, que tiene muchos descendientes, que pueden tener sus propios descendientes, como en HTML y XML. Toda la interfaz se crea en función de los datos de un único DataModel. En él, todos los datos se transfieren a la presentación en general. Si alguien está familiarizado con ASP.NET, Azul y su DataModel son como Razor y su ViewModel. Al igual que en HTML, puede vincular funciones a eventos de elementos DOM. Puede diseñar elementos usando el marco CSS. Este no es el mismo CSS que HTML, pero es muy similar. También hay enlace bidireccional como en Angular o MVVM en WPF, UWP. Más información en el sitio .

Una breve descripción del resto de los marcos


  • Orbtk : casi lo mismo que Azul y también en alfa profundo
  • Conrod - Video Puede crear aplicaciones de escritorio multiplataforma.
  • Yew es WebAssembly y similar a React. Para desarrollo web.

Cliente


La estructura en la que se agrupan las funciones auxiliares para leer y escribir en un socket


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. Leer datos del socket
  2. Buffer para que los datos se lean desde el socket.
  3. Bloqueo de llamadas. Aquí, el hilo de ejecución se detiene hasta que se leen los datos o se produce un tiempo de espera.
  4. Obtenemos una cadena de una matriz de bytes en la codificación UTF8.
  5. Llegamos aquí si la conexión se interrumpió por un tiempo de espera u otro error.
  6. Envía una cadena a un zócalo.
  7. Convierta la cadena a bytes en la codificación UTF8 y envíe los datos al socket. Escribir datos en un socket no está bloqueando, es decir El hilo de ejecución continuará su trabajo. Si no se pudieron enviar los datos, interrumpimos el programa con el mensaje "no se puede enviar".

Una estructura que agrupa funciones para manejar eventos del usuario y modificar nuestro 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. El tiempo de espera en milisegundos después del cual se interrumpirá la operación de bloqueo de la lectura del socket.
  2. La función se cumple cuando el usuario desea enviar un nuevo mensaje al servidor.
  3. Tomamos posesión del mutex con nuestro modelo de datos. Esto bloquea el hilo de redibujo de la interfaz hasta que se libera el mutex.
  4. Hacemos una copia del texto ingresado por el usuario para transferirlo más y borrar el campo de entrada de texto.
  5. Estamos enviando un mensaje
  6. Informamos al Framework que después de procesar este evento, necesitamos volver a dibujar la interfaz.
  7. La función funciona cuando el usuario quiere conectarse al servidor.
  8. Conectamos la estructura para representar el período de tiempo desde la biblioteca estándar.
  9. Si ya estamos conectados al servidor, interrumpimos la ejecución de la función y le decimos al Framework que no hay necesidad de volver a dibujar la interfaz.
  10. Agregue una tarea que se ejecutará de forma asíncrona en el subproceso del grupo de subprocesos de Azul Framework. Acceder al mutex con el modelo de datos bloquea la actualización de la IU hasta que se libere el mutex.
  11. Agregue una tarea recurrente que se ejecuta en el hilo principal. Cualquier cálculo largo en este demonio está bloqueado por las actualizaciones de la interfaz.
  12. Nos ponemos en posesión del mutex.
  13. Leemos el puerto ingresado por el usuario y creamos una dirección local basada en él, lo escucharemos.
  14. Cree un socket UDP que lea los paquetes que llegan a la dirección local.
  15. Leemos la dirección del servidor ingresada por el usuario.
  16. Le decimos a nuestro socket UDP que lea los paquetes solo desde este servidor.
  17. Establezca el tiempo de espera para la operación de lectura desde el socket. La escritura en el socket ocurre sin esperar, es decir, simplemente escribimos datos y no esperamos nada, y la operación de lectura del socket bloquea el flujo y espera hasta que lleguen los datos que se pueden leer. Si no establece un tiempo de espera, la operación de lectura desde el socket esperará indefinidamente.
  18. Establezca un indicador que indique que el usuario ya se ha conectado al servidor.
  19. Pasamos el socket creado al modelo de datos.
  20. Informamos al Framework que después de procesar este evento, necesitamos volver a dibujar la interfaz.
  21. Una operación asincrónica que se ejecuta en el grupo de subprocesos de Azul Framework.
  22. Obtenga una copia del socket de nuestro modelo de datos.
  23. Intentando leer datos de un socket. Si no realiza una copia del socket y espera directamente aquí hasta que llegue un mensaje del socket que está en el mutex en nuestro modelo de datos, entonces toda la interfaz dejará de actualizarse hasta que lancemos el mutex.
  24. Si recibimos algún tipo de mensaje, entonces cambiar nuestro modelo de datos, modificar, hace lo mismo que lock (). Desenvolver () pasando el resultado al lambda y liberando el mutex después de que termine el código lambda.
  25. Establezca una bandera para indicar que tenemos un nuevo mensaje.
  26. Agregue un mensaje a la matriz de todos los mensajes de chat.
  27. Una operación síncrona repetitiva que se ejecuta en el subproceso principal.
  28. Si tenemos un nuevo mensaje, informamos al Framework que necesitamos volver a dibujar la interfaz desde cero y continuar trabajando con este demonio; de lo contrario, no dibujaremos la interfaz desde el principio, pero aún llamaremos a esta Función en el próximo ciclo.
  29. Crea una copia de nuestro socket para no mantener el mutex bloqueado con nuestro modelo de datos.
  30. Obtenemos el mutex y obtenemos un enlace al zócalo.
  31. Crea una copia del zócalo. Mutex se liberará automáticamente al salir de una función.

Procesamiento de datos asincrónicos y demonios en 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; }); } 

El demonio siempre se ejecuta en el hilo principal, por lo que el bloqueo es inevitable allí. Con una tarea asincrónica, si lo hace, por ejemplo, de esta manera, no habrá bloqueo durante 10 segundos.

 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 función de modificación llama a lock () y el mutex con el modelo de datos bloquea la actualización de la interfaz mientras dura su ejecución.

Nuestros estilos


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

En realidad, funciones para crear nuestro DOM para mostrar a su usuario


 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 función que crea el DOM final y se llama cada vez que necesita volver a dibujar la interfaz.
  2. Si ya estamos conectados al servidor, mostramos el formulario para enviar y leer mensajes; de lo contrario, mostramos el formulario para conectarse al servidor.
  3. Crea un formulario para ingresar los datos necesarios para conectarse al servidor.
  4. Cree un botón con la inscripción de texto Iniciar sesión.
  5. Conviértalo en un objeto DOM.
  6. Agregue la clase de fila a ella.
  7. Agregue la clase css naranja.
  8. Agregue un controlador de eventos para hacer clic en el botón.
  9. Cree una etiqueta de texto con texto para mostrar al usuario y la fila de la clase CSS.
  10. Creamos un cuadro de texto para ingresar texto con texto de la propiedad de nuestra fila de clase de modelo y CSS.
  11. Vincula el campo de texto a la propiedad de nuestro DataModel. Este es un enlace bidireccional. Ahora, editar TextInput cambia automáticamente el texto en la propiedad de nuestro modelo y lo contrario también es cierto. Si cambiamos el texto en nuestro modelo, el texto en TextInput cambiará.
  12. Creamos un elemento DOM raíz en el que colocamos nuestros elementos de la interfaz de usuario.
  13. Crea un formulario para enviar y leer mensajes.
  14. Cree un botón con el texto "Enviar" y css con las clases "fila", "naranja" y un controlador de eventos cuando haga clic.
  15. Creamos un campo de entrada de texto con enlace bidireccional con la propiedad del modelo self.messaging_model.text_input_state y css con la clase "fila".
  16. Agregue etiquetas de texto que muestren mensajes escritos en el chat.

Nuestro modelo que almacena el estado de nuestra interfaz.


La documentación de Azul dice que debería almacenar todos los datos de la aplicación, incluida la conexión a la base de datos, así que puse un socket UDP en ella.

 //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. Esto nos permitirá mostrar nuestra estructura como una cadena en una plantilla del formulario {:?}
  2. Nuestro modelo de datos. Para que se pueda usar en Azul. Ella debe implementar el rasgo de diseño.
  3. Una marca para verificar si el usuario está conectado al servidor o no.
  4. Un modelo para mostrar un formulario para enviar mensajes al servidor y guardar mensajes recibidos del servidor.
  5. Modelo para mostrar el formulario para conectarse al servidor.
  6. El puerto que ingresó el usuario. Lo escucharemos con nuestro zócalo.
  7. La dirección del servidor que ingresó el usuario. Nos conectaremos a él.
  8. Mensaje de usuario Lo enviaremos al servidor.
  9. Una matriz de mensajes que provienen del servidor.
  10. El socket a través del cual nos comunicamos con el servidor.
  11. Una bandera para verificar si un nuevo mensaje ha llegado del servidor.

Y finalmente, el principal punto de entrada a la aplicación. Inicia un ciclo desde el dibujo de la GUI y el procesamiento de entrada del usuario


 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. Creamos una aplicación con datos de inicio.
  2. Los estilos utilizados por la aplicación por defecto.
  3. Agregue nuestros propios estilos a ellos.
  4. Creamos una ventana en la que se mostrará nuestra aplicación.
  5. Inicie la aplicación en esta ventana.

Servidor


El principal punto de entrada a la aplicación.


Aquí generalmente tenemos una aplicación de consola.

 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. Crea un zócalo.
  2. Creamos un canal unidireccional con un remitente de mensajes sx y muchos destinatarios de rx.
  3. Comenzamos a enviar mensajes a todos los destinatarios en una secuencia separada.
  4. Leemos los datos del socket y los enviamos a la secuencia, que envía mensajes a los clientes conectados al servidor.

Función para crear una secuencia para enviar mensajes a clientes


 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. Comience un nuevo hilo. move significa que las variables se apoderan del lambda y el flujo, respectivamente. Más específicamente, nuestro nuevo hilo "absorberá" las variables rx y socket.
  2. Una colección de direcciones conectadas a nosotros por los clientes. Les enviaremos a todos nuestros mensajes. En general, en un proyecto real, sería necesario manejar desconectar un cliente de nosotros y eliminar su dirección de esta matriz.
  3. Comenzamos un ciclo infinito.
  4. Leemos los datos del canal. Aquí la secuencia se bloqueará hasta que lleguen nuevos datos.
  5. Si no hay tal dirección en nuestra matriz, agréguela allí.
  6. Decodifica una cadena UTF8 de una matriz de bytes.
  7. Creamos una matriz de bytes que vamos a enviar a todos nuestros clientes.
  8. Revisamos la recopilación de direcciones y enviamos datos a todos.
  9. La operación de escritura en el socket UDP no es de bloqueo, por lo que aquí la función no esperará hasta que el mensaje llegue al destinatario y se ejecute casi instantáneamente.
  10. esperar en caso de error hará una salida de emergencia del programa con el mensaje dado.

La función crea un socket basado en la entrada del usuario.


 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. Leemos el puerto que nuestro servidor escuchará y crearemos una dirección de servidor local basada en él.
  2. Cree un socket UDP escuchando en esta dirección.
  3. Establezca el tiempo de espera para la operación de lectura. La operación de lectura está bloqueando y bloqueará la transmisión hasta que lleguen nuevos datos o se produzca un tiempo de espera.
  4. Devolvemos el socket creado desde la Función.
  5. La función lee los datos del socket y los devuelve junto con la dirección del remitente.

Función para leer datos de un 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. El búfer es el lugar donde leeremos los datos.
  2. Inicia un bucle que se ejecutará hasta que se lean datos válidos.
  3. Obtenemos el número de bytes leídos y la dirección del remitente.
  4. Cortamos la matriz desde el principio hasta el número de bytes leídos y la convertimos en un vector de bytes.
  5. Si se produce un tiempo de espera u otro error, continúe con la siguiente iteración del bucle.

Sobre capas en la aplicación


Offtopic: un pequeño programa educativo para dos de junio en el trabajo. Decidí ponerlo aquí, tal vez alguien sea útil. Los poetas afilados de junio son ejemplos en C # y estamos hablando de ASP.NET
Entonces, no había nada que hacer, era por la tarde, y decidí escribir un pequeño programa educativo sobre arquitectura para Artem y Victor. Pues vamos.

En realidad, agregué aquí porque el modo es de reconocimiento y solo puedo escribir artículos una vez por semana, y el material ya está allí y la próxima semana quería subir algo más a Habr.

Por lo general, una aplicación está en capas. En cada capa hay objetos que implementan el comportamiento característico de la capa en la que se encuentran. Y asi. Estas son las capas.

  1. Capa de presentación
  2. Capa lógica empresarial.
  3. Capa de acceso a datos.
  4. Entidades (Usuario, Animal, etc.)


Cada capa puede contener su propio DTO y clases completamente arbitrarias con métodos arbitrarios. Lo principal es que realizan la funcionalidad asociada con la capa en la que se encuentran. En aplicaciones simples, pueden faltar algunas de las capas. Por ejemplo, una vista de capa se puede implementar a través del patrón MVC, MVP, MVVM. Lo cual es completamente opcional. Lo principal es que las clases que están en esta capa implementan la funcionalidad asignada a la capa. Recuerde, los patrones y la arquitectura son solo recomendaciones, no instrucciones. Patrón y arquitectura no es una ley, esto es un consejo.

Por lo tanto, consideraremos cada capa en el ejemplo de una aplicación ASP.NET estándar que usa el Entity Framework estándar.

Capa de presentación


Tenemos MVC aquí. Esta es la capa que proporciona la interacción del usuario. Los comandos vienen aquí y los usuarios obtienen datos de aquí. No necesariamente personas, si tenemos una API, entonces nuestro usuario es un programa diferente. Los autos se comunican con los autos.

Capa de lógica empresarial


Aquí, por lo general, las clases se llaman Servicio, por ejemplo, UserService, aunque puede ser cualquier cosa. Solo un conjunto de clases con métodos. Lo principal es que los cálculos y cálculos de nuestra aplicación tienen lugar aquí. Esta es la capa más gruesa y voluminosa. Existe la mayor parte del código y varias clases. Esta, de hecho, es nuestra aplicación.

Capa de acceso a datos


Usualmente aquí EF implementa patrones de Unidad de Trabajo y Repositorio. Entonces sí, DbContext es, se puede decir, Unidad de trabajo, y DB establece que es Repositorio. De hecho, este es el lugar donde colocamos los datos y de dónde los obtenemos. Independientemente de si el origen de datos es una base de datos, API de otra aplicación, un caché en memoria o simplemente algún tipo de generador de números aleatorios. Cualquier fuente de datos.

Entidades


Sí, solo todo tipo de usuario, animal y más. Un punto importante: pueden tener algún tipo de comportamiento característico solo de ellos. Por ejemplo:

 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; } } 

Bueno, y un ejemplo muy simple. Shoba fue


 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


, . , . <3.

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


All Articles