原文链接:https://alexis-lozano.com/hexagonal-architecture-in-rust-5/
翻译:trdthg
选题:trdthg
2021-09-12 - Rust 六边形架构 #5 - 其他用例
上次,我们做了一些重构。不知何故,我们的客户对我们很生气……我的意思是,他应该高兴,代码现在比以前更干净了。
在吃完一块蛋糕之后 (显然,这是我们应得的),让我们继续实现剩下的用例。这篇文章可能会有点长,但是,嘿,如果你不想阅读整个过程,代码在 github 上 : ) 我们将实现的用例是:
- 获取所有宝可梦
- 查询一只宝可梦
- 删除一只宝可梦
获取所有宝可梦
像往常一样,我们将从测试开始。让我们首先创建一个新的用例 文件:domain/fetch_all_pokemons.rs:
// domain/mod.rs
pub mod fetch_all_pokemons;
让我们考虑一下这个用例有哪些可能的结果?
- 成功了,我们得到了宝可梦
- 存储库中发生了未知错误,我们无法得到我们的宝可梦
让我们先从错误案例开始:
#[cfg(test)]
mod tests {
use super::*;
use crate::repositories::pokemon::InMemoryRepository;
#[test]
fn it_should_return_an_unknown_error_when_an_unexpected_error_happens() {
let repo = Arc::new(InMemoryRepository::new().with_error());
let res = execute(repo);
match res {
Err(Error::Unknown) => {}
_ => unreachable!(),
};
}
}
如您所见,我仍然没有写一行代码,只写了测试。当然,现在还不能通过编译。这里有两件事很有趣:
- 这里的测试和创建宝可梦那里几乎一样
- 这里不需要 repo 之外的其他参数
接下来让我们添加一些必要的代码让测试通过:
use crate::repositories::pokemon::Repository;
use std::sync::Arc;
pub enum Error {
Unknown,
}
pub fn execute(repo: Arc<dyn Repository>) -> Result<(), Error> {
Err(Error::Unknown)
}
接着继续添加下一个测试:
#[test]
fn it_should_return_all_the_pokemons_ordered_by_increasing_number_otherwise() {
let repo = Arc::new(InMemoryRepository::new());
repo.insert(
PokemonNumber::pikachu(),
PokemonName::pikachu(),
PokemonTypes::pikachu(),
)
.ok();
repo.insert(
PokemonNumber::charmander(),
PokemonName::charmander(),
PokemonTypes::charmander(),
)
.ok();
let res = execute(repo);
match res {
Ok(res) => {
assert_eq!(res[0].number, u16::from(PokemonNumber::charmander()));
assert_eq!(res[0].name, String::from(PokemonName::charmander()));
assert_eq!(res[0].types, Vec::<String>::from(PokemonTypes::charmander()));
assert_eq!(res[1].number, u16::from(PokemonNumber::pikachu()));
assert_eq!(res[1].name, String::from(PokemonName::pikachu()));
assert_eq!(res[1].types, Vec::<String>::from(PokemonTypes::pikachu()));
}
_ => unreachable!(),
};
}
在测试里,我按照 number
递减的顺序插入了宝可梦,所以存储库中宝可梦的排序应该是确定的。当存储库没有输出错误时,我们应该能从用例中得到一个
Vec<Response>
。现在我们要为用例加入 Response
,并编写一部分用例函数的内容:
pub struct Response {
pub number: u16,
pub name: String,
pub types: Vec<String>,
}
pub fn execute(repo: Arc<dyn Repository>) -> Result<Vec<Response>, Error> {
match repo.fetch_all() {
Ok(pokemons) => Ok(pokemons
.into_iter()
.map(|p| Response {
number: u16::from(p.number),
name: String::from(p.name),
types: Vec::<String>::from(p.types),
})
.collect::<Vec<Response>>()),
Err(FetchAllError::Unknown) => Err(Error::Unknown),
}
}
然后再为存储库添加并实现 fetch_all
方法:
pub enum FetchAllError {
Unknown,
}
pub trait Repository: Send + Sync {
fn fetch_all(&self) -> Result<Vec<Pokemon>, FetchAllError>;
}
impl Repository for InMemoryRepository {
fn fetch_all(&self) -> Result<Vec<Pokemon>, FetchAllError> {
if self.error {
return Err(FetchAllError::Unknown);
}
let lock = match self.pokemons.lock() {
Ok(lock) => lock,
_ => return Err(FetchAllError::Unknown),
};
let mut pokemons = lock.to_vec();
pokemons.sort_by(|a, b| a.number.cmp(&b.number));
Ok(pokemons)
}
}
为了实现宝可梦按照 number
排序,PokemonNumber
需要实现 PartialEq
、 Eq
、PartialOrd
、Ord
这 4 个 Trait
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct PokemonNumber(u16);
运行 cargo test
:
cargo test
running 6 tests
...
it_should_return_an_unknown_error_when_an_unexpected_error_happens ... ok
it_should_return_all_the_pokemons_ordered_by_increasing_number_otherwise ... ok
我们已经完成了这个用例 :) 现在让我们快速实现 api。在 api/mod.rs 中,添加以下内容:
mod fetch_all_pokemons;
// in the router macro
(GET) (/) => {
fetch_all_pokemons::serve(repo.clone())
}
接着实现具体的 handler api/fetch_all_pokemons.rs:
use crate::api::Status;
use crate::domain::fetch_all_pokemons;
use crate::repositories::pokemon::Repository;
use rouille;
use serde::Serialize;
use std::sync::Arc;
#[derive(Serialize)]
struct Response {
number: u16,
name: String,
types: Vec<String>,
}
pub fn serve(repo: Arc<dyn Repository>) -> rouille::Response {
match fetch_all_pokemons::execute(repo) {
Ok(res) => rouille::Response::json(
&res.into_iter()
.map(|p| Response {
number: p.number,
name: p.name,
types: p.types,
})
.collect::<Vec<Response>>(),
),
Err(fetch_all_pokemons::Error::Unknown) => {
rouille::Response::from(Status::InternalServerError)
}
}
}
现在您可以运行 cargo run
,并使用你最喜欢的 HTTP 客户端 (curl、postman、...)。通过在 /
上发送 POST
请求来创建一些宝可梦。然后您可以使用浏览器访问 http://localhost:8000。这是我得到的:
[
{
"number": 4,
"name": "Charmander",
"types": [
"Fire"
]
},
{
"number": 25,
"name": "Pikachu",
"types": [
"Electric"
]
}
]
查询一只宝可梦
第二个用例!现在我们想通过给系统一个宝可梦的编号来获取一个宝可梦。让我们创建一个新的用例文件 domain/fetch_pokemon.rs:
// domain/mod.rs
pub mod fetch_pokemon;
让我们思考一下用例运行时可能遇到什么情况。
- 存储库可能会引发未知错误。
- 请求参数可能是不合法的。
- 用户给定的编号可能对应没有宝可梦。
- 获得宝可梦成功了
让我们从未知错误的测试开始。新建 domain/fetch_pokemon.rs 并添加以下内容:
use crate::domain::entities::PokemonNumber;
use std::sync::Arc;
#[cfg(test)]
mod tests {
use super::*;
use crate::repositories::pokemon::InMemoryRepository;
#[test]
fn it_should_return_an_unknown_error_when_an_unexpected_error_happens() {
let repo = Arc::new(InMemoryRepository::new().with_error());
let req = Request::new(PokemonNumber::pikachu());
let res = execute(repo, req);
match res {
Err(Error::Unknown) => {}
_ => unreachable!(),
};
}
}
接着,为测试实现必要的类型和函数:
pub struct Request {
pub number: u16,
}
pub enum Error {
Unknown
}
为了让测试更清晰,我们还为 Request
实现了一个 new
方法,当然只在测试时使用:
#[cfg(test)]
mod tests {
...
impl Request {
fn new(number: PokemonNumber) -> Self {
Self {
number: u16::from(number),
}
}
}
}
最后,再实现一个只用满足这项测试的 execute
函数即可:
use crate::repositories::pokemon::Repository;
pub fn execute(repo: Arc<dyn Repository>, req: Request) -> Result<(), Error> {
Err(Error::Unknown)
}
让我们运行 cargo test fetch_pokemon
:
test it_should_return_an_unknown_error_when_an_unexpected_error_happens ... ok
好的。我们可以开始下一个测试了。让我们来看一下请求格式错误的情况:
#[test]
fn it_should_return_a_bad_request_error_when_request_is_invalid() {
let repo = Arc::new(InMemoryRepository::new());
let req = Request::new(PokemonNumber::bad());
let res = execute(repo, req);
match res {
Err(Error::BadRequest) => {}
_ => unreachable!(),
};
}
首先,让我们在 PokemonNumber
中创建 bad
函数。在 domain/entities.rs 中添加以下内容:
#[cfg(test)]
impl PokemonNumber {
...
pub fn bad() -> Self {
Self(0)
}
}
接着,在 Error
中添加 Badrequest
类型与之对应:
pub enum Error {
BadRequest,
...
}
现在已经能够通过编译,但是测试尚且不能通过,实际上,我们的 execute
函数暂时只能返回 Unknown
类型:
use std::convert::TryFrom;
pub fn execute(repo: Arc<dyn Repository>, req: Request) -> Result<(), Error> {
match PokemonNumber::try_from(req.number) {
Ok(number) => Err(Error::Unknown),
_ => Err(Error::BadRequest),
}
}
现在两个测试都通过了!我们现在可以进行第三个测试:在存储库中找不到宝可梦时,应该返回 NotFound
:
#[test]
fn it_should_return_a_not_found_error_when_the_repo_does_not_contain_the_pokemon() {
let repo = Arc::new(InMemoryRepository::new());
let req = Request::new(PokemonNumber::pikachu());
let res = execute(repo, req);
match res {
Err(Error::NotFound) => {}
_ => unreachable!(),
};
}
同样,在 Error
中补充 NotFound
错误类型:
pub enum Error {
...
NotFound,
...
}
像之前一样,测试没有通过。我们需要在 execute
函数中处理对应的错误类型:
use crate::repositories::pokemon::{FetchOneError, ...};
Ok(number) => match repo.fetch_one(number) {
Ok(_) => Ok(()),
Err(FetchOneError::NotFound) => Err(Error::NotFound),
Err(FetchOneError::Unknown) => Err(Error::Unknown),
}
接着在 repositories/pokemon.rs 中补充对应的方法和错误类型:
pub enum FetchOneError {
NotFound,
Unknown,
}
pub trait Repository: Send + Sync {
...
fn fetch_one(&self, number: PokemonNumber) -> Result<(), FetchOneError>;
}
impl Repository for InMemoryRepository {
...
fn fetch_one(&self, number: PokemonNumber) -> Result<(), FetchOneError> {
if self.error {
return Err(FetchOneError::Unknown);
}
Err(FetchOneError::NotFound)
}
}
Et voilà,测试通过了!最后去处理获取成功的测试吧:
#[cfg(test)]
mod tests {
use crate::domain::entities::{PokemonName, PokemonTypes};
#[test]
fn it_should_return_the_pokemon_otherwise() {
let repo = Arc::new(InMemoryRepository::new());
repo.insert(
PokemonNumber::pikachu(),
PokemonName::pikachu(),
PokemonTypes::pikachu(),
)
.ok();
let req = Request::new(PokemonNumber::pikachu());
let res = execute(repo, req);
match res {
Ok(res) => {
assert_eq!(res.number, u16::from(PokemonNumber::pikachu()));
assert_eq!(res.name, String::from(PokemonName::pikachu()));
assert_eq!(res.types, Vec::<String>::from(PokemonTypes::pikachu()));
}
_ => unreachable!(),
};
}
}
首先创建 Response
类型,并改变 execute
的返回值类型:
pub struct Response {
pub number: u16,
pub name: String,
pub types: Vec<String>,
}
pub fn execute(repo: Arc<dyn Repository>, req: Request) -> Result<Response, Error> {
现在,我们去处理 Ok(_)
的部分:
use crate::domain::entities::{Pokemon, ...};
Ok(Pokemon {
number,
name,
types,
}) => Ok(Response {
number: u16::from(number),
name: String::from(name),
types: Vec::<String>::from(types),
})
最后修改存储库的 fetch_one
函数:
pub trait Repository: Send + Sync {
fn fetch_one(&self, number: PokemonNumber) -> Result<Pokemon, FetchOneError>;
}
impl Repository for InMemoryRepository {
fn fetch_one(&self, number: PokemonNumber) -> Result<Pokemon, FetchOneError> {
if self.error {
return Err(FetchOneError::Unknown);
}
let lock = match self.pokemons.lock() {
Ok(lock) => lock,
_ => return Err(FetchOneError::Unknown),
};
match lock.iter().find(|p| p.number == number) {
Some(pokemon) => Ok(pokemon.clone()),
None => Err(FetchOneError::NotFound),
}
}
}
所有的测试都通过了现在:
test it_should_return_a_not_found_error_when_the_repo_does_not_contain_the_pokemon ... ok
test it_should_return_the_pokemon_otherwise ... ok
test it_should_return_a_bad_request_error_when_request_is_invalid ... ok
test it_should_return_an_unknown_error_when_an_unexpected_error_happens ... ok
让我们现在向 API 中前加一个新路由:
mod fetch_pokemon;
// in the router macro
(GET) (/{number: u16}) => {
fetch_pokemon::serve(repo.clone(), number)
}
接着再 api/fetch_pokemon.rs 创建对应的 serve
函数:
use crate::api::Status;
use crate::domain::fetch_pokemon;
use crate::repositories::pokemon::Repository;
use rouille;
use serde::Serialize;
use std::sync::Arc;
#[derive(Serialize)]
struct Response {
number: u16,
name: String,
types: Vec<String>,
}
pub fn serve(repo: Arc<dyn Repository>, number: u16) -> rouille::Response {
let req = fetch_pokemon::Request { number };
match fetch_pokemon::execute(repo, req) {
Ok(fetch_pokemon::Response {
number,
name,
types,
}) => rouille::Response::json(&Response {
number,
name,
types,
}),
Err(fetch_pokemon::Error::BadRequest) => rouille::Response::from(Status::BadRequest),
Err(fetch_pokemon::Error::NotFound) => rouille::Response::from(Status::NotFound),
Err(fetch_pokemon::Error::Unknown) => rouille::Response::from(Status::InternalServerError),
}
}
现在您可以运行应用程序,打开您的 HTTP 客户端并将一些宝可梦添加到存储库中。然后,您应该能够通过在网址末尾添加宝可梦的数量来一一获取它们。 例如,在我的计算机上,http://localhost:8000/25 上的 GET 请求返回了:
{
"number": 25,
"name": "Pikachu",
"types": [
"Electric"
]
}
删除一只宝可梦
我保证,我们很快就会完成。删除宝可梦是我们的最后一个用例。这个用例的结果有四种可能:
- 成功
- 请求格式错误
- 没有找到对应的宝可梦
- 未知错误
您应该已经注意到,它们与获取 Pokemon 用例中的完全相同。我会解释更快一些。现在就开始了。让我们直接编写所有测试:
// domain/mod.rs
pub mod delete_pokemon;
// domain/delete_pokemon.rs
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::entities::{PokemonName, PokemonTypes};
use crate::repositories::pokemon::InMemoryRepository;
#[test]
fn it_should_return_an_unknown_error_when_an_unexpected_error_happens() {
let repo = Arc::new(InMemoryRepository::new().with_error());
let req = Request::new(PokemonNumber::pikachu());
let res = execute(repo, req);
match res {
Err(Error::Unknown) => {}
_ => unreachable!(),
};
}
#[test]
fn it_should_return_a_bad_request_error_when_request_is_invalid() {
let repo = Arc::new(InMemoryRepository::new());
let req = Request::new(PokemonNumber::bad());
let res = execute(repo, req);
match res {
Err(Error::BadRequest) => {}
_ => unreachable!(),
};
}
#[test]
fn it_should_return_a_not_found_error_when_the_repo_does_not_contain_the_pokemon() {
let repo = Arc::new(InMemoryRepository::new());
let req = Request::new(PokemonNumber::pikachu());
let res = execute(repo, req);
match res {
Err(Error::NotFound) => {}
_ => unreachable!(),
};
}
#[test]
fn it_should_return_ok_otherwise() {
let repo = Arc::new(InMemoryRepository::new());
repo.insert(
PokemonNumber::pikachu(),
PokemonName::pikachu(),
PokemonTypes::pikachu(),
)
.ok();
let req = Request::new(PokemonNumber::pikachu());
let res = execute(repo, req);
match res {
Ok(()) => {},
_ => unreachable!(),
};
}
impl Request {
fn new(number: PokemonNumber) -> Self {
Self {
number: u16::from(number),
}
}
}
}
除了成功情况下的测试,其他的测试基本上是对 domain/fetch_pokemon.rs 的复制粘贴。接下来是类型:
pub struct Request {
pub number: u16,
}
pub enum Error {
BadRequest,
NotFound,
Unknown,
}
我们不需要定义 Response
类型,因为在 Ok
情况下我们不会返回任何内容。让我们定义 execute
函数:
use crate::domain::entities::PokemonNumber;
use crate::repositories::pokemon::{DeleteError, Repository};
use std::convert::TryFrom;
use std::sync::Arc;
pub fn execute(repo: Arc<dyn Repository>, req: Request) -> Result<(), Error> {
match PokemonNumber::try_from(req.number) {
Ok(number) => match repo.delete(number) {
Ok(()) => Ok(()),
Err(DeleteError::NotFound) => Err(Error::NotFound),
Err(DeleteError::Unknown) => Err(Error::Unknown),
},
_ => Err(Error::BadRequest),
}
}
太好了!现在我们只剩下实现存储库了,我们很快就会完成:
pub enum DeleteError {
NotFound,
Unknown,
}
pub trait Repository: Send + Sync {
fn delete(&self, number: PokemonNumber) -> Result<(), DeleteError>;
}
impl Repository for InMemoryRepository {
fn delete(&self, number: PokemonNumber) -> Result<(), DeleteError> {
if self.error {
return Err(DeleteError::Unknown);
}
let mut lock = match self.pokemons.lock() {
Ok(lock) => lock,
_ => return Err(DeleteError::Unknown),
};
let index = match lock.iter().position(|p| p.number == number) {
Some(index) => index,
None => return Err(DeleteError::NotFound),
};
lock.remove(index);
Ok(())
}
}
运行测试:
test it_should_return_a_bad_request_error_when_request_is_invalid ... ok
test it_should_return_a_not_found_error_when_the_repo_does_not_contain_the_pokemon ... ok
test it_should_return_an_unknown_error_when_an_unexpected_error_happens ... ok
test it_should_return_ok_otherwise ... ok
We can now add the new route in our API. Let's add the following to api/mod.rs:
现在我们可以在 API 中添加新路由:
mod delete_pokemon;
// in the router macro
(DELETE) (/{number: u16}) => {
delete_pokemon::serve(repo.clone(), number)
}
enum Status {
Ok,
...
}
impl From<Status> for rouille::Response {
fn from(status: Status) -> Self {
let status_code = match status {
Status::Ok => 200,
...
我在 Status
中补充一个 OK 类型用来表示没有相应体的 200 状态码,现在添加对应的 serve
函数:
use crate::api::Status;
use crate::domain::delete_pokemon;
use crate::repositories::pokemon::Repository;
use rouille;
use std::sync::Arc;
pub fn serve(repo: Arc<dyn Repository>, number: u16) -> rouille::Response {
let req = delete_pokemon::Request { number };
match delete_pokemon::execute(repo, req) {
Ok(()) => rouille::Response::from(Status::Ok),
Err(delete_pokemon::Error::BadRequest) => rouille::Response::from(Status::BadRequest),
Err(delete_pokemon::Error::NotFound) => rouille::Response::from(Status::NotFound),
Err(delete_pokemon::Error::Unknown) => rouille::Response::from(Status::InternalServerError),
}
}
通过使用 HTTP 客户端,现在你应该能够删除宝可梦了 :)
总结
我希望它不会太长......但现在我们的客户很高兴!耶!下一次,我们将为我们的用例实现另一个前端。现在我们只有一个 HTTP API,如果还能有一个 CLI 来管理我们的宝可梦就好了 :)
代码可以在 Github 上查看