Глава 2. Структурирование кода в Rust

Теперь, когда мы разобрались с основами Rust, мы можем перейти к структурированию кода по нескольким файлам, чтобы мы могли при помощи Rust разные задачи. Для этого нам требуется разобраться с тем как управлять зависимостями, а также как скомпилировать базовое и структурированное приложение. Нам также следует рассмотреть изоляцию кода чтобы мы могли повторно применять его и поддерживать гибкость разработки своего приложения, что позволяет без особых усилий быстро вносить изменения. После обсуждения этого мы также заставим ваше приложение напрямую взаимодействовать со своим пользователем, принимая команды пользователя. Мы также будем применять корзины (crates) Rust. Корзина это двоичный файл или библиотека, которые мы импортируем и применяем.

В этой главе мы рассмотрим следующие вопросы:

  • Управление нашим кодом при помощи корзин и Cargo вместо pip

  • Структурирование кода по множеству файлов и модулей

  • Построение интерфейсов модулей

  • Взаимодействие с вашей средой

Технические требования

Мы больше не собираемся реализовывать простые приложения в одну страничку, которые не полагаются на зависимости сторонних разработчиков, как мы это делали в своей первой главе. В результате вам придётся непосредственно установить в своём компьютере Rust. Мы также будем управлять сторонними зависимостями через Cargo. Вы можете установить Rust и Cargo отсюда: https://www.rust-lang.org/tools/install.

На момент написание этих строк наилучшей интегрированной средой разработки (IDE, integrated development environment) для написания на Rust является Visual Studio Code. Он обладает рядом подключаемых модулей, которые способны помогать вам с отслеживанием и проверкой вашего кода Rust. Вы можете установить его при помощи следующей ссылки. {Прим. пер.: подробнее про Visual Studio Code в наших переводах Внутреннее устройство CPython Энтони Шоу, Real Python, 2021, WSL для профессионалов Хайден Барнс, Apress, 2021, Изучаем подсистемы Windows для Linux Хайден Барнс, Apress, 2020.}

Вы можете найти все файлы кода в нашем репозитории GitHub для этой главы.

Управление нашим кодом при помощи шасси и Cargo вместо pip

Построение нашего собственного приложения будет включать в себя следующие этапы:

  1. Создание простого файла Rust и его запуск.

  2. Создание простого приложения при помощи Cargo.

  3. Исполнение нашего приложения при помощи Cargo.

  4. Управление зависимостями при помощи Cargo.

  5. Применение корзины стороннего разработчика для упорядочения JSON.

  6. Документирование нашего приложения при помощи Cargo.

Прежде чем мы начнём выстраивать структуру своей программы при помощи Cargo, нам следует скомпилировать некий базовый сценарий Rust и запустить его:

  1. Для этого создайте файл с названием hello_world.rs с необходимой функцией main, размещающей функцию println! с какой- то строкой, как мы это можем видеть здесь:

    
    fn main() {
        println!("hello world");
    }
     	   
  2. После выполнения этого мы можем перейти к этому файлу и выполнить команду rustc:

    
    rustc hello_world.rs
     	   
  3. Данная команда скомпилирует этот файл в некий двоичный, который подлежит выполнению. Если мы выполнили компиляцию в Windows, мы можем запустить свой двоичный файл при помощи такой команды:

    
    .\hello_world.exe
     	   
  4. Когда мы выполнили компиляцию в Linux или Mac, мы можем запускать его следующей командой:

    
    ./hello_world.exe
     	   

    Затем наша консоль должна выдать на печать определённую нами строку. Хотя это и может показаться полезным при построении обособленного файла, такой подход не рекомендуется для управления программами, распространяющимися на множество файлов. Более того, он даже не рекомендуется когда вы рассматриваете зависимости. И именно тут на помощь приходит Cargo. Cargo управляет всем - исполнением, тестированием, документированием, сборкой и зависимостями из одного места - при помощи нескольких простых команд.

Теперь, когда у нас имеется базовое представление о том как компилировать базовый файл, мы можем перейти к сборке полностью оперённого приложения:

  1. В своём терминале перейдите туда, где вы желаете чтобы пребывало ваше приложение и создайте новый проект с названием wealth_manager следующим образом:

    
    cargo new wealth_manager
    		

    Это создаст наше приложение со следующей структурой:

    
    └── wealth_manage
        ├── .git
        ├── Cargo.toml
        ├── .gitignore
        └── src
            └── main.rs
    		

    Здесь мы можем видеть что Cargo собрал необходимую базовую структуру приложения, которая способна управлять компиляцией, работать с GitHub и управлять зависимостями прямо из одного места. Необходимые метаданные и зависимости нашего приложения определяются в соответствующем файле Cargo.toml.

    Для выполнения команд Cargo с этим приложением, наш терминал должен пребывать в том же самом каталоге, что и файл Cargo.toml. Тот код, который мы намерены изменять и который составляет наше приложение располагается в соответствующем каталоге src. Нашей точкой входа для всего приложения выступает файл main.rs. В Python мы можем обладать множеством точек входа и мы поясним это в Главе 4, Сборка модулей pip в Python, в которой мы будем собирать чистые пакеты Python для самого первого раза. Если мы откроем свой файл .gitignore, мы должны обнаружить следующее:

    
    /target
    		

    Это не ошибка; именно так выглядит чистый Rust. Всё что производит Cargo когда дело доходит до компиляции, документирования, кэширования и тому подобного, всё это сохраняется в каталоге target.

  2. Прямо сейчас, всё что у нас есть, это наш файл main, который имеет вывод на печать консоли, сообщающий "hello world". Мы можем выполнить его при помощи такой команды:

    
    cargo run
    		
  3. При помощи данной команды мы получаем в своём терминале следующий вывод:

    
    Compiling wealth_manager v0.1.0 (/Users/maxwellflitton
    /Documents/github/book_two/chapter_two/wealth_manager)
    
    Finished dev [unoptimized + debuginfo] target(s) in 0.45s
    
    Running 'target/debug/wealth_manager'
    
    Hello, world!
    		

    Здесь мы видим что наше приложение было скомпилировано. После его компиляции постулируется что наш процесс компиляции был завершён.

    Однако, мы должны заметить, что здесь также постулируется что наш завершённый процесс не оптимизирован за счёт отладочных сведений. Это означает, что наш скомпилированный продукт не настолько быстр, насколько это можно; однако он содержит средства отладки, если они понадобятся. Данный тип компиляции является быстрым по сравнению с оптимизированной версией и должен применяться при разработке приложения в противоположность промышленным средам реального времени. Затем мы увидим, что наш скомпилированный файл находится в target/debug/wealth_manager и он далее запускается, приводя в результате к выводу hello_world.rs.

  4. Если мы желаем исполнить некий выпуск, мы просто исполняем такую команду:

    
    cargo run --release
    		

    Она компилирует оптимизированную версию нашего прикладного приложения в каталоге ./target/release/ с исполняемым файлом wealth_manager. Если мы хотим только скомпилировать приложение без его исполнения, мы можем просто переключиться с команды run на build.

