» Rust:使用Rocket构建REST API » 2. 开发 » 2.5 4层架构

4层架构

目前,你的代码可以跑起来。但是,细观之下,不够“洁净”。

不够“洁净”

如下是代码不够洁净的几个点:

// 数据库实例
lazy_static::lazy_static! {
    static ref DB: Mutex<Database> = Mutex::new(Database::new().unwrap());
}
  • 不应该显式地使用全局变量来承载数据库实例并到处使用。

在 Rust 中,静态项是在整个程序执行期间都有效的值(具有 'static 生命周期)。

pub fn new() -> SqliteResult<Self> {
    let conn = Connection::open("test.db")?;
    conn.execute(
        "CREATE TABLE IF NOT EXISTS books (
            id INTEGER PRIMARY KEY,
            title TEXT NOT NULL,
            author TEXT NOT NULL,
            published_at TEXT NOT NULL,
            description TEXT NOT NULL,
            isbn TEXT NOT NULL,
            total_pages INTEGER NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
            updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        )",
        [],
    )?;
    Ok(Database { conn })
}
  • 业务代码中去初始化数据库不是最佳实践。数据库操作属于“基础架构”,而不是“业务”。它们属于不同的关注点,你应该将其分开到不同地方。
#[get("/books")]
fn get_books() -> Result<Json<Vec<model::Book>>, status::Custom<Json<ErrorResponse>>> {
    let db = DB.lock().unwrap();
    match db.get_books() {
        Ok(books) => Ok(Json(books)),
        Err(err) => Err(status::Custom(
            Status::InternalServerError,
            Json(ErrorResponse {
                error: err.to_string(),
            }),
        )),
    }
}
  • 不推荐在业务代码中直接依赖三方库的数据库实例。db 来自 Rusqlite 三方 crate。 要不然将来想切换到其他数据库驱动或 ORM 库的话,业务改动成本极高。

关注点分离

Clean Arch 来自 https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

整洁架构(The Clean Architecture) 是 Robert C. Martin 在他的书《整洁架构:软件结构与设计的工匠指南》中引入的软件架构模式。 它强调了软件系统内的关注点分离,其主要目标是使系统在其生命周期内更易于维护、可扩展和可测试。

4层架构在细节上有所不同,但与整洁架构类似。它们都有相同的目标,即关注点分离。它们都通过将软件划分为层来实现这种分离。

4 layers

  1. Adapter 适配器层:负责路由和适配框架、协议和前端显示(Web、移动等)。

  2. Application 应用层:主要负责获取输入、组装上下文、参数验证,调用领域层进行业务处理。该层是开放式的,应用层可以绕过领域层并直接访问基础设施层。

  3. Domain 领域层:封装核心业务逻辑,并通过领域服务和领域实体的方法向应用层提供业务实体和业务逻辑操作。领域是应用程序的核心,不依赖于任何其他层

  4. Infrastructure 基础设施层:主要处理诸如数据库上的CRUD操作、搜索引擎、文件系统、用于分布式服务的RPC等技术细节。外部依赖项需要通过此处的网关进行转换,然后才能被上面的应用层和领域层使用。

重构

让我们基于4层架构来重构。

新的目录结构如下所示:

projects/lr_rest_books_rust
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── src
│   ├── adapter
│   │   ├── mod.rs
│   │   └── router.rs
│   ├── application
│   │   ├── executor
│   │   │   ├── book_operator.rs
│   │   │   └── mod.rs
│   │   ├── mod.rs
│   │   └── wire_helper.rs
│   ├── domain
│   │   ├── gateway
│   │   │   ├── book_manager.rs
│   │   │   └── mod.rs
│   │   ├── mod.rs
│   │   └── model
│   │       ├── book.rs
│   │       └── mod.rs
│   ├── infrastructure
│   │   ├── config
│   │   │   └── mod.rs
│   │   ├── database
│   │   │   ├── mod.rs
│   │   │   └── sqlite.rs
│   │   └── mod.rs
│   └── main.rs
└── test.db

