Learning Rust: Como conversei com o UDP com a Azul



Eu continuo aprendendo ferrugem. Ainda não sei muito, então cometo muitos erros. A última vez que tentei fazer um jogo de cobra . Tentei ciclos, coleções, trabalhei com o 3D Three.rs . Aprendeu sobre ggez e ametista . Desta vez, tentei criar um cliente e um servidor para conversar. Para GUI usado Azul . Também assistiram Conrod , Yew e Orbtk . Tentei multithreading, canais e redes. Levei em consideração os erros do artigo anterior e tentei tornar isso mais detalhado. Para detalhes, bem-vindo ao gato.

Fontes, funciona no Windows 10 x64

Para redes, usei o UDP porque quero fazer meu próximo projeto usando esse protocolo e queria treiná-lo aqui. Para a GUI, rapidamente acessei os projetos do Google no Rust, observei os exemplos básicos para eles, e o Azul me fisgou porque ele usa um Modelo de Objeto de Documento e um mecanismo de estilo semelhante ao CSS, e eu estava envolvido no desenvolvimento da Web por um longo tempo. Em geral, eu escolhi o Framework subjetivamente. Até o momento, está em alfa profundo: a rolagem não funciona, o foco de entrada não funciona, não há cursor. Para inserir dados em um campo de texto, você precisa passar o mouse sobre ele e mantê-lo diretamente acima dele enquanto digita. Mais detalhes ...

Na verdade, a maior parte do artigo são comentários de código.

Azul


Estrutura da GUI usando estilo funcional, DOM, CSS. Sua interface consiste em um elemento raiz, que possui muitos descendentes, que podem ter seus próprios descendentes, como em HTML e XML. Toda a interface é criada com base nos dados de um único DataModel. Nele, todos os dados são transferidos para a apresentação em geral. Se alguém está familiarizado com o ASP.NET, o Azul e seu DataModel são como o Razor e seu ViewModel. Como no HTML, você pode vincular funções a eventos de elementos DOM. Você pode estilizar elementos usando a estrutura CSS. Este não é o mesmo CSS que o HTML, mas é muito semelhante a ele. Também há ligação bidirecional como no Angular ou MVVM no WPF, UWP. Mais informações no site .

Uma breve visão geral do restante das estruturas


  • Orbtk - quase o mesmo que Azul e também em alfa profundo
  • Conrod - Vídeo Você pode criar aplicativos de desktop multiplataforma.
  • Yew é WebAssembly e semelhante ao React. Para desenvolvimento web.

Cliente


A estrutura na qual as funções auxiliares de leitura e gravação em um soquete são agrupadas


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. Ler dados do soquete
  2. Buffer para dados a serem lidos do soquete.
  3. Bloqueio de chamada. Aqui, o encadeamento de execução para até que os dados sejam lidos ou ocorra um tempo limite.
  4. Obtemos uma string de uma matriz de bytes na codificação UTF8.
  5. Chegamos aqui se a conexão foi interrompida por um tempo limite ou outro erro ocorreu.
  6. Envia uma string para um soquete.
  7. Converta a string em bytes na codificação UTF8 e envie os dados para o soquete. A gravação de dados em um soquete não está bloqueando, ou seja, o segmento de execução continuará seu trabalho. Se os dados não puderam ser enviados, interrompemos o programa com a mensagem "não é possível enviar".