Теперь, когда мы получили своё приложение исполняемым, давайте изучим как мы можем управлять имеющимися вокруг него метаданными. Это можно делать при помощи редактирования нашего файла Cargo.toml. Когда мы откроем его, мы получим следующее:


[package]
name = "wealth_manager"
version = "0.1.0"
authors = ["maxwellflitton"]
edition = "2018"

[dependencies]
 	   

Название, версия и авторы достаточно прозрачны. Вот воздействие каждого из разделов на наш проект:

  • Если мы изменим значение name в своём файле Cargo.toml, тогда при сборке или исполнении нашего приложения будут создаваться новые исполняемые файлы с этим названием. Но старые всё ещё будут сохраняться там же.

  • version служит распространению такими службами как crates.io {корзины}, если мы хотим открыть источник для своего приложения под прочее применение. Для этого также необходимы авторы и наше приложение всё ещё будет компилироваться и запускаться локально отсюда.

  • edition это номер редакции Rust, применяемой нами. Rust обновляется часто. Эти обновления накапливаются со временем и каждые два - три года сглаженные новые функциональные возможности упаковываются, документируются и добавляются в новую редакцию. Самая последняя редакция (2021) доступна в https://devclass.com/2021/10/27/rust-1-56-0-arrives-delivering-rust-2021-edition-support/.

  • У нас также имеются dependencies (зависимости). Именно тут мы имеем возможность импорта корзин (crates) сторонних разработчиков.

Чтобы увидеть как это работает, давайте воспользуемся корзиной для преобразования структуры данных акции в JSON с последующим выводом её на печать. Написание этого кода своими руками будет слегка головоломным. К счастью, мы можем установить корзину serde и воспользоваться макросом json!. Чтобы наш Cargo выгрузил и установил эту корзину, мы заполним свой раздел зависимостей в нашем файле Cargo.toml приводимым тут кодом:


[dependencies]
serde="1.0.117"
serde_json="1.0.59"
 	   

В своём файле main.rs мы затем импортируем этот макрос и структуру, необходимые для преобразования данных относительно акции в JSON с последующей выдачей на печать в следующем коде:


use serde_json::{json, Value};

fn main() {
    let stock: Value = json!({
        "name": "MonolithAi",
        "price": 43.7,
        "history": [19.4, 26.9, 32.5]
    });

    println!("first price: {}", stock["history"][0]);
    println!("{}", stock.to_string());
}
 	   

Важно отметить, что мы возвращаем из своего значения serde_json структуру Value. Чтобы увидеть как мы можем использовать это возвращаемое значение, мы можем изучить документацию для этой структуры. Именно так мы можем обнаружить что система документирования Rust очень выразительна. Для данной структуры мы можем найти документацию тут: https://docs.rs/serde_json/1.0.64/serde_json/enum.Value.html.

Как мы можем видеть на Рисунке 2-1, эта документация охватывает все те функции, которые поддерживает данная структура. Наш макрос json! является возвращаемым Object(Map<String, Value>). Мы также имеем диапазон прочих значений, причём в зависимости от того как мы вызываем свой макрос json!. Данная документация также охватывает некий диапазон функций, которые могут использоваться для проверки значения типа, когда наше значение JSON это null, а также способы, которыми мы можем приводить значение своего JSON к определённому типу:

 

Рисунок 2-1


Документация значения serde_json

Когда мы исполним команду Cargo run, мы обнаружим что Cargo скомпилировал ту корзину, которую мы определили в своих зависимостях. Затем мы обнаружим компиляцию своего собственного прикладного приложения и получим вывод на печать значение стоимости и относящиеся к нашей акции данные, как это отображено здесь:


first price: 19.4
{"history":[19.4,26.9,32.5], "name":"MonolithAi",\
  "price":43.7}
 	   

Вернувшись обратно к документации, мы способны создавать и свою собственную. Это просто; нам не требуется ничего устанавливать. Всё что нам требуется, так это создать документацию в своём коде, также как docstrings в Python. Для демонстрации этого мы можем создать некую функцию, которая складывает вместе две переменные и определяет строки документации следующим кодом:


