Introduction 介绍
I tried implementing an API server using Rust and the Axum framework.
我尝试使用 Rust 和 Axum 框架实现 API 服务器。
Target Audience 本文受众
- Those who want to implement an API server with Rust.
那些想要用 Rust 实现 API 服务器的人。 - Those who want to implement DDD with Rust.
那些想要用 Rust 实现 DDD (Domain-driven design领域驱动设计)的人。
Not Covered 本文不包括
- Basic Rust syntax. 基本 Rust 语法。
- Basic concepts of DDD. DDD 的基本概念。
- How to use specific crates.
如何使用特定的板条箱。
Dependency Direction 依赖方向
The architecture we are creating has the dependency relationship as shown above. Keeping this dependency relationship in mind while reading the article will enhance your understanding. It’s important to note that the infrastructure layer does not depend on the application layer.
我们正在创建的架构具有如上所示的依赖关系。阅读本文时牢记这种依赖关系将增强您的理解。需要注意的是,基础设施层不依赖于应用程序层。
Implementation Begins 实施开始
Specifying Requirements 明确要求
This time, we decided to create a system for universities to manage clubs.
这次,我们决定创建一个大学管理俱乐部的系统。
- Ability to add members. 能够添加成员。
- Fourth-year students cannot be added.
无法添加四年级学生。 - Ability to remove members.
能够删除成员。 - Owners cannot be removed.
无法删除所有者。 - Fourth-year students graduate.
四年级学生毕业。 - Clubs require a minimum of 3 members to operate.
俱乐部需要至少 3 名会员才能运营。 - Clubs have a maximum capacity.
俱乐部有最大容纳人数。 - Clubs require a representative.
俱乐部需要一名代表。 - Individuals aged 20 and above can join social gatherings.
20岁及以上的个人可以参加社交聚会。 - Only third-year students can become club representatives.
只有三年级学生才能成为俱乐部代表。
Domain Layer 领域层
Let’s start by creating the domain layer. The Circle aggregate consists of two entities: Circle
, which serves as the aggregate root, and Member
, representing members within the aggregate.
让我们从创建域层开始。 Circle 聚合由两个实体组成: Circle
,用作聚合根,以及 Member
,代表聚合内的成员。
pub struct Circle {
pub id: CircleId, // Circle ID (Value Object)
pub name: String,
pub capacity: usize,
pub owner: Member,
pub members: Vec<Member>,
}
pub struct Member {
pub id: MemberId, // Member ID (Value Object)
pub name: String,
pub age: usize,
pub grade: Grade,
pub major: Major,
}
We’re using Value Objects
for IDs. I won't explain the difference between Entity
and Value Object
this time, so if you're interested, please look it up.
我们使用 Value Objects
作为 ID。这次我就不解释 Entity
和 Value Object
的区别了,有兴趣的话可以查一下。
use std::fmt;
use std::hash::{Hash, Hasher};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CircleId(usize);
impl CircleId {
pub fn gen() -> Self {
Self(rand::random::<usize>())
}
}
impl std::convert::From<usize> for CircleId {
fn from(id: usize) -> Self {
Self(id)
}
}
impl Hash for CircleId {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl fmt::Display for CircleId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl std::convert::From<CircleId> for usize {
fn from(circle_id: CircleId) -> usize {
circle_id.0
}
}
Here’s the Rust code implementing methods for the domain layer, focusing on the Circle
aggregate and the Member
entity.
下面是领域层实现方法的 Rust 代码,重点关注 Circle
聚合和 Member
实体。
use crate::domain::aggregate::member::Member;
use crate::domain::aggregate::value_object::circle_id::CircleId;
use super::value_object::grade::Grade;
use anyhow::Error;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Circle {
pub id: CircleId, // Circle ID (Value Object)
pub name: String,
pub capacity: usize,
pub owner: Member,
pub members: Vec<Member>,
}
impl Circle {
// Method for creating a new circle
pub fn new(name: String, owner: Member, capacity: usize) -> Result<Self, Error> {
// Only 3rd graders can be owners
if owner.grade != Grade::Third {
return Err(Error::msg("Owner must be 3rd grade"));
}
// Circle must have at least 3 members
if capacity < 3 {
return Err(Error::msg("Circle capacity must be 3 or more"));
}
Ok(Circle {
id: CircleId::gen(),
name,
owner,
capacity,
members: vec![],
})
}
// Method for reconstructing a circle
pub fn reconstruct(
id: CircleId,
name: String,
owner: Member,
capacity: usize,
members: Vec<Member>,
) -> Self {
Circle {
id,
name,
owner,
capacity,
members,
}
}
// Method for updating a circle
pub fn update(&mut self, name: Option<String>, capacity: Option<usize>) {
if let Some(name) = name {
self.name = name;
}
if let Some(capacity) = capacity {
self.capacity = capacity;
};
}
// Method to check if the circle is full
fn is_full(&self) -> bool {
self.members.len() + 1 >= self.capacity
}
// Method to check if the circle can operate
fn is_runnable(&self) -> bool {
self.members.len() + 1 >= 3
}
// Method to check if a member can join a social gathering
fn is_drinkable_alcohol(member: &Member) -> bool {
member.is_adult()
}
// Method to add a member to the circle
pub fn add_member(&mut self, member: Member) -> Result<(), Error> {
// Can't add member if circle is full
if self.is_full() {
return Err(Error::msg("Circle member is full"));
}
// 4th graders can't join circles
if member.grade == Grade::Fourth {
return Err(Error::msg("4th grade can't join circle"));
}
self.members.push(member);
Ok(())
}
// Method to remove a member from the circle
pub fn remove_member(&mut self, member: &Member) -> Result<(), Error> {
// Owner can't be removed
if self.owner.id == member.id {
return Err(Error::msg("Owner can't be removed"));
}
self.members.retain(|m| m.id != member.id);
Ok(())
}
// Method to graduate 4th graders
pub fn graduate(&mut self) {
self.members.retain(|m| m.grade != Grade::Fourth);
}
}
impl Member {
// Method for creating a new member
pub fn new(name: String, age: usize, grade: Grade, major: Major) -> Self {
Member {
id: MemberId::gen(),
name,
age,
grade,
major,
}
}
// Method for reconstructing a member
pub fn reconstruct(id: MemberId, name: String, age: usize, grade: Grade, major: Major) -> Self {
Member {
id,
name,
age,
grade,
major,
}
}
// Method to check if the member is 20 or older
pub fn is_adult(&self) -> bool {
self.age >= 20
}
}
This code includes methods for creating, reconstructing, updating, and managing members and circles in the domain layer.
该代码包括用于在领域层中创建、重建、更新和管理成员和圈子的方法。
Interfaces 接口
We create interfaces to expose the domain’s behavior externally. We’ll create an interface for manipulating the Circle aggregate.
我们创建接口来向外部公开域的行为。我们将创建一个用于操作 Circle 聚合的接口。
pub trait CircleRepositoryInterface {
fn find_circle_by_id(&self, circle_id: &CircleId) -> Result<Circle, Error>;
fn create(&self, circle: &Circle) -> Result<(), Error>;
fn update(&self, circle: &Circle) -> Result<Circle, Error>;
fn delete(&self, circle: &Circle) -> Result<(), Error>;
}
trait
in Rust is similar to an interface
in other languages.
Rust 中的 trait
与其他语言中的 interface
类似。
Infrastructure Layer 基础设施层
The infrastructure layer handles persistence. It doesn’t matter where the data is stored; it could be Firestore, Postgres, etc. For this example, we’ll store data in memory.
基础设施层处理持久性。数据存储在哪里并不重要;它可以是 Firestore、Postgres 等。在本例中,我们将数据存储在内存中。
In the infrastructure layer, we implement the interfaces from the domain layer.
在基础设施层,我们实现领域层的接口。
use anyhow::Error;
use crate::domain::{
aggregate::{
circle::Circle,
member::Member,
value_object::{circle_id::CircleId, grade::Grade, major::Major, member_id::MemberId},
},
interface::circle_repository_interface::CircleRepositoryInterface,
};
use super::db::Db;
#[derive(Clone, Debug)]
pub struct CircleRepository {
db: Db,
}
impl CircleRepository {
pub fn new() -> Self {
Self { db: Db::new() }
}
}
impl CircleRepositoryInterface for CircleRepository {
fn find_circle_by_id(&self, circle_id: &CircleId) -> Result<Circle, Error> {
match self.db.get::<CircleData, _>(&circle_id.to_string())? {
Some(data) => Ok(Circle::try_from(data)?),
None => Err(Error::msg("Circle not found")),
}
}
fn create(&self, circle: &Circle) -> Result<(), Error> {
match self.db.get::<CircleData, _>(&circle.id.to_string())? {
Some(_) => Err(Error::msg("Circle already exists")),
None => {
self.db
.set(circle.id.to_string(), &CircleData::from(circle.clone()))?;
Ok(())
}
}
}
fn update(&self, circle: &Circle) -> Result<Circle, Error> {
match self.db.get::<CircleData, _>(&circle.id.to_string())? {
Some(_) => self
.db
.set(circle.id.to_string(), &CircleData::from(circle.clone()))
.and_then(|_| self.db.get::<CircleData, _>(&circle.id.to_string()))
.map(|data| match data {
Some(data) => Circle::try_from(data),
None => Err(Error::msg("Failed to convert circle data")),
})?,
None => Err(Error::msg("Circle not found")),
}
}
fn delete(&self, circle: &Circle) -> Result<(), Error> {
match self.db.get::<CircleData, _>(&circle.id.to_string())? {
Some(_) => self.db.remove(circle.id.to_string()),
None => Err(Error::msg("Circle not found")),
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
struct CircleData {
id: usize,
name: String,
owner: MemberData,
capacity: usize,
members: Vec<MemberData>,
}
impl std::convert::From<Circle> for CircleData {
fn from(circle: Circle) -> Self {
CircleData {
id: circle.id.into(),
name: circle.name,
owner: MemberData::from(circle.owner),
capacity: circle.capacity,
members: circle.members.into_iter().map(MemberData::from).collect(),
}
}
}
impl std::convert::TryFrom<CircleData> for Circle {
type Error = Error;
fn try_from(data: CircleData) -> Result<Self, Self::Error> {
Ok(Circle::reconstruct(
CircleId::from(data.id),
data.name,
Member::reconstruct(
MemberId::from(data.owner.id),
data.owner.name,
data.owner.age,
Grade::try_from(data.owner.grade)?,
Major::from(data.owner.major.as_str()),
),
data.capacity,
data.members
.into_iter()
.map(Member::try_from)
.collect::<Result<Vec<Member>, Error>>()?,
))
}
}
#[derive(serde::Deserialize, serde::Serialize)]
struct MemberData {
id: usize,
name: String,
age: usize,
grade: usize,
major: String,
}
impl std::convert::From<Member> for MemberData {
fn from(value: Member) -> Self {
Self {
id: value.id.into(),
name: value.name,
age: value.age,
grade: value.grade.into(),
major: value.major.into(),
}
}
}
impl std::convert::TryFrom<MemberData> for Member {
type Error = Error;
fn try_from(value: MemberData) -> Result<Self, Self::Error> {
Ok(Member::reconstruct(
MemberId::from(value.id),
value.name,
value.age,
Grade::try_from(value.grade)?,
Major::from(value.major.as_str()),
))
}
}
We use XxxData
to represent values retrieved from the database. In this case, it's CircleData
and MemberData
. We implement the TryFrom
trait to convert these database types into domain layer types using the reconstruct
method, allowing us to decouple the application layer from the infrastructure layer.
我们使用 XxxData
来表示从数据库检索的值。在本例中,它是 CircleData
和 MemberData
。我们实现 TryFrom
特征,使用 reconstruct
方法将这些数据库类型转换为域层类型,从而使我们能够将应用程序层与基础设施层解耦。
Exactly, abstracting away the database implementation details is crucial in Domain-Driven Design (DDD). This abstraction ensures that changes in the underlying database technology (like switching from a local in-memory database to Firebase or PostgreSQL) don’t affect the application layer. By depending on abstractions rather than concrete implementations, the application layer remains decoupled from the infrastructure layer.
确切地说,抽象出数据库实现细节在领域驱动设计(DDD)中至关重要。这种抽象确保底层数据库技术的更改(例如从本地内存数据库切换到 Firebase 或 PostgreSQL)不会影响应用程序层。通过依赖抽象而不是具体实现,应用程序层保持与基础设施层的解耦。
While the implementation of the database isn’t directly related to DDD concepts, it’s essential for ensuring that the domain logic can operate independently of specific database choices. If you’re interested, feel free to delve deeper into the database implementation details.
虽然数据库的实现与 DDD 概念没有直接关系,但它对于确保域逻辑可以独立于特定数据库选择进行操作至关重要。如果您有兴趣,请随意深入研究数据库实现细节。
use std::{
collections::HashMap,
sync::{Arc, RwLock},
};
#[derive(Clone, Debug)]
pub struct Db {
db: Arc<RwLock<HashMap<String, String>>>,
}
impl Db {
pub fn new() -> Self {
Self {
db: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn get<D, K>(&self, key: K) -> anyhow::Result<Option<D>>
where
K: AsRef<str>,
D: serde::de::DeserializeOwned,
{
let db = self
.db
.read()
.map_err(|e| anyhow::anyhow!("Error reading from database: {:?}", e))?;
match db.get(key.as_ref()) {
Some(value) => {
let deserialized_value = serde_json::from_str(value)
.map_err(|e| anyhow::anyhow!("Error deserializing value: {:?}", e))?;
Ok(Some(deserialized_value))
}
None => Ok(None),
}
}
pub fn keys(&self) -> Vec<String> {
let db = self.db.read().expect("read data from db");
db.keys().cloned().collect()
}
pub fn remove<K>(&self, key: K) -> anyhow::Result<()>
where
K: AsRef<str>,
{
let mut db = self
.db
.write()
.map_err(|e| anyhow::anyhow!("Error writing to database: {:?}", e))?;
db.remove(key.as_ref())
.ok_or_else(|| anyhow::anyhow!("Key not found in database"))?;
Ok(())
}
pub fn set<S, K>(&self, key: K, value: &S) -> anyhow::Result<()>
where
K: Into<String>,
S: serde::ser::Serialize,
{
let value = serde_json::to_string(value)?;
let mut db = self
.db
.write()
.map_err(|e| anyhow::anyhow!("Error writing to database: {:?}", e))?;
db.insert(key.into(), value);
Ok(())
}
}
Application Layer 应用层
In the application layer, we use entities and value objects to achieve use cases and delegate processing to repositories (infrastructure layer). We request processing from repositories without directly depending on the infrastructure layer. This is known as the Dependency Inversion Principle, where we rely on abstractions rather than concrete implementations to realize use cases. In this context, we’ll implement the use case of creating a circle.
在应用程序层,我们使用实体和值对象来实现用例并将处理委托给存储库(基础设施层)。我们请求存储库进行处理,而不直接依赖于基础设施层。这称为依赖倒置原则,我们依靠抽象而不是具体实现来实现用例。在这种情况下,我们将实现创建圆圈的用例。
use anyhow::Result;
use serde::Deserialize;
use crate::domain::{
aggregate::{
circle::Circle,
member::Member,
value_object::{grade::Grade, major::Major},
},
interface::circle_repository_interface::CircleRepositoryInterface,
};
#[derive(Debug, Deserialize)]
pub struct CreateCircleInput {
pub circle_name: String,
pub capacity: usize,
pub owner_name: String,
pub owner_age: usize,
pub owner_grade: usize,
pub owner_major: String,
}
impl CreateCircleInput {
pub fn new(
circle_name: String,
capacity: usize,
owner_name: String,
owner_age: usize,
owner_grade: usize,
owner_major: String,
) -> Self {
CreateCircleInput {
circle_name,
capacity,
owner_name,
owner_age,
owner_grade,
owner_major,
}
}
}
#[derive(Debug, Deserialize)]
pub struct CreateCircleOutput {
pub circle_id: usize,
pub owner_id: usize,
}
pub struct CreateCircleUsecase<T>
where
T: CircleRepositoryInterface,
{
circle_repository: T,
}
impl<T> CreateCircleUsecase<T>
where
T: CircleRepositoryInterface,
{
pub fn new(circle_repository: T) -> Self {
CreateCircleUsecase { circle_repository }
}
pub fn execute(
&mut self,
circle_circle_input: CreateCircleInput,
) -> Result<CreateCircleOutput> {
let grade = Grade::try_from(circle_circle_input.owner_grade)?;
let major = Major::from(circle_circle_input.owner_major.as_str());
let owner = Member::new(
circle_circle_input.owner_name,
circle_circle_input.owner_age,
grade,
major,
);
let owner_id = owner.id;
let circle = Circle::new(
circle_circle_input.circle_name,
owner,
circle_circle_input.capacity,
)?;
self.circle_repository
.create(&circle)
.map(|_| CreateCircleOutput {
circle_id: usize::from(circle.id),
owner_id: usize::from(owner_id),
})
}
}
Use Case I/O (Input/Output): We define the I/O of the use case as CreateCircleInput
and CreateCircleOutput
.
用例 I/O(输入/输出):我们将用例的 I/O 定义为 CreateCircleInput
和 CreateCircleOutput
。
CreateCircleUsecase Struct: CreateCircleUsecase
is a generic struct that receives something implementing the CircleRepositoryInterface
trait as its field.
CreateCircleUsecase 结构: CreateCircleUsecase
是一个通用结构,它接收实现 CircleRepositoryInterface
特征的内容作为其字段。
Impl CreateCircleUsecase: We implement two methods, new
and execute
, for the CreateCircleUsecase
struct.
Impl CreateCircleUsecase:我们为 CreateCircleUsecase
结构实现两个方法, new
和 execute
。
- The
new
Method: This method generates instances and receives something implementing theCircleRepositoryInterface
trait, akin to a constructor in other languages. Here, we inject dependencies using an abstraction (trait) instead of a concrete implementation, adhering to the Dependency Inversion Principle.new
方法:此方法生成实例并接收实现CircleRepositoryInterface
特征的内容,类似于其他语言中的构造函数。在这里,我们使用抽象(特征)而不是具体实现来注入依赖项,遵循依赖倒置原则。 - The
execute
Method: This method executes the use case. It takesCreateCircleInput
, creates a Circle Entity, and saves it to the repository.self
refers to the instance ofCreateCircleUsecase
, allowing us to access the field withself.circle_repository
as it implements theCircleRepositoryInterface
trait, enabling thecreate
method call.execute
方法:该方法执行用例。它采用CreateCircleInput
,创建一个 Circle 实体,并将其保存到存储库中。self
引用CreateCircleUsecase
的实例,允许我们使用self.circle_repository
访问该字段,因为它实现了CircleRepositoryInterface
特征,从而启用 < b6>方法调用。
This setup ensures that the use case depends solely on the domain and avoids dependencies on the infrastructure layer.
此设置可确保用例仅依赖于域,并避免依赖于基础设施层。
Presentation Layer 表示层
In the Presentation Layer, we define endpoints, handle request reception, manage responses, and map values to pass to the Application Layer.
在表示层中,我们定义端点、处理请求接收、管理响应以及映射值以传递到应用程序层。
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct CreateCircleRequestBody {
pub circle_name: String,
pub capacity: usize,
pub owner_name: String,
pub owner_age: usize,
pub owner_grade: usize,
pub owner_major: String,
}
impl std::convert::From<CreateCircleRequestBody> for CreateCircleInput {
fn from(
CreateCircleRequestBody {
circle_name,
capacity,
owner_name,
owner_age,
owner_grade,
owner_major,
}: CreateCircleRequestBody,
) -> Self {
CreateCircleInput::new(
circle_name,
capacity,
owner_name,
owner_age,
owner_grade,
owner_major,
)
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct CreateCircleResponseBody {
pub circle_id: usize,
pub owner_id: usize,
}
impl std::convert::From<CreateCircleOutput> for CreateCircleResponseBody {
fn from(
CreateCircleOutput {
circle_id,
owner_id,
}: CreateCircleOutput,
) -> Self {
CreateCircleResponseBody {
circle_id,
owner_id,
}
}
}
pub async fn handle_create_circle(
State(state): State<AppState>,
Json(body): Json<CreateCircleRequestBody>,
) -> Result<Json<CreateCircleResponseBody>, String> {
let circle_circle_input = CreateCircleInput::from(body);
let mut usecase = CreateCircleUsecase::new(state.circle_repository);
usecase
.execute(circle_circle_input)
.map(CreateCircleResponseBody::from)
.map(Json)
.map_err(|e| e.to_string())
}
#[derive(Clone)]
struct AppState {
circle_repository: CircleRepository,
}
fn router() -> Router<AppState> {
Router::new()
.route("/circle", post(handle_create_circle))
}
#[tokio::main]
async fn main() -> Result<(), ()> {
let state = AppState {
circle_repository: CircleRepository::new(),
};
let app = router().with_state(state);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000")
.await
.unwrap();
println!("Listening on: {}", listener.local_addr().unwrap());
axum::serve(listener, app).await.unwrap();
Ok(())
}
Input/Output (I/O): CreateCircleRequestBody
is a struct used to receive the request body. We implement From
for CreateCircleRequestBody
to construct CreateCircleInput
. The response is implemented similarly.
输入/输出(I/O): CreateCircleRequestBody
是用于接收请求正文的结构体。我们为 CreateCircleRequestBody
实现 From
来构造 CreateCircleInput
。响应的实现类似。
AppState: AppState
represents the application's state, provided by axum. It holds the repository and acts as a Dependency Injection (DI) container for resolving dependencies. While there's only one dependency in this case, it provides flexibility such as swapping CircleRepository
with a mock like CircleRepositoryMock
for testing purposes.
AppState: AppState
代表应用程序的状态,由axum提供。它保存存储库并充当用于解决依赖关系的依赖注入 (DI) 容器。虽然在这种情况下只有一个依赖项,但它提供了灵活性,例如将 CircleRepository
与 CircleRepositoryMock
等模拟交换以进行测试。
handle_create_circle
: This function receives the request and executes CreateCircleUsecase
. The State(state)
in the first argument retrieves the value from AppState
, which is then injected into CreateCircleUsecase
to call the execute
method, as mentioned earlier. I/O is converted to desired formats accordingly.handle_create_circle
:该函数接收请求并执行 CreateCircleUsecase
。第一个参数中的 State(state)
从 AppState
检索值,然后将其注入到 CreateCircleUsecase
中以调用 execute
方法,如上所述早些时候。 I/O 相应地转换为所需的格式。
I tried implementing an API server using Rust, Axum, and Domain-Driven Design (DDD). While Rust adoption in business is still relatively low, I believe it will gain traction, especially in backend development. I encourage you to give it a try. Feel free to contribute by adding new use cases or aggregates to further aid learning.
我尝试使用 Rust、Axum 和领域驱动设计 (DDD) 来实现 API 服务器。虽然 Rust 在商业中的采用率仍然相对较低,但我相信它将获得牵引力,尤其是在后端开发方面。我鼓励你尝试一下。请随意添加新的用例或聚合来进一步帮助学习。
您可以在此处找到该项目的源代码。
learning-rust/src/hello.rs at io-uring · amacal/learning-rust · GitHub