Uma estrutura que agrupa funções para manipular eventos do usuário e modificar nosso 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. O tempo limite em milissegundos após o qual a operação de bloqueio da leitura do soquete será interrompida.
  2. A função é concluída quando o usuário deseja enviar uma nova mensagem para o servidor.
  3. Tomamos posse do mutex com nosso modelo de dados. Isso bloqueia o encadeamento de redesenho de interface até que o mutex seja liberado.
  4. Fazemos uma cópia do texto inserido pelo usuário para transferi-lo ainda mais e limpar o campo de entrada de texto.
  5. Estamos enviando uma mensagem.
  6. Informamos ao Framework que, após o processamento deste evento, precisamos redesenhar a interface.
  7. A função funciona quando o usuário deseja se conectar ao servidor.
  8. Conectamos a estrutura para representar o período de tempo da biblioteca padrão.
  9. Se já estamos conectados ao servidor, interrompemos a execução da função e informamos ao Framework que não há necessidade de redesenhar a interface.
  10. Adicione uma tarefa que será executada de forma assíncrona no thread do pool de threads do Azul Framework. O acesso ao mutex com o modelo de dados bloqueia a atualização da interface do usuário até que o mutex seja liberado.
  11. Adicione uma tarefa recorrente que é executada no thread principal. Quaisquer cálculos longos neste daemon são bloqueados pela atualização da interface.
  12. Nós entramos na posse do mutex.
  13. Lemos a porta inserida pelo usuário e criamos um endereço local com base nela, vamos ouvir.
  14. Crie um soquete UDP que leia pacotes que chegam ao endereço local.
  15. Lemos o endereço do servidor digitado pelo usuário.
  16. Dizemos ao nosso soquete UDP para ler pacotes somente deste servidor.
  17. Defina o tempo limite para a operação de leitura do soquete. A gravação no soquete ocorre sem esperar, ou seja, apenas gravamos dados e não esperamos nada, e a operação de leitura do soquete bloqueia o fluxo e aguarda até que os dados que podem ser lidos cheguem. Se você não definir um tempo limite, a operação de leitura do soquete aguardará indefinidamente.
  18. Defina um sinalizador indicando que o usuário já está conectado ao servidor.
  19. Passamos o soquete criado para o modelo de dados.
  20. Informamos ao Framework que, após o processamento deste evento, precisamos redesenhar a interface.
  21. Uma operação assíncrona que é executada no pool de threads do Azul Framework.
  22. Obtenha uma cópia do soquete em nosso modelo de dados.
  23. Tentando ler dados de um soquete. Se você não fizer uma cópia do soquete e aguardar diretamente aqui até que uma mensagem chegue do soquete que está no mutex em nosso modelo de dados, toda a interface deixará de ser atualizada até que nós liberemos o mutex.
  24. Se recebermos algum tipo de mensagem, alterando nosso modelo de dados, modify fará o mesmo que lock (). Desembrulhe () passando o resultado para o lambda e liberando o mutex após o término do código lambda.
  25. Defina um sinalizador para indicar que temos uma nova mensagem.
  26. Adicione uma mensagem à matriz de todas as mensagens de bate-papo.
  27. Uma operação síncrona repetida em execução no encadeamento principal.
  28. Se tivermos uma nova mensagem, informamos ao Framework que precisamos redesenhar a interface do zero e continuar trabalhando com esse daemon; caso contrário, não desenharemos a interface desde o início, mas ainda chamaremos essa função no próximo ciclo.
  29. Cria uma cópia do nosso soquete para não manter o mutex bloqueado com nosso modelo de dados.
  30. Nós obtemos o mutex e obtemos um link para o soquete.
  31. Crie uma cópia do soquete. O Mutex será liberado automaticamente ao sair de uma função.

Processamento de dados assíncrono e daemons em 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; }); } 

O daemon é sempre executado no encadeamento principal, portanto, o bloqueio é inevitável lá. Com uma tarefa assíncrona, se você fizer, por exemplo, assim, não haverá bloqueio por 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; }); } 

A função de modificação chama lock () e o mutex com o modelo de dados, portanto, bloqueia a atualização da interface durante a execução.

Nossos estilos


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