/// Adds two numbers together.
///
/// # Arguments
/// * one (i32): one of the numbers to be added
/// * two (i32): one of the numbers to be added
///
/// # Returns
/// (i32): the sum of param one and param two
///
/// # Usage
/// The function can be used by the following code:
///
/// '''rust
/// result: i32 = add_numbers(2, 5);
/// '''
fn add_numbers(one: i32, two: i32) -> i32 {
    return one + two
}
 	   

Мы можем обнаружить, что эта документация является Markdown! Данный пример является излишеством для такого типа функции. Стандартный разработчик должен быть способным реализовывать подобную функцию без каких бы то ни было образцов. Для более сложных функций и структур следует отметить, что ничто не мешает нам документировать примеры кода относительно того как реализовывать то, что мы документируем. Для сборки документации требуется всего одна команда:


cargo doc
		

После завершения данного процесса мы можем открыть свою документацию следующей командой:


cargo doc --open
		

Это откроет нашу документацию в веб браузере как отображено на Рисунке 2-2:

 

Рисунок 2-2


Представление документации нашего модуля

Здесь мы можем видеть, что доступны наши функции main и add_numbers. Слева мы также можем обнаружить, что установленные нами зависимости также доступны. Если мы кликнем по своей функции add_numbers, мы сможем обнаружить написанный нами Markdown, как это отражено на Рисунке 2-3:

 

Рисунок 2-3


Представление документации нашей функции add_numbers

Вот и всё - мы способны создавать интерактивную документацию своего кода по мере построения своего приложения. Следует отметить, что остальная часть этой книги не будет использовать Markdown во фрагментах своего кода; это просто удлинило бы нашу книгу. Однако, по мере написания кода рекомендуется документировать все структуры и функции.

Теперь, когда мы исполнили свой код, настроили структуру базового приложения и документировали свой код, мы готовы перейти к своему следующему разделу построения структуры своего приложения во множестве файлов.

Структурирование кода по множеству файлов и модулей

Для сборки своего модуля мы намерены выполнить следующие этапы:

  1. Разметить структуру файлов и папок.

  2. Создать свои структуры Stock.

  3. Привязать свою структуру Stock к соответствующему файлу main.

  4. Воспользоваться модулем stocks в своём файле main.

  5. Добавить код из другого модуля.

Теперь, когда мы на этапе построения своего приложения из множества файлов, нам придётся определить свой первый модуль в нашем приложении, которым выступает модуль stocks.

  1. Мы можем выполнить свой модуль имеющим структуру, определяемую следующим образом:

    
    ├── main.rs
    └── stocks
        ├── mod.rs
        └── structs
            ├── mod.rs
            └── stock.rs
    		

    Мы выбрали эту структуру для обеспечения большей гибкости; если нам требуется добавить дополнительные структуры, мы можем выполнить это в своём каталоге structs. Мы также можем добавить и прочие каталоги помимо своего каталога structs. Например, мы можем пожелать собрать механизм хранения данных для своих акций. Этого можно достичь добавив каталог storage в свой каталог stocks и применяя его во всём модуле по мере необходимости.

  2. На данный момент мы просто хотим создать некую структуру акции в своём модуле stocks, импортировать его в свой файл main.rs и воспользоваться им. На первый шаг состоит в определении своей структуры Stock в файле stock.rs при помощи такого кода:

    
    pub struct Stock {
        pub name: String,
        pub open_price: f32,
        pub stop_loss: f32,
        pub take_profit: f32,
        pub current_price: f32
    }
    		

    Это выглядит знакомо, поскольку это та же самая структура Stock, которую мы определяли в своей предыдущей главе. Однако имеется лёгкое отличие. Нам следует заметить, что имеется ключевое слово pub перед определением этой структуры и определением каждого поля. Это обусловлено тем, что нам приходится объявлять их общедоступными (public) прежде чем мы сможем применять их вне этого файла. Это также применимо и к реализуемым в том же самом файле функциям, как это отображено в приводимом ниже коде:

    
    impl Stock {
        pub fn new(stock_name: &str, price: f32) -> \
          Stock {
            return Stock{
                name: String::from(stock_name),
                open_price: price,
                stop_loss: 0.0,
                take_profit: 0.0,
                current_price: price
            }
        }
        pub fn with_stop_loss(mut self, value: f32) \
          -> Stock {
            self.stop_loss = value;
            return self
        }
        pub fn with_take_profit(mut self, value: f32) \
          -> Stock {
            self.take_profit = value;
            return self
        }
        pub fn update_price(&mut self, value: f32) {
            self.current_price = value;
        }
    }
    		

    Теперь мы можем видеть, что у нас имеется общедоступная структура, которая доступна со всеми её функциями.

Теперь нам придётся разрешить свою структуру для применения в своём файле main.rs. Именно тут вступают в игру файлы mod.rs. mod.rs по существу это файлы __init__.py в Python. Они отображают что данный каталог это модуль. Однако, в отличие от Python, чтобы быть способными иметь доступ к ним из прочих файлов, структуры данных Rust необходимо объявлять общедоступными. На Рисунке 2-4 мы можем видеть как эта структура передаётся через модуль stocks в наш файл main.rs:

 

Рисунок 2-4


Как структура передаётся через модули

Здесь мы наблюдаем, что мы просто объявляем общедоступным свою структуру в самом дальнем модуле от main.rs в файле mod.rs, относящемся к данному каталогу. Затем мы объявляем общедоступным свой модуль structs в файле stocks mod.rs. Теперь самое время пояснить выражение mod в таких объявлениях модулей. Если мы пожелаем, мы можем объявлять в одном файле множество модулей. Следует подчеркнуть, в нашем примере это не так. При помощи приводимого далее кода мы можем объявить модуль one и модуль two в одном файле:


mod one {
    . . .
}
Mod two {
    . . .
}
		