移动 model.rsdomain/model/book.rs:

注意: 本教程中后续为了表述路径方便,一律忽略 src/ 前缀。

#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct Book {
    pub id: u32,
    pub title: String,
    pub author: String,
    pub published_at: String,
    pub description: String,
    pub isbn: String,
    pub total_pages: u32,
    pub created_at: String,
    pub updated_at: String,
}

创建兄弟文件 mod.rs 用于导出符号:

mod book;

pub use book::Book;

domain 目录存放核心业务规则。非业务代码远离该目录。

创建 domain/gateway/book_manager.rs:

use std::error::Error;

use crate::domain::model;

pub trait BookManager: Send + Sync {
    fn create_book(&self, b: &model::Book) -> Result<u32, Box<dyn Error>>;
    fn update_book(&self, id: u32, b: &model::Book) -> Result<(), Box<dyn Error>>;
    fn delete_book(&self, id: u32) -> Result<(), Box<dyn Error>>;
    fn get_book(&self, id: u32) -> Result<Option<model::Book>, Box<dyn Error>>;
    fn get_books(&self) -> Result<Vec<model::Book>, Box<dyn Error>>;
}

BookManager 接口定义领域实体 Book 的业务能力。 此处只是特征,具体实现交由基础架构层去完成。 另外,这些方法使得应用层可以调用它执行业务逻辑。

创建 infrastructrue/database/sqlite.rs:

use std::error::Error;
use std::sync::Mutex;

use chrono::Utc;
use rusqlite::{params, Connection, Result as RusqliteResult};

use crate::domain::gateway::BookManager;
use crate::domain::model;

pub struct SQLitePersistence {
    conn: Mutex<Connection>,
}

impl SQLitePersistence {
    pub fn new(file_name: &str) -> RusqliteResult<Self> {
        let conn = Connection::open(file_name)?;
        Ok(SQLitePersistence {
            conn: Mutex::new(conn),
        })
    }
}

impl BookManager for SQLitePersistence {
    fn create_book(&self, b: &model::Book) -> Result<u32, Box<dyn Error>> {
        let conn = self.conn.lock().unwrap();
        conn.execute(
            "INSERT INTO books (title, author, published_at, description, isbn, total_pages)
             VALUES (?, ?, ?, ?, ?, ?)",
            params![
                b.title,
                b.author,
                b.published_at,
                b.description,
                b.isbn,
                b.total_pages,
            ],
        )?;
        Ok(conn.last_insert_rowid() as u32)
    }

    fn update_book(&self, id: u32, b: &model::Book) -> Result<(), Box<dyn Error>> {
        let conn = self.conn.lock().unwrap();
        conn.execute(
            "UPDATE books SET title = ?, author = ?, published_at = ?, description = ?, isbn = ?, total_pages = ?, updated_at = ?
             WHERE id = ?",
            params![
                b.title,
                b.author,
                b.published_at,
                b.description,
                b.isbn,
                b.total_pages,
                Utc::now().to_rfc3339(),
                id,
            ],
        )?;
        Ok(())
    }

    fn delete_book(&self, id: u32) -> Result<(), Box<dyn Error>> {
        let conn = self.conn.lock().unwrap();
        conn.execute("DELETE FROM books WHERE id = ?1", params![id])?;
        Ok(())
    }

    fn get_book(&self, id: u32) -> Result<Option<model::Book>, Box<dyn Error>> {
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare("SELECT * FROM books WHERE id = ?1")?;
        let book_iter = stmt.query_map(params![id], |row| {
            Ok(model::Book {
                id: row.get(0)?,
                title: row.get(1)?,
                author: row.get(2)?,
                published_at: row.get(3)?,
                description: row.get(4)?,
                isbn: row.get(5)?,
                total_pages: row.get(6)?,
                created_at: row.get(7)?,
                updated_at: row.get(8)?,
            })
        })?;

        for result in book_iter {
            return Ok(Some(result?));
        }
        Ok(None)
    }

