はじめに
LIFULLその2 Advent Calendar 2020 – Qiitaの12日目の記事になります。
RustでClean ArchitectureでWeb Applicationを作ろうとしましたが、投稿に遅刻しているので(13日に書いてる)妥協してValueObjectを使うにあたって調査したことを記事にしようと思います汗
記事を早く作るため箇条書きを多用してます。
先行実装
-
- 巨人の肩に乗るため、すでにこれについての先行実装を調べました。
-
- あまりヒットするものはなかったものの、以下の記事が非常に良かったです。
DDDのパターンをRustで表現する ~ Value Object編 ~ – CADDi Tech Blog
ddd-in-rust/chapter02_value_object/src at master · kuwana-kb/ddd-in-rust
実装パターン
-
- 以下の部分について考えてみたことを列挙してみます。
- 例としてはUserを題材にします。
何も考えずとりあえず箱をつくる
pub struct User {
name: String,
mail: String,
}
impl User {
pub fn new(name: &str, mail: &str) -> Self {
return Self { name: name.to_string(), mail: mail.to_string() };
}
}
#[test]
fn new_test() {
let user = User::new("ユーザ名", "hoge@example.com");
assert!(user.name == "ユーザ名");
assert!(user.mail == "hoge@example.com");
}
classはないのでstructとimplをつかっています。
また(1)structのほうではStringで受け取って、(2)newでは&strで受け取っています。
(1)にしているのは、文字列の操作でStringにのほうがなんやかんや柔軟に使えるから、という理由です。
(2)にしているのはnewをする際に毎回”text”.to_string()と書くのがダルいからです。
問題点
-
- このままだと、validationをどう書けばいいかわからないです。
-
- emailが不正な値だったらnewの中でその処理書きたいです。ただし、書いたとしてその中でpanicを起こさせることしかできないです。
- 正しい値ではなかったら、何かしらの手段でApplicationにエラーを渡したいです。
newの返り値にOptionをつかう
pub struct User {
name: String,
mail: String,
}
impl User {
pub fn new(name: &str, mail: &str) -> Option<Self> {
// from https://qiita.com/sakuro/items/1eaa307609ceaaf51123
let re = Regex::new(r#"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$"#)
.unwrap();
// validation for params
if name.len() <= 0 || name.len() >= 30 || !re.is_match(mail) {
return None;
}
return Some(Self { name: name.to_string(), mail: mail.to_string() });
}
}
#[test]
fn new_test() {
let op_user = User::new("ユーザ名", "hoge@example.com");
assert!(op_user != None);
let user = op_user.unwrap();
assert!(user.name == "ユーザ名");
assert!(user.mail == "hoge@example.com");
}
newがNoneかSome(T)で返却されることによってValueObject生成の成否を検知できるようになった。
生成後、判定を正しく行う場合、matchを使うか、unwrapやunwrap_orなどを使うか、場合によっては?を使うのもありですね
std::option::Option – Rust
?によるOptionのアンパック – Rust By Example 日本語版
気になる点
-
- 毎回生成が正しいか、生成が成功したか判定をしないといけないため、ValueObjectを大量に生成する場合、のいいアイディアがないです。
trait ValueObjectを作っていい感じにまとめて生成とか考えようとしましたが、知識不足も相まってパンクしました。
ただし必要な処理だと思うので、大きなデメリットとは思いません。
Noneが渡されたとき、何が間違ってNoneが返されたのかわからない
newの返り値にResult<Self, E>をつかう
use regex::Regex;
pub struct MyError {
message: String,
}
impl MyError {
pub fn new(name: &str) -> Self {
return Self { message: name.to_string() };
}
}
pub struct MyUser {
name: String,
mail: String,
}
impl MyUser {
// MyErrorは自分で定義したものと読み替えてください
pub fn new(name: &str, mail: &str) -> Result<Self, MyError> {
// from https://qiita.com/sakuro/items/1eaa307609ceaaf51123
let re = Regex::new(r#"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$"#)
.unwrap();
// validation for params
if name.len() <= 0 || name.len() >= 30 {
return Err(MyError::new("名前は0文字以上30文字以下である必要があります。"));
}
if !re.is_match(mail) {
return Err(MyError::new("emailが不正なフォーマットです"));
}
return Ok(Self {
name: name.to_string(),
mail: mail.to_string(),
});
}
}
#[test]
fn test() {
let usr1 = MyUser::new("ユーザ名", "hoge@example.com");
assert!(usr1.is_ok());
let user = usr1.ok().unwrap();
assert_eq!(user.name, "ユーザ名");
assert_eq!(user.mail, "hoge@example.com");
let usr2 = MyUser::new("ユーザ名", "hoge/example.com");
assert!(usr2.is_err());
if let Err(err) = usr2 {
assert_eq!(err.message, "emailが不正なフォーマットです")
}
}
-
- 今回は使ってないですが、errorハンドリング周りはは現在thiserrorでErrorを自作して、anyhowで早期リターンをかくと良いそうです。
Rustのエラー処理 – Qiita
dtolnay/thiserror: derive(Error) for struct and enum error types
dtolnay/anyhow: Flexible concrete Error type built on std: :Error
実際のコード例
ddd-in-rust/lib.rs at master · kuwana-kb/ddd-in-rust
https://github.com/kuwana-kb/ddd-in-rust/blob/master/chapter02_value_object/src/a3_all_vo.rs#L38
気になる点
Optionを使う場合も同様に書きましたが、大量にValueObjectを生成する際に工夫しないとコードがきたなくなりそうです。
エラーメッセージがしっかり取れるところはいいですね。
まとめ
-
- 少ないですが、一旦ここらへん終わります。
-
- 私見ですが、今列挙した中ではエラー処理をしっかり書くならResultで、成否のエラーをひとまとめに扱う、とかならOptionでいいのかな、とおもったりしました。
ただし、Rustのお作法として、もっといい書き方ありそうな気もするので自信はないです。
サクッと作るならOption、しっかりつくるならResultですかね。
余談ですが、Rustの標準に用意されている関数が非常に多くて驚きました。
ドキュメントに記載されているまだ試していない関数などもあるので、色々試行錯誤したいです。
owari