Na verdade, funções para criar nosso DOM para exibir ao usuário


 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. A função que cria o DOM final e é chamada toda vez que você precisa redesenhar a interface.
  2. Se já estamos conectados ao servidor, mostramos o formulário para enviar e ler mensagens, caso contrário, exibimos o formulário para conectar-se ao servidor.
  3. Cria um formulário para inserir os dados necessários para conectar-se ao servidor.
  4. Crie um botão com a inscrição de texto Login.
  5. Converta-o em um objeto DOM.
  6. Adicione a classe de linha a ela.
  7. Adicione a laranja da classe css.
  8. Adicione um manipulador de eventos para clicar no botão.
  9. Crie um rótulo de texto com o texto a ser exibido na linha do usuário e da classe css.
  10. Criamos uma caixa de texto para inserir texto com o texto da propriedade do nosso modelo e linha da classe css.
  11. Vincule o campo de texto à propriedade do nosso DataModel. Esta é uma ligação bidirecional. Agora, editar o TextInput altera automaticamente o texto na propriedade do nosso modelo e o oposto também é verdadeiro. Se alterarmos o texto em nosso modelo, o texto em TextInput será alterado.
  12. Criamos um elemento DOM raiz no qual colocamos nossos elementos de interface do usuário.
  13. Cria um formulário para enviar e ler mensagens.
  14. Crie um botão com o texto "Enviar" e css com as classes "linha", "laranja" e um manipulador de eventos quando ele for clicado.
  15. Crie um campo de entrada de texto bidirecional com a propriedade de modelo self.messaging_model.text_input_state e css com a classe "row".
  16. Adicione rótulos de texto que exibam mensagens escritas no bate-papo.

Nosso modelo que armazena o estado da nossa interface


A documentação da Azul diz que ele deve armazenar todos os dados do aplicativo, incluindo a conexão com o banco de dados, então coloquei um soquete UDP nele.

 //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. Isso nos permitirá exibir nossa estrutura como uma string em um modelo no formato {:?}
  2. Nosso modelo de dados. Para que possa ser usado na Azul. Ela deve implementar a característica de layout.
  3. Um sinalizador para verificar se o usuário está conectado ao servidor ou não.
  4. Um modelo para exibir um formulário para enviar mensagens para o servidor e salvar mensagens recebidas do servidor.
  5. Modelo para exibir o formulário para conectar-se ao servidor.
  6. A porta que o usuário digitou. Vamos ouvi-lo com o nosso soquete.
  7. O endereço do servidor que o usuário digitou. Vamos nos conectar a ele.
  8. Mensagem do usuário. Nós o enviaremos para o servidor.
  9. Uma matriz de mensagens que vieram do servidor.
  10. O soquete através do qual nos comunicamos com o servidor.
  11. Um sinalizador para verificar se uma nova mensagem chegou do servidor.

E, finalmente, o principal ponto de entrada para o aplicativo. Inicia um ciclo de desenho da GUI e processamento de entrada do usuário


 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. Criamos um aplicativo com dados iniciais.
  2. Os estilos usados ​​pelo aplicativo por padrão.
  3. Adicione nossos próprios estilos a eles.
  4. Criamos uma janela na qual nosso aplicativo será exibido.
  5. Inicie o aplicativo nesta janela.

Servidor


O principal ponto de entrada do aplicativo


Aqui geralmente temos um aplicativo de 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. Crie um soquete.
  2. Criamos um canal unidirecional com um remetente de mensagem sx e muitos destinatários rx.
  3. Começamos a enviar mensagens para todos os destinatários em um fluxo separado.
  4. Lemos os dados do soquete e enviamos para o fluxo, que envia mensagens aos clientes conectados ao servidor.

Função para criar um fluxo para enviar mensagens aos 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. Inicie um novo thread. move significa que as variáveis ​​assumem o lambda e o fluxo, respectivamente. Mais especificamente, nosso novo thread "absorverá" as variáveis ​​rx e socket.
  2. Uma coleção de endereços conectados a nós pelos clientes. Enviaremos a todos eles nossas mensagens. Em geral, em um projeto real, seria necessário desconectar um cliente de nós e remover seu endereço dessa matriz.
  3. Começamos um loop infinito.
  4. Lemos os dados do canal. Aqui o fluxo será bloqueado até que novos dados cheguem.
  5. Se não houver esse endereço em nossa matriz, adicione-o lá.
  6. Decodifique uma sequência UTF8 de uma matriz de bytes.
  7. Criamos uma matriz de bytes que vamos enviar a todos os nossos clientes.
  8. Examinamos a coleta de endereços e enviamos dados para todos.
  9. A operação de gravação no soquete UDP é sem bloqueio; portanto, aqui a Função não espera até que a mensagem chegue ao destinatário e seja executada quase instantaneamente.
  10. esperar em caso de erro fará uma saída de emergência do programa com a mensagem fornecida.