Теперь, когда мы определили свои модули в нашем примере проекта main, мы просто объявляем свой модуль stocks в соответствующем файле main.rs. Основная причина того, почему это не общедоступное объявление состоит в том, что сам файл main.rs выступает основной точкой входа для нашего приложения; мы не будем импортировать этот модуль куда либо ещё:

  1. Теперь, когда наша структура доступна, мы можем просто применять её так, как если бы она была бы определена в том же самом файле следующим образом:

    
    mod stocks;
    use stocks::structs::stock::Stock;
    fn main() {
        let stock: Stock = Stock::new("MonolithAi", 36.5);
        println!("here is the stock name: {}",\
          stock.name);
        println!("here is the stock name: {}",\
          stock.current_price);
    }
    		
  2. Исполнение данного кода без всяких сюрпризов выдаёт нам:

    
    here is the stock name: MonolithAi
    here is the stock name: 36.5
    		

Теперь, когда у нас имеются необходимые основы применения структур из различных рассматриваемых файлов, чтобы стать более гибкими,мы можем перейти к изучению прочих направлений доступа к структурам данных из прочих файлов:

  1. Самое первое понятие, которое нам надлежит рассмотреть это доступ из файлов в том же самом каталоге. Для демонстрации этого мы можем сделать одноразовый пример построения функции печати в своих структурах. В новом файле со значением пути src/stocks/structs/utils.rs мы можем создать игрушечную функцию которая лишь выводит на печать что наш конструктор для соответствующей структуры сработал, как это показано в приводимом ниже коде:

    
    pub fn constructor_shout(stock_name: &str) -> () {
        println!("the constructor for the {} is firing", \
          stock_name);
    }
    		
  2. Затем мы объявляем в своём файле src/stocks/structs/mod.rs следующий код:

    
    pub mod stock;
    mod utils;
    		

    Необходимо отметить, что мы не превращаем его в общедоступный; мы просто объявляем его вместо этого. Ничто не запрещает нам превратить его в общедоступный; тем не менее, при подходе без общедоступности, мы делаем возможным доступ к нему только для файлов внутри текущего каталога src/stocks/structs/stock.rs.

  3. Теперь мы хотим чтобы наша структура Stock выполняла доступ к нему в нашем конструкторе, что можно осуществить при помощи импорта в src/stocks/structs/stock.rs следующей строкой:

    
    use super::utils::constructor_shout;
    		
  4. Если мы хотим переместить свою ссылку в соответствующий каталог src/stocks/, мы можем воспользоваться super::super. Мы можем соединять в цепочку столько super, сколько пожелаем, в зависимости от глубины своего дерева. Необходимо отметить, что мы можем выполнять доступ только к тому, что мы объявили в своём файле mod.rs в этом каталоге. В своём файле src/stocks/structs/stock.rs мы теперь можем воспользоваться функцией в нашем конструкторе при помощи такого кода:

    
    pub fn new(stock_name: &str, price: f32) -> Stock {
        constructor_shout(stock_name);
        return Stock{
            name: String::from(stock_name),
            open_price: price,
            stop_loss: 0.0,
            take_profit: 0.0,
            current_price: price
        }
    }
    		
  5. Теперь, когда мы запустим своё приложение, мы получим в своём терминале приводимый ниже вывод на печать:

    
    the constructor for the MonolithAi is firing
    here is the stock name: MonolithAi
    here is the stock name: 36.5
    		

    Мы можем видеть, что наша программа выполняется в точности так же, причём с соответствующей дополнительной строкой из функции util, которую мы импортировали. Если мы создадим ещё один модуль, мы сможем получить доступ к своему модулю stocks, потому как наш модуль stocks определён в основном файле main.rs.

Хотя нам удалось управиться с доступом к структурам данных из различных файлов и модулей, это не слишком масштабируемо, тому же это влечёт к некоторым правилам в которых мы реализуем stocks. Чтобы мы имели возможность писать масштабируемый безопасный код, в следующем разделе нам требуется блокировать имеющуюся функциональность при помощи интерфейсов.

Сборка интерфейсов модуля

В отличие от Python, в котором мы способны импортировать всё что пожелаем, причём откуда угодно и в лучшем случае наши IDE всего лишь выделят нам подсветку синтаксиса, если мы попытаемся получить доступ к структурам данных, которые я явном виде не были превращены в общедоступные, Rust не будет активно компилироваться. Это даёт нам возможность на самом деле блокировать свои модули и реализовывать функциональные возможности через интерфейс.

Однако прежде чем мы начнём это, давайте полностью изучим какую функциональность мы будем блокировать. Хорошим практическим приёмом является применение максимально изолированных модулей. В нашем модуле stocks (акции) основная логика должна содержаться лишь в том как обращаться с акциями и ни с чем иным. Это может показаться слегка излишним, однако, если задуматься, когда речь заходит об усложнениях, мы быстро осознаем что этот модуль будет расширяться.

В демонстрационных целях этой главы давайте просто создадим функциональные возможности заказа акций.Мы можем либо купить, либо продать акции. Эти заказы на акции поступают многократно. Довольно часто можно купить n акций компании. Нам также необходимо проверить будет ли заказ на акции коротким или длительным. При помощи короткого заказа мы занимаем деньги у своего брокера, покупаем за эти деньги акции, сразу же их продаём и затем выкупаем эти акции обратно позже. Если цена акций падает, мы зарабатываем, поскольку удерживаем разницу при выплате брокеру. Когда мы открываем длительную позицию, мы просто покупаем акции и держим их. Если они пойдут вверх, мы заработаем на них, поэтому в зависимости от типа ордера будут различные результаты.