    fn get_books(&self) -> Result<Vec<model::Book>, Box<dyn Error>> {
        let conn = self.conn.lock().unwrap();
        let mut stmt = conn.prepare("SELECT * FROM books")?;
        let book_iter = stmt.query_map([], |row| {
            Ok(model::Book {
                id: row.get(0)?,
                title: row.get(1)?,
                author: row.get(2)?,
                published_at: row.get(3)?,
                description: row.get(4)?,
                isbn: row.get(5)?,
                total_pages: row.get(6)?,
                created_at: row.get(7)?,
                updated_at: row.get(8)?,
            })
        })?;

        let mut books = Vec::new();
        for result in book_iter {
            books.push(result?);
        }
        Ok(books)
    }
}

如你所见,结构体 SQLitePersistence 实现了特征 BookManager 中所有方法。

创建 infrastructrue/config/mod.rs:

use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize)]
pub struct Config {
    pub app: ApplicationConfig,
    pub db: DBConfig,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct DBConfig {
    pub file_name: String,
}

#[derive(Debug, Deserialize, Serialize)]
pub struct ApplicationConfig {
    pub port: i32,
}

结构体 Config 将硬编码的配置项拎出。一般而言,配置“拎出来”都是好的工程实践。

创建 application/executor/book_operator.rs:

use std::sync::Arc;

use crate::domain::gateway;
use crate::domain::model;

pub struct BookOperator {
    book_manager: Arc<dyn gateway::BookManager>,
}

impl BookOperator {
    pub fn new(b: Arc<dyn gateway::BookManager>) -> Self {
        BookOperator { book_manager: b }
    }

    pub fn create_book(&self, b: model::Book) -> Result<model::Book, Box<dyn std::error::Error>> {
        let id = self.book_manager.create_book(&b)?;
        let mut book = b;
        book.id = id;
        Ok(book)
    }

    pub fn get_book(&self, id: u32) -> Result<Option<model::Book>, Box<dyn std::error::Error>> {
        self.book_manager.get_book(id)
    }

    pub fn get_books(&self) -> Result<Vec<model::Book>, Box<dyn std::error::Error>> {
        self.book_manager.get_books()
    }

    pub fn update_book(
        &self,
        id: u32,
        b: model::Book,
    ) -> Result<model::Book, Box<dyn std::error::Error>> {
        self.book_manager.update_book(id, &b)?;
        Ok(b)
    }

    pub fn delete_book(&self, id: u32) -> Result<(), Box<dyn std::error::Error>> {
        self.book_manager.delete_book(id)
    }
}

应用层封装所有底层的逻辑,并提供接口给顶层的适配层

创建 application/wire_helper.rs:

use std::sync::Arc;

use crate::domain::gateway;
use crate::infrastructure::database;
use crate::infrastructure::Config;

pub struct WireHelper {
    persistence: Arc<database::SQLitePersistence>,
}

impl WireHelper {
    pub fn new(c: &Config) -> Result<Self, Box<dyn std::error::Error>> {
        let persistence = Arc::new(database::SQLitePersistence::new(&c.db.file_name)?);
        Ok(WireHelper { persistence })
    }

    pub fn book_manager(&self) -> Arc<dyn gateway::BookManager> {
        Arc::clone(&self.persistence) as Arc<dyn gateway::BookManager>
    }
}

WireHelper 帮助实现 DI(依赖注入)。 如果想要更全面的 DI 支持,考虑使用 shaku

有了这些准备之后,router 终于可以“整洁地”完成工作了。 它不用感知底层数据库的存在。此刻,关注点被分离了。

创建 adapter/router.rs:

use rocket::http::Status;
use rocket::response::{content, status};
use rocket::serde::json::Json;

use crate::application;
use crate::application::executor;
use crate::domain::model;

pub struct RestHandler {
    book_operator: executor::BookOperator,
}

#[derive(serde::Serialize)]
pub struct ErrorResponse {
    error: String,
}

#[get("/")]
pub fn health_check() -> content::RawJson<&'static str> {
    content::RawJson("{\"status\":\"ok\"}")
}