A função cria um soquete com base na entrada do usuário


 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. Lemos a porta que nosso servidor escutará e criamos um endereço de servidor local com base nela.
  2. Crie um soquete UDP escutando neste endereço.
  3. Defina o tempo limite para a operação de leitura. A operação de leitura está bloqueando e bloqueará o fluxo até que novos dados cheguem ou ocorra um tempo limite.
  4. Retornamos o soquete criado da Função.
  5. A função lê os dados do soquete e os retorna juntamente com o endereço do remetente.

Função para ler dados de um soquete


 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. O buffer é o local onde vamos ler os dados.
  2. Inicia um loop que será executado até que dados válidos sejam lidos.
  3. Obtemos o número de bytes lidos e o endereço do remetente.
  4. Cortamos a matriz desde o início até o número de bytes lidos e a convertemos em um vetor de bytes.
  5. Se ocorrer um tempo limite ou outro erro, prossiga para a próxima iteração do loop.

Sobre camadas no aplicativo


Offtopic: Um pequeno programa educacional para dois de junho no trabalho. Eu decidi colocá-lo aqui, talvez alguém venha a calhar. Os poetas afiados de junho são exemplos em C # e estamos falando sobre o ASP.NET
Então, não havia nada a fazer, era à noite e decidi escrever um pequeno programa educacional sobre arquitetura para Artem e Victor. Bem, vamos lá.

Na verdade, eu adicionei aqui porque o modo é de reconhecimento e só posso escrever artigos uma vez por semana, e o material já está lá e na próxima semana eu queria enviar outra coisa para Habr.

Normalmente, um aplicativo é em camadas. Em cada camada, há objetos que implementam a característica de comportamento da camada em que estão localizados. E assim Essas são as camadas.

  1. Camada de apresentação.
  2. Camada lógica de negócios.
  3. Camada de acesso a dados.
  4. Entidades (usuário, animal etc.)


Cada camada pode conter seu próprio DTO e classes completamente arbitrárias com métodos arbitrários. O principal é que eles executem a funcionalidade associada à camada em que estão localizados. Em aplicativos simples, algumas das camadas podem estar ausentes. Por exemplo, uma visualização de camada pode ser implementada através do padrão MVC, MVP, MVVM. O que é completamente opcional. O principal é que as classes que estão nessa camada implementam a funcionalidade atribuída à camada. Lembre-se, padrões e arquitetura são apenas recomendações, não direções. Padrão e arquitetura não é uma lei, este é um conselho.

E assim, consideraremos cada camada no exemplo de um aplicativo ASP.NET padrão usando o Entity Framework padrão.

Camada de apresentação


Temos MVC aqui. Essa é a camada que fornece a interação do usuário. Os comandos vêm aqui e os usuários obtêm dados daqui. Não necessariamente pessoas, se tivermos uma API, nosso usuário será um programa diferente. Carros se comunicam com carros.

Camada lógica de negócios


Aqui, geralmente, as classes são chamadas de Serviço, por exemplo, UserService, embora possa ser qualquer coisa. Apenas um conjunto de classes com métodos. O principal é que os cálculos e cálculos de nossa aplicação ocorram aqui. Essa é a camada mais grossa e mais volumosa. Existe a maior parte do código e várias classes. Esta é, de fato, a nossa aplicação.

Camada de acesso a dados


Geralmente aqui a EF implementa padrões de Unidade de trabalho e repositório. Então, sim, o DbContext é, você pode dizer, Unidade de Trabalho, e o DB define que é Repositório. Este é, de fato, o local onde colocamos os dados e de onde obtemos. Independentemente de a fonte de dados ser um banco de dados, API de outro aplicativo, cache na memória ou apenas algum tipo de gerador de números aleatórios. Qualquer fonte de dados.

Entidades


Sim, apenas todos os tipos de usuário, animal e muito mais. Um ponto importante - eles podem ter algum tipo de comportamento característico apenas deles. Por exemplo:

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

Bem, e um exemplo muito simples. Shoba era


 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


Bem, quero agradecer ao meu Nastya por corrigir erros gramaticais no artigo. Então sim, Nastya, você não é em vão com um diploma vermelho e geralmente é legal. Eu te amo <3.

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


All Articles