Мы должны отметить что это не книга по разработке программного обеспечения для рынка акций, а потому нам следует сохранять подробности простыми, дабы не запутаться. Некий образец подхода для нас чтобы получить демонстрацию интерфейсов состоит в применении многоуровневого подхода, описанного на Рисунке 2-5:

 

Рисунок 2-5


Подход к образцу интерфейса модуля

Для достижения этого подхода мы можем привести в исполнение следующие шаги:

  1. Выполнить структуру схемы своего модуля при помощи правильных файлов.

  2. Создать некое упорядочение для своих различных типов заказов.

  3. Построить структуру заказа.

  4. Установить корзину (crate) chrono, необходимую для объектов datetime.

  5. Создать конструктор заказа, который применяет корзину chrono.

  6. Создать динамические значения для этой структуры.

  7. Создать интерфейс закрытия заказа.

  8. Создать интерфейс открытия заказа.

  9. Воспользоваться интерфейсами заказа в своём файле main.

Итак, приступим:

  1. Здесь мы разрешаем себе доступ к структуре stock только через структуру order. И снова, имеется множество способов подхода к этой задаче, что является демонстрацией того, как создавать интерфейсы Rust. Чтобы добиться этого в нашем коде, у нас имеется определяемая ниже файловая структура:

    
    ├── main.rs
    └── stocks
        ├── enums
        │   ├── mod.rs
        │   └── order_types.rs
        ├── mod.rs
        └── structs
            ├── mod.rs
            ├── order.rs
            └── stock.rs
    		
  2. Прежде всего мы можем определить свои типы enum (нумерации) заказов в своём файле enums/order_types.rs при помощи такого кода:

    
    pub enum OrderType {
        Short,
        Long
    }
    		
  3. Мы будем пользоваться ими в своих заказах и интерфейсах. Чтобы сделать доступным этот тип нумерации для всего остального модуля нам придётся объявить их в своём файле enums/mod.rs при помощи следующего кода:

    
    pub mod order_types;
    		
  4. Теперь, когда мы построили свой тип enum, самое время пустить его в работу. Теперь мы можем построить свою структуру ордера в файле stocks/structs/order.rs с приводимым ниже кодом:

    
    use chrono::{Local, DateTime};
    use super::stock::Stock;
    use super::super::enums::order_types::OrderType;
    
    pub struct Order {
        pub date: DateTime,
        pub stock: Stock,
        pub number: i32,
        pub order_type: OrderType
    }
    		
  5. Здесь мы пользуемся корзиной chrono для определения времени размещения этого ордера; мы также должны указать для каких акций предназначен этот заказ, значение числа покупаемых акций, а также тип заказа. Мы должны не забыть определить свою зависимость chrono в нашем файле Cargo.toml следующим кодом:

    
    [dependencies]
    serde="1.0.117"
    serde_json="1.0.59"
    chrono="0.4.19"
    		

    Основная причина по которой мы оставляем свою структуру stock обособленной от структуры order состоит в разрешении гибкости. Например, могут иметься прочие вещи, которые мы можем выполнять со своими данными акций, которые не связаны с заказом. Мы можем пожелать собрать некую структуру, которая размещает акции в определённом списке отслеживания пользователя, а этот пользователь в реальности ничего не покупает, а всего лишь желает отслеживать значение доступности акций.

  6. Тем не менее, существуют и иные варианты применения данных акции. Учитывая это, мы можем видеть, что сохраняя необходимые сведения и методы относительно акции в некой индивидуальной структуре stock помогает не только снижать объём кода, который нам приходится писать когда мы добавляем дополнительные функциональные возможности, но также и стандартизировать имеющиеся для акции сведения. Это также упрощает нам сопровождение всего кода. Когда мы добавляем или удаляем поле, либо изменяем метод для данных акции, нам приходится изменять их лишь в одном месте, а не по множеству расположений. Наш конструктор для нашей структуры order может быть выполнен в том же самом файле при помощи следующего кода:

    
    impl Order {
        pub fn new(stock: Stock, number: i32, \
          order_type: OrderType) -> Order {
          let today: DateTime<Local> = Local::now();
            return Order{date: today, stock, number, \
              order_type}
        }
    }
    		

    Здесь мы создали структуру Order, получая параметры stock, number и order_type и создавая структуру datetime.

  7. Поскольку наш заказ сосредоточен на логике относительно цен этого заказа, поскольку он размещает значение числа акций, доставлявшихся в этом заказе, в своём блоке impl мы можем построить своё текущее значение этого заказа таким кодом:

    
    	pub fn current_value(&self) -> f32 {
            return self.stock.current_price * self \
              .number as f32
        }
    		

    Следует отметить, что в качестве параметра нам приходится применять &self вместо простого использования self. Это позволяет нам применять данную функцию много раз. Если бы этот параметр не был бы ссылкой, тогда нам приходилось бы перемещать свою структуру в эту функцию. Мы бы не были способны вычислять соответствующее значение множество раз и это было бы полезным делать только если соответствующий тип не Copy.

  8. Мы также можем построить на этой функции вычисление текущей прибыли в блоке impl при помощи приводимого ниже кода:

    
        pub fn current_profit(&self) -> f32 {
            let current_price: f32 = self.current_value();
            let initial_price: f32 = self.stock. \
              open_price * self.number as f32;
    
            match self.order_type {
              OrderType::Long => return current_price -\
                initial_price,
              OrderType::Short => return initial_price -\
                current_price
            }
        }
    		

    Здесь мы получаем текущую и начальную стоимости. Затем мы сопоставляем тип заказа, поскольку он меняет то как вычисляется прибыль. Теперь, когда наша структура завершена, нам придётся убедиться что эта структура доступна через её определение в нашем файле stocks/structs/mod.rs при помощи следующего кода:

    
    pub mod stock;
    pub mod order;
    		
  9. Теперь мы готовы к созданию своих интерфейсов. Чтобы собрать наш интерфейс в своём файле stocks/mod.rs, нам вначале придётся импортировать всё что нам требуется, как это показано в приводимом далее коде:

    
    pub mod structs;
    pub mod enums;
    
    use structs::stock::Stock;
    use structs::order::Order;
    use enums::order_types::OrderType;
    		
  10. Теперь, когда у нас имеется всё что нужно для построения нашего интерфейса, мы можем собрать свой интерфейс закрытия заказа при помощи такого кода:

    
    pub fn close_order(order: Order) -> f32 {
        println!("order for {} is being closed", \
          &order.stock.name);
        return order.current_profit()
    }
    		
  11. Это достаточно простой интерфейс; мы можем сделать больше, например вызов базы данных или API, однако для данной демонстрации нам достаточно выдать на печать значение подлежащей продаже акции и вернуть значение полученной нами текущей прибыли. Имея в виду это, мы можем собрать более сложный интерфейс открывая некий заказ в том же самом файле при помощи приводимого далее кода:

    
    pub fn open_order(number: i32, order_type: OrderType,\
                      stock_name: &str, open_price: f32,\
                      stop_loss: Option<f32>, \
                        take_profit: Option<f32>) -> \
                          Order { \
        println!("order for {} is being made", \
          &stock_name);              
        let mut stock: Stock = Stock::new(stock_name, \
          open_price);
        match stop_loss {
            Some(value) => stock = \
              stock.with_stop_loss(value),
            None => {}
        }
        match take_profit {
            Some(value) => stock = \
              stock.with_take_profit(value),
            None => {}
        }
        return Order::new(stock, number, order_type)
    }
    		

    Здесь мы получаем все необходимые нам параметры. Мы также представили тип аргумента Option<f32>, который реализуется как некий нумерующий тип. Это позволяет нам передавать передавать значение None. Затем мы создаём изменяемую акцию (так как стоимость может меняться и нам придётся обновлять её), а далее проверяем предоставлено ли значение stop_loss; если это так, мы затем добавляем останов потерь для этой акции. После этого мы проверяем предоставлено ли значение take_profit, обновляя им свою акцию в таком случае.

  12. Теперь, когда мы собрали все свои интерфейсы, всё что нам требуется, это воспользоваться ими в своём файле main.rs. В этом файле main нам необходимо импортировать все требуемые структуры и интерфейсы для их применения при помощи следующего кода:

    
    mod stocks;
    
    use stocks::{open_order, close_order};
    use stocks::structs::order::Order;
    use stocks::enums::order_types::OrderType;
    		
  13. В своей функции main мы можем начать помещать эти интерфейсы в работу, создавая новые изменяемые заказы следующим кодом:

    
    println!("hello stocks");
    let mut new_order: Order = open_order(20, \
        OrderType::Long, "bumper", 56.8, None, None);
    		
  14. Здесь мы устанавливаем take_profit и stop_loss в None, но мы можем добавить их в случае необходимости. Для прояснения того что мы привнесли, мы можем выдать на печать свои текущие значение и прибыль таким кодом:

    
    println!("the current price is: {}",
        &new_order.current_value());
    println!("the current profit is: {}",
        &new_order.current_profit());
    		
  15. Далее мы получаем некие движения на ранке акций, которые можно имитировать обновлением значения стоимости и выводом на печать полученного значения наших инвестиций при каждом изменении при помощи следующего кода:

    
    new_order.stock.update_price(43.1);
    println!("the current price is: {}", \
        &new_order.current_value());
    println!("the current profit is: {}", \
        &new_order.current_profit());
    
    new_order.stock.update_price(82.7);
    println!("the current price is: {}", \
             &new_order.current_value());
    println!("the current profit is: {}", \
             &new_order.current_profit());
    		
  16. Теперь у нас имеется некая прибыль и мы продадим свои акции чтобы закрыть имеющийся заказ и выведем на печать полученную прибыль таким кодом:

    
    let profit: f32 = close_order(new_order);
    println!("we made {} profit", profit);
    		
  17. Теперь наши интерфейсы, модуль и файл main собраны. Запуск команды Cargo run снабдит нам с таким выводом на печать:

    
    hello stocks
    the constructor for the bumper is firing
    the current price is: 1136
    the current profit is: 0
    the current price is: 862
    the current profit is: -274
    the current price is: 1654
    the current profit is: 518
    order for bumper is being closed
    we made 518 profit
    		

Как мы можем видеть, наш модуль работает и он обладает ясным интерфейсом. Для целей этой книги наш пример заканчивается на этом, поскольку мы показали как мы можем собирать модули в Rust с интерфейсами. Тем не менее, если вы желаете двинуться далее с построением этого приложения, мы можем предложить подход, отображаемый на Рисунке 2-6:

 

Рисунок 2-6


Подстройка нашего приложения

В соответствующем модуле учётной записи мы бы собрали структуры данных для отслеживания значения числа пользователей, проходящих через торги. Затем мы бы собрали модуль storage, который бы обладал интерфейсами считывания и записи для учётных записей и акций. Основная причина почему хранение это обособленный модуль состоит в том, что мы придерживаться тех же самых интерфейсов и кромсать свою логику хранения под её капотом.

Например, мы можем начать с простой системы хранения файлом JSON для разработки и локального применения; однако, наше приложение затем поместится в сервере и загрузит пользователей, начинающих торги и доступ к своим учётным записям. Мы сможем переключиться с чтения и записи в файл на драйвер базы данных с соответствующей моделью базы данных. Наша система затем получит большой объём обмена и наше приложение начнёт расщепляться на кластер микрослужб. Одно приложение будет всё ещё общаться с какой- то базой данных, в то время как другое для часто запрашиваемых акций/ учётных записей могло бы общаться в кэшем Redis.

С учётом сказанного, оставление хранения обособленным сохраняет нам гибкость. Изменение необходимых требований для нашего хранения не повлечёт к разрушению общего строения. По существу, файл настройки мог позволять переключаться на различные метода, в зависимости от имеющейся среды. Пока ваши интерфейсы остаются прежними, перестройка кода не будет гигантской задачей.

Преимущество документирования при кодировании

По мере расширения нашего модуля на множество файлов, мы теперь ссылаемся на функции и структуры, которые пребывают в разных файлах. Именно тут можно отметить важность документации. Мы можем вернуться к своему моменту в технических требованиях применения Visual Studio Code. Этот код в GitHub полностью документирован. Если установлены подключаемые модули Rust, простое наведение указателя мыши над соответствующей структурой или функцией вызовет всплывание соответствующей документации, позволяя нам увидеть что требуется в нашем интерфейсом, как это отображено на Рисунке 2-7:

 

Рисунок 2-7


Всплывающая документация в Visual Studio Code

Существует причина, по которой плохо структурированный код без документации именуется техническим долгом, и она состоит в том, что со временем он накапливает затраты. Плохо структурированный од без документации быстро разрабатывать, однако по мере роста его приложения становится всё труднее что- либо менять и понимать что происходит. Хорошо структурированный модуль с добротной документацией Markdown Rust это отличный способ поддержания высокой производительности вас и вашей команды.

Теперь у нас имеется работающее приложение, которое занимает множество страниц и является ясным и масштабируемым. Однако юзер не может применять его динамически, поскольку всё жёстко закодировано. Это не практично. В своём следующем разделе мы взаимодействуем с имеющимся окружением, а потому мы можем передавать в свою программу параметры.

Взаимодействие со средой

Мы на том этапе, на котором единственный момент, который удерживает нас от построения полностью работающего приложения это взаимодействие с внешним окружением. Как постулировалось в нашем предыдущем разделе, это открытая тема, которая охватывает всё, о получения аргументов командной строки до взаимодействия с серверами и базами данных. Как и в предыдущем разделе, мы рассмотрим достаточно, чтобы разобраться с тем как создавать структурный код Rust, получающий извне данные и обрабатывающий их.

Чтобы изучить это, мы намерены заставить своё приложение акций получать аргументы командной строки от своего пользователя с тем, чтобы мы бы смогли либо покупать, либо продавать акции. Мы не будем усложнять вещи выбором того будут ли это короткие или длинные позиции и не будем вводить хранилище.

Тем не менее, к окончанию данного раздела мы будем вооружены походом к построению кода, который масштабируется и получает данные из внешнего мира. При помощи него, дальнейшее знакомство с корзинами (crates), которые подключаются к базам данных или считывают/ записывают файлы позволит нам бесшовно добавить их в наш хорошо структурированный код. Что касается баз данных, мы рассмотрим как отражать имеющуюся схему базы данных и подключаться к ней в Главе 10, Внедрение Rust в прикладное приложение Flask Python.

Для своего игрушечного примера мы выработаем какое- то случайное число для стоимости продажи своей акции чтобы вычислить продадим ли мы с прибылью или с убытком. Теперь мы выполним это добавив корзину rand в свой раздел зависимостей в имеющийся файл Cargo.toml с rand="0.8.3". Мы можем взаимодействовать со своей средой осуществляя такие шаги:

  1. Импортировать все необходимые корзины.

  2. Собрать все входные данные в своём окружении.

  3. Обработать свои входные данные при помощи заказа.

Давайте приступим:

  1. Теперь, когда добавлена наша корзина rand, мы можем добавить некие дополнительный импорт, который нам требуется в основном файле main.rs следующим кодом:

    
    use std::env;
    use rand::prelude::*;
    use std::str::FromStr;
    		

    Для получения передаваемых в Cargo параметров мы применяем env. Из prelud в корзине rand мы импортируем всё, поэтому мы можем генерировать случайные числа, к тому же мы импортируем признак (trait) FromStr, а потому мы можем преобразовывать строки, передаваемые из параметров командной строки в числа.

  2. В своей функции main мы сперва собираем все переданные из соответствующей командной строки параметры таким кодом:

    
    let args: Vec<String> = env::args().collect();
    let action: &String = &args[1];
    let name: &String = &args[2];
    let amount: i32 = i32::from_str(&args[3]).unwrap();
    let price: f32 = f32::from_str(&args[4]).unwrap();
    		

    Мы устанавливаем, что мы намерены собрать все параметры командной строки в векторе строк. Мы делаем это по той причине, что практически всё можно представлять строкой. Затем мы определяем все необходимые нам параметры. Следует отметить, что мы начинаем с индекса 1 вместо 0. Это обусловлено тем, что индекс 0 заполнен самой командой run. Мы также можем заметить, что мы, когда нам требуются строки непосредственно распакованными, мы преобразовываем их в числа. Это слегка рискованно; в идеале мы должны устанавливать соответствие полученного результата из функции from_str и снабжать своего пользователя лучшими сведениями при надлежащем построении промышленного инструмента командной строки.

  3. Теперь, когда у нас имеется всё что нам необходимо, мы создаём новый заказ при помощи собранных нами сведений при помощи приводимого ниже кода:

    
    let mut new_order: Order = open_order(amount, \
        OrderType::Long, &name.as_str(), price, \
            None, None);
    		

    Мы создаём новый заказ каждый раз даже если это продажа, так как у нас нет хранилища и нам необходимо иметь все свои структурированные данные и логику относительно своей позиции акции. Затем мы устанавливаем соответствие своим действиям. Если мы хотим продать свои акции, мы вырабатываем новую стоимость для этой акции перед продажей. С учётом этого мы можем обнаружить остались ли мы в прибыли или нет при помощи такого кода:

    
    match action.as_str() {
        "buy" => {
            println!("the value of your investment is:\
              {}", new_order.current_value());
        }
        "sell" => {
            let mut rng = rand::thread_rng();
            
            let new_price_ref: f32 = rng.gen();
            let new_price: f32 = new_price_ref * 100 as \
              f32;
            new_order.stock.update_price(new_price);
            let sale_profit: f32 = close_order(new_order);
            println!("here is the profit you made: {}", \
                sale_profit);
        }
        _ => {
            panic!("Only 'buy' and 'sell' actions are \
              supported");
        }
    }
    		

    Следует отметить, что у нас имеется _ в конце выражения соответствия. Это происходит по той причине, что теоретически строка может быть любой, а Rust безопасный язык. Это не позволит нам скомпилировать код, если мы не будем учитывать каждый результат. _ это универсальный шаблон. Если созданы не все шаблоны сопоставления, то выполняется он. Для нашего случая мы просто возбуждаем ошибку, заявляя, что поддерживаются только продажи и покупки.

  4. Для запуска нашей программы мы выполняем такую команду:

    
    cargo run sell monolithai 26 23.4
    		
  5. Её исполнение снабдит нас следующим выводом:

    
    order for monolithai is being made
    order for monolithai is being closed
    here is the profit you made: 1825.456
    		

    Значение прибыли может отличаться, поскольку вырабатываемое нами число случайное.

Вот и всё — наше приложение интерактивное и масштабируемое. Если вы хотите создавать более полные интерфейсы командной строки с меню справки, рекомендуется ознакомиться и воспользоваться корзиной clap.

Выводы

В этой главе мы прошлись по основам Cargo. При помощи Cargo мы управляем сборкой базовых приложений, документируем их, компилируем их и затем выполняем их. Глядя на то, насколько опрятной и простой была эта реализация, становится понятным почему Rust является одним из самых популярных языков программирования. Управление всеми функциональными возможностями, документацией и зависимостями в одном файле из нескольких строк кода ускоряет весь процесс целиком. В сочетании со строгим, предупредительным компилятором превращает Rust в не требующего семи пядей, когда дело доходит до управления сложными проектами. Мы управились со своей сложностью, обернув свой модуль простыми в применении интерфейсами и взаимодействуя с вводом данных пользователя через его командную строку.

Прямо сейчас, пока вы простаиваете, вы можете начать строить код для решения ряда задач. Если вы хотите собрать приложение, которое взаимодействует в качестве веб сервера Rust с интерфейсом и базой данных, я рекомендую вам прочесть мою другую книгу по веб разработке на Rust, Rust Web Programming и начать с Главы 3, посокльку вы теперь обладаете достаточным объёмом основ Rust чтобы приступить к строительству серверов Rust.

В своей следующей главе из этой книги мы рассмотрим как пользоваться одновременностью Rust.

Вопросы

  1. Продолжая писать код как мы документируем его?

  2. Почему важна единая концепция изоляции модулей?

  3. Как мы позволяем своим модулям сохранять преимущества изолированных модулей?

  4. Как мы управляем зависимостями в своём приложении?

  5. Как мы можем гарантировать, что в выражении сопоставления учитываются все результаты, при том, что теоретически существует бесконечное число результатов, таких как сопоставление различных строк?

  6. Допустим, у меня имеется структура с названием SomeStruct в каком- то файле some_file/some_struct.rs. Как мы можем сделать её доступной вне того каталога, в котором она расположена?

  7. Допустим, мы изменили своё мнение относительно нашей структуры SomeStruct из 6 вопроса и мы хотим чтобы она стала доступной только в нашем каталоге some_file/. Как это сделать?

  8. Как мы можем выполнить доступ к своей структуре SomeStruct в соответствующем файле some_file/another_struct.rs?

Ответы

  1. Наши строки документации способны поддерживать Markdown в то время как мы строим свои структуры и функции. Поскольку это Markdown, мы можем документировать способы реализации структур или функций. Когда мы пользуемся Visual Studio Code, это также повышает нашу производительность, так как простое наведение указателя мыши на функцию или структуру открывает их документацию.

  2. Ограничение наших модулей одной концепцией повышает гибкость приложения, позволяя нам нарезать и изменять модули по мере необходимости.

  3. Чтобы наши модули были изолированы, нам нужно, чтобы интерфейсы модуля оставались теми же самыми; это означает, что мы можем изменить логику внутри модуля, не изменяя ничего в остальной части приложения. Если мы удалим свой модуль, нам нужно будет искать только реализации интерфейса во всем приложении, а не реализацию всех функций и структур в модуле.

  4. Мы управляем зависимостями в файле Cargo.toml. Простое исполнение Cargo установит все имеющиеся у нас требования при компиляции перед исполнением.

  5. Мы можем учитывать всё, перехватывая что угодно что не удовлетворяет всем нашим соответствиям. Это выполняется через реализацию шаблона _ в самом конце нашего выражения соответствия, выполняя прикреплённый к нему код.

  6. Мы можем сделать его общедоступным, написав pub mod some_struct; в своём файле some_file/mod.rs.

  7. Написав mod some_struct; в своём файле some_file/mod.rs мы превращаем его в общедоступный только в каталоге some_file/.

  8. Мы можем получить доступ в SomeStruct, набрав в файле some_file/another_struct.rs use super::some_struct::SomeStruct;.

Дальнейшее чтение