はじめに
LIFULLその2 Advent Calendar 2020 - Qiitaの12日目の記事になります。
RustでClean ArchitectureでWeb Applicationを作ろうとしましたが、投稿に遅刻しているので(13日に書いてる)妥協してValueObjectを使うにあたって調査したことを記事にしようと思います汗
記事を早く作るため箇条書きを多用してます。
先行実装
- 巨人の肩に乗るため、すでにこれについての先行実装を調べました。
- あまりヒットするものはなかったものの、以下の記事が非常に良かったです。
- DDDのパターンをRustで表現する ~ Value Object編 ~ - CADDi Tech Blog
実装パターン
- 以下の部分について考えてみたことを列挙してみます。
- 例としては
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()
と書くのがダルいからです。
- (1)にしているのは、文字列の操作で
問題点
- このままだと、validationをどう書けばいいかわからないです。
- emailが不正な値だったら
new
の中でその処理書きたいです。ただし、書いたとしてその中でpanic
を起こさせることしかできないです。 - 正しい値ではなかったら、何かしらの手段でApplicationにエラーを渡したいです。
newの返り値にOption<Self>
をつかう
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
などを使うか、場合によっては?
を使うのもありですね
気になる点
- 毎回生成が正しいか、生成が成功したか判定をしないといけないため、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
で早期リターンをかくと良いそうです。
気になる点
-
Option
を使う場合も同様に書きましたが、大量にValueObjectを生成する際に工夫しないとコードがきたなくなりそうです。 - エラーメッセージがしっかり取れるところはいいですね。
まとめ
- 少ないですが、一旦ここらへん終わります。
- 私見ですが、今列挙した中ではエラー処理をしっかり書くなら
Result
で、成否のエラーをひとまとめに扱う、とかならOption
でいいのかな、とおもったりしました。- ただし、Rustのお作法として、もっといい書き方ありそうな気もするので自信はないです。
- サクッと作るなら
Option
、しっかりつくるならResult
ですかね。
- 余談ですが、Rustの標準に用意されている関数が非常に多くて驚きました。
- ドキュメントに記載されているまだ試していない関数などもあるので、色々試行錯誤したいです。
owari