Я продолжаю изучать Rust. Я еще много не знаю, поэтому делаю много ошибок. В прошлый раз я попробовал сделать игру Змейка. Попробовал циклы, коллекции, работу с 3D Three.rs. Узнал про ggez и Amethyst. В этот раз я попробовал сделать клиент и сервер для чата. Для GUI использовал Azul. Так же смотрел Conrod, Yew и Orbtk. Попробовал многопоточность, каналы и работу с сетью. Учел ошибки прошлой статьи и постарался сделать эту более развернутой. За подробностями добро пожаловать под кат.
→ Исходники, работает на Windows 10 x64
Для сетевого взаимодействия я использовал UDP потому, что хочу сделать свой следующий проект с использованием этого протокола и хотел потренировать с ним тут. Для GUI по-быстрому погуглил проекты на Rust, посмотрел базовые примеры для них и меня зацепил Azul потому, что там используется Document Object Model и движок стилей похожий на CSS, а я длительное время занимался веб-разработкой. В общем, выбирал Фреймворк субъективно. Он, пока что, в глубокой альфе: не работает скроллинг, не работает фокус ввода, нет курсора. Для того чтобы ввести данные в текстовое поле нужно навести мышку на него и прям над ним держать его пока печатаете. Подробнее ...
Собственно, большая часть статьи — это комментарии к коду.
Azul
GUI фреймворк, использующий функциональный стиль, DOM, CSS. Ваш интерфейс состоит из корневого элемента, у которого множество потомков, у которых могут быть свои потомки как, например, в HTML и XML. Весь интерфейс создается на основе данных из одной единственной DataModel. В ней передаются в представление вообще все данные. Если кто знаком с ASP.NET, то Azul и его DataModel это как Razor и его ViewModel. Как и в HTML можно привязать функции к событиям DOM элементов. Можно стилизовать элементы с помощью CSS фреймворка. Это не тот же самый CSS что в HTML, но очень на него похож. Так же есть двухсторонняя привязка как в Angular или MVVM в WPF, UWP. Подробнее на сайте.
Краткий обзор остальных фреймворков
- Orbtk — Почти что то же самое, что и Azul и так же в глубокой альфе
- Conrod — Видео Можно создавать кросс платформенные десктопные приложения.
- Yew — WebAssembly и похож на React. Для веб разработки.
Клиент
Структура, в которой группируются вспомогательные функции для чтения и записи в сокет
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,
}
}
}
- Читаем денные из сокета
- Буфер для данных, которые будем считывать из сокета.
- Блокирующий вызов. Здесь поток выполнения останавливается до тех пор, пока не будут считанны данные или произойдет таймаут.
- Получаем строку из массива байт в кодировке UTF8.
- Сюда мы попадаем, если соединение оборвалось по таймауту или произошла другая ошибка.
- Отправляет строку в сокет.
- Преобразуем строку в байты в кодировке UTF8 и отправляем данные в сокет. Запись данных в сокет не блокирующая, т.е. поток выполнения продолжит свою работу. Если отправить данные не удалось, то прерываем работу программы с сообщением «can't send».
Структура, которая группирует функции для обработки событий, поступающих от пользователя, и изменения нашей 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
}
}
}
- Таймаут в миллисекундах, после которого будет прервана блокирующая операция чтения из сокета.
- Функция отрабатывает, когда пользователь хочет оправить новое сообщение на сервер.
- Получаем во владение мютекс с нашей моделью данных. Это блокирует поток перерисовки интерфейса до тех пор, пока мютекс не будет освобожден.
- Делаем копию введенного пользователем текста, чтобы передать его дальше и очищаем поле для ввода текста.
- Отправляем сообщение.
- Сообщаем Фреймворку, что после обработки этого события нужно перерисовать интерфейс.
- Функция отрабатывает, когда пользователь хочет подключиться к серверу.
- Подключаем структуру для представления отрезка времени из стандартной библиотеки.
- Если мы уже подключены к серверу, то прерываем выполнение функции сообщаем Фреймворку, что нет необходимости перерисовывать интерфейс.
- Добавляем задачу, которая будет выполняться асинхронно в потоке из пула потоков Фреймворка Azul. Обращение к мютексу с моделью данных блокирует обновление UI до тех пор, пока мютекс не освободится.
- Добавляем повторяющуюся задачу, которая выполняется в основном потоке. Любые длительные вычисления в этом демоне блокирует обновление интерфейса.
- Получаем во владение мютекс.
- Считываем введенный пользователем порт и создаем на основе него локальный адрес, будем прослушивать.
- Создаем UDP сокет, который считывает пакеты, приходящие на локальный адрес.
- Считываем введенный пользователем адрес сервера.
- Говорим нашему UDP сокету читать пакеты только от этого сервера.
- Устанавливаем таймаут для операции чтения из сокета. Запись в сокет происходит без ожидания, т. е. мы просто пишем данные и не ждем ничего, а операция чтения из сокета блокирует поток и ждет пока не придут данные, которые можно считать. Если не установить таймаут, то операция чтения из сокета будет ждать бесконечно.
- Устанавливаем флаг, указывающий на то, что пользователь уже подключился к серверу.
- Передаем в модель данных созданный сокет.
- Сообщаем Фреймворку, что после обработки этого события нужно перерисовать интерфейс.
- Асинхронная операция, выполняющаяся в пуле потоков Фреймворка Azul.
- Получаем копию сокета из нашей модели данных.
- Пытаемся прочитать данные из сокета. Если не сделать копию сокета и напрямую ждать тут, пока придёт сообщение из сокета, который в мютекс в нашей модели денных, то весь интерфейс перестанет обновляться до тех пор, пока мы не освободим мютекс.
- Если нам пришло какое-то сообщение, то изменяем нашу модель данных modify делает то же, что и lock().unwrap() с передачей результата в лямбду и освобождением мютекс после того, как закончится код лямбды.
- Устанавливаем флаг, обозначающий, что у нас новое сообщение.
- Добавляем сообщение в массив всех сообщения чата.
- Повторяющаяся синхронная операция работающая в основном потоке.
- Если у нас есть новое сообщение, то сообщаем Фреймворку, что нужно перерисовать интерфейс с нуля и продолжить работу этого демона иначе не рисуем интерфейс с начала, но все равно вызываем этот Функция в следующем цикле.
- Создает копию нашего сокета для того, чтобы не держать заблокированным мютекс с нашей моделью данных.
- Получаем во владение мютекс и получаем ссылку на сокет.
- Создаем копию сокета. Мютекс освободится автоматически при выходе из Функцияа.
Асинхронная обработка данных и демоны в 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;
});
}
Демон всегда выполняется в основном потоке, поэтому там блокировка неизбежна. С асинхронной задачей, если сделать, например, вот так-то никакой блокировки на 10 секунд не будет.
fn start_async_task(app_data: Arc<Mutex<MyDataModel>>, _: Arc<()>) {
//Не блокируем UI. Ожидаем асинхронно.
thread::sleep(Duration::from_secs(10));
app_data.modify(|state| {
state.counter += 10000;
});
}
Функция modify вызывает lock() а мютекс с моделью данных поэтому блокирует обновление интерфейса на время своего выполнения.
Наши стили
const CUSTOM_CSS: &str = "
.row { height: 50px; }
.orange {
background: linear-gradient(to bottom, #f69135, #f37335);
font-color: white;
border-bottom: 1px solid #8d8d8d;
}";
Собственно, Функции для создания нашего DOM для отображения его пользователю
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
}
}
- Функция, которая создает конечный DOM, и вызывается каждый раз, когда нужно перерисовать интерфейс.
- Если мы уже подключены к серверу, то показываем форму для отправки и чтения сообщений, иначе отображаем форму для подключения к серверу.
- Создает форму для ввода данных необходимых для подключения к серверу.
- Создаем кнопку с текстовой надписью Login.
- Преобразуем ее в объект DOM.
- Добавляем ей класс row.
- Добавляем ей css класс orange.
- Добавляем обработчик события для нажатия на кнопку.
- Создаем текстовую метку с текстом для отображения пользователю и css классом row.
- Создаем текстовое поле для ввода текста с текстом из свойства нашей модели и css классом row.
- Привязываем текстовое поле к свойству нашей DataModel. Это двухсторонняя привязка. Теперь редактирование TextInput автоматически изменяет текст в свойстве нашей модели и обратное тоже верно. Если мы изменим текст в нашей модели, то изменится текст в TextInput.
- Создаем корневой DOM элемент, в который помещаем наши UI элементы.
- Создает форму для отправки и чтения сообщений.
- Создаем кнопку с текстом «Send» и css классами «row», «orange» и обработчиком события при ее нажатии.
- Создаем поле для ввода текста с двухсторонней привязкой с свойству модели self.messaging_model.text_input_state и css классом «row».
- Добавляем текстовые метки, которые отображают сообщения, которые были написаны в чате.
Наша модель, которая хранит состояние нашего интерфейса
В документации Azul написано, что в ней должны храниться все данные приложения, в том числе и подключение к базе данных, поэтому я поместил в нее UDP сокет.
//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,
}
- Это позволит отображать нашу структуру в виде строки в шаблоне вида {:?}
- Наша модель данных. Для того, чтобы ее можно было использовать в Azul. Она обязательно должна реализовать трейт Layout.
- Флаг для проверки того, подключен ли пользователь к серверу или нет.
- Модель для отображения формы для отправки сообщений на сервер и сохранения полученных с сервера сообщений.
- Модель для отображения формы для подключения к серверу.
- Порт, который ввел пользователь. Мы будем его прослушивать нашим сокетом.
- Адрес сервера, который ввел пользователь. Мы будем к нему подключаться.
- Сообщение пользователя. Мы его отправим на сервер.
- Массив сообщений, который пришел с сервера.
- Сокет, через который мы общаемся с сервером.
- Флаг для проверки того, пришло ли нам новое сообщение от сервера.
И, наконец, главная точка входа в приложение. Запускает цикл от рисовки GUI и обработки ввода пользователя
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();
}
- Создаем приложение со стартовыми данными.
- Стили, используемые приложением по умолчанию.
- Добавляем к ним наши собственные стили.
- Создаем окно, в котором будет отображать наше приложение.
- Запускаем приложение в этом окне.
Сервер
Главная точка входа в приложение
Здесь у нас обычно консольное приложение.
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();
}
}
- Создаем сокет.
- Создаем односторонний канал с одним отправителем сообщений sx и множеством получателей rx.
- Запускаем рассылку сообщений всем получателям в отдельном потоке.
- Читаем данные из сокета и оправляем их в поток, занимающийся рассылкой сообщений клиентам, подключенным к серверу.
Функция для создания потока для рассылки сообщений клиентам
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());
});
}
});
}
- Запускаем новый поток. move значит, что переменные переходят во владение лямбды и потока, соответственно. Конкретнее, наш новый поток «поглотит» переменные rx и socket.
- Коллекция адресов, подключенных к нам клиентов. Всем им мы будем рассылать наши сообщения. Вообще, в реальном проекте надо бы сделать обработку отключения от нас клиента и удаления его адреса из этого массива.
- Запускаем бесконечный цикл.
- Читаем данные из канала. Тут поток будет заблокирован до тех пор, пока не придут новые данные.
- Если такого адреса нет в нашем массиве, то добавляем его туда.
- Декодируем UTF8 строку из массива байт.
- Создаем массив байт, которые собираемся отправить всем нашим клиентам.
- Проходим по коллекции адресов и отправляем данные каждому.
- Операция записи в UDP сокет неблокирующая, поэтому здесь Функция не будет ждать пока сообщение придёт к получателю и выполнится почти мгновенно.
- expect в случае ошибки сделает экстренный выход из программы с заданным сообщением.
Функция создает сокет на основе данных введенных пользователем
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
}
- Считываем порт, который будет слушать наш сервер, и создаем на его основе локальный адрес сервера.
- Создаем UDP сокет, прослушивающий этот адрес.
- Устанавливаем таймаут для операции чтения. Операция чтения блокирующая и она заблокирует поток до тех пор, пока не придут новые данные или не наступит таймаут.
- Возвращаем из Функции созданные сокет.
- Функция читает данные из сокета и возвращает их вместе с адресом отправителя.
Функция для чтения данных из сокета
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;
}
};
}
}
- Буфер — место, куда будем считывать данные.
- Запускает цикл, который будет выполняться до тех пор, пока не будут считаны валидные данные.
- Получаем количество считанных байт и адрес отправителя.
- Делаем срез массива от его начала до количеств, считанных байт и преобразуем его в вектор байт.
- Если произошёл таймаут или другая ошибка, то переходим к следующей итерации цикла.
Про слои в приложении
Офтоп: Маленький ликбез для двух джунов на работе. Решил выложить сюда, может кому пригодится. Джуны шарписты поэтом примеры на C# и речь про ASP.NET идет
Так, делать было нечего, дело было вечером, и я решил маленький ликбез по архитектуре для Артема и Виктора написать. Ну что, поехали.
Собственно, добавил сюда потому, что рековери мод и я могу только раз в неделю статьи писать, а материал уже есть и на следующей неделе я хотел кое-что другое уже на Хабр залить.
Обычно, приложение делят на слои. В каждом слое находятся объекты, реализующие поведение характерное для слоя, в котором они находятся. И так. Вот эти слои.
Каждый слой может содержать свои DTO и совершенно произвольные классы с произвольными методами. Главное, чтобы они выполняли функционал, связанный со слоем, в котором они находятся. В простых приложениях некоторые из слоев могут отсутствовать. Например, слой представление может реализоваться через MVC, MVP, MVVM паттерн. Что совершенно не обязательно. Главное, чтобы классы, которые находятся в этом слое реализовали функционал, возложенный на слой. Помните, паттерны и архитектура — это всего лишь рекомендации, а не указания. Паттерн и архитектура — это не закон, это совет.
И так, рассмотрим каждый слой на примере стандартного ASP.NET приложения, использующего стандартный Entity Framework.
У нас тут MVC. Это тот слой, который обеспечивает взаимодействие с пользователем. Сюда приходят команды и от сюда получают данные пользователи. Не обязательно люди, если у нас API, то наш пользователь — это другая программа. Машины общаются с машинами.
Тут, обычно, классы именуют Service, например, UserService, хотя может быть вообще, что угодно. Просто набор классов с методами. Главное, чтобы тут происходили вычисления и расчеты нашего приложения. Это самый толстый и громоздкий слой. Тут больше всего кода и различных классов. Это, собственно, и есть наше приложение.
Обычно у нас тут EF реализует паттерны Unit Of Work и Repository. Таки да, DbContext это, можно сказать, Unit Of Work, а ДБ сеты его это Repository. Это, собственно, то место куда мы кладем данные и откуда их берем. Не зависимо от того, источник данных это БД, АПИ другого приложения, Кеш в Памяти или просто какой-то генератор случайных чисел. Любой источник данных.
Да, просто всякие User, Animal и прочее. Одно важное замечание – у них может быть какое-то поведение характерное только для них. Например:
Собственно, добавил сюда потому, что рековери мод и я могу только раз в неделю статьи писать, а материал уже есть и на следующей неделе я хотел кое-что другое уже на Хабр залить.
Обычно, приложение делят на слои. В каждом слое находятся объекты, реализующие поведение характерное для слоя, в котором они находятся. И так. Вот эти слои.
- Слой представления.
- Слой бизнес логики.
- Слой доступа к данным.
- Сущности (User, Animal и т. д.)
Каждый слой может содержать свои DTO и совершенно произвольные классы с произвольными методами. Главное, чтобы они выполняли функционал, связанный со слоем, в котором они находятся. В простых приложениях некоторые из слоев могут отсутствовать. Например, слой представление может реализоваться через MVC, MVP, MVVM паттерн. Что совершенно не обязательно. Главное, чтобы классы, которые находятся в этом слое реализовали функционал, возложенный на слой. Помните, паттерны и архитектура — это всего лишь рекомендации, а не указания. Паттерн и архитектура — это не закон, это совет.
И так, рассмотрим каждый слой на примере стандартного ASP.NET приложения, использующего стандартный Entity Framework.
Слой представления
У нас тут MVC. Это тот слой, который обеспечивает взаимодействие с пользователем. Сюда приходят команды и от сюда получают данные пользователи. Не обязательно люди, если у нас API, то наш пользователь — это другая программа. Машины общаются с машинами.
Слой бизнес логики
Тут, обычно, классы именуют Service, например, UserService, хотя может быть вообще, что угодно. Просто набор классов с методами. Главное, чтобы тут происходили вычисления и расчеты нашего приложения. Это самый толстый и громоздкий слой. Тут больше всего кода и различных классов. Это, собственно, и есть наше приложение.
Слой доступа к данным
Обычно у нас тут EF реализует паттерны Unit Of Work и Repository. Таки да, DbContext это, можно сказать, Unit Of Work, а ДБ сеты его это Repository. Это, собственно, то место куда мы кладем данные и откуда их берем. Не зависимо от того, источник данных это БД, АПИ другого приложения, Кеш в Памяти или просто какой-то генератор случайных чисел. Любой источник данных.
Сущности
Да, просто всякие User, Animal и прочее. Одно важное замечание – у них может быть какое-то поведение характерное только для них. Например:
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;
}
}
Ну, и совсем простенький пример. Шобы было
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();
}
}
}
P.S.
Ну шо, таки хочу сказать спасибо моей Насте за то что исправила грамматические ошибки в статье. Таки да, Настя ты не зря с красным дипломом и вообще классная. Люблю тебя <3.