#[get("/books")]
pub fn get_books(
    rest_handler: &rocket::State<RestHandler>,
) -> Result<Json<Vec<model::Book>>, status::Custom<Json<ErrorResponse>>> {
    match rest_handler.book_operator.get_books() {
        Ok(books) => Ok(Json(books)),
        Err(err) => Err(status::Custom(
            Status::InternalServerError,
            Json(ErrorResponse {
                error: err.to_string(),
            }),
        )),
    }
}

#[get("/books/<id>")]
pub fn get_book(
    rest_handler: &rocket::State<RestHandler>,
    id: u32,
) -> Result<Json<model::Book>, status::Custom<Json<ErrorResponse>>> {
    match rest_handler.book_operator.get_book(id) {
        Ok(book) => match book {
            Some(b) => Ok(Json(b)),
            None => Err(status::Custom(
                Status::NotFound,
                Json(ErrorResponse {
                    error: format!("book {id} not found"),
                }),
            )),
        },
        Err(err) => Err(status::Custom(
            Status::InternalServerError,
            Json(ErrorResponse {
                error: err.to_string(),
            }),
        )),
    }
}

#[post("/books", format = "json", data = "<book>")]
pub fn create_book(
    rest_handler: &rocket::State<RestHandler>,
    book: Json<model::Book>,
) -> Result<Json<model::Book>, status::Custom<Json<ErrorResponse>>> {
    match rest_handler.book_operator.create_book(book.into_inner()) {
        Ok(b) => Ok(Json(b)),
        Err(err) => Err(status::Custom(
            Status::InternalServerError,
            Json(ErrorResponse {
                error: err.to_string(),
            }),
        )),
    }
}

#[put("/books/<id>", format = "json", data = "<book>")]
pub fn update_book(
    rest_handler: &rocket::State<RestHandler>,
    id: u32,
    book: Json<model::Book>,
) -> Result<Json<model::Book>, status::Custom<Json<ErrorResponse>>> {
    match rest_handler
        .book_operator
        .update_book(id, book.into_inner())
    {
        Ok(b) => Ok(Json(b)),
        Err(err) => Err(status::Custom(
            Status::InternalServerError,
            Json(ErrorResponse {
                error: err.to_string(),
            }),
        )),
    }
}

#[delete("/books/<id>")]
pub fn delete_book(
    rest_handler: &rocket::State<RestHandler>,
    id: u32,
) -> Result<status::NoContent, status::Custom<Json<ErrorResponse>>> {
    match rest_handler.book_operator.delete_book(id) {
        Ok(_) => Ok(status::NoContent),
        Err(err) => Err(status::Custom(
            Status::InternalServerError,
            Json(ErrorResponse {
                error: err.to_string(),
            }),
        )),
    }
}

pub fn make_router(wire_helper: &application::WireHelper) -> RestHandler {
    RestHandler {
        book_operator: executor::BookOperator::new(wire_helper.book_manager()),
    }
}

替换 main.rs 中内容,使其更整洁一些:

#[macro_use]
extern crate rocket;

mod adapter;
mod application;
mod domain;
mod infrastructure;
use crate::adapter::router::*;
use crate::infrastructure::{ApplicationConfig, Config, DBConfig};

#[launch]
fn rocket() -> _ {
    let c = Config {
        app: ApplicationConfig { port: 8000 },
        db: DBConfig {
            file_name: "test.db".to_string(),
        },
    };
    let wire_helper = application::WireHelper::new(&c).expect("Failed to create WireHelper");
    let r = adapter::make_router(&wire_helper);
    rocket::build().manage(r).mount(
        "/",
        routes![
            health_check,
            get_books,
            get_book,
            create_book,
            update_book,
            delete_book
        ],
    )
}

重构完成。🎉赞!