Slide 1

Slide 1 text

Rust で DDD DDD のパターンを Rust で表現する ~Value Object編~ kuwana-kb @ CADDi

Slide 2

Slide 2 text

発表者 桑名 泰輔 Twitter: @kuwana_kb_ 仕事: バックエンドエンジニア @ CADDi WebAPI サーバを Rust で書いたり GCP を中⼼にインフラ触ったり 趣味: オンラインゲーム(FPSが特に!) オーディオ(ヘッドホンとカスタムIEM)

Slide 3

Slide 3 text

本⽇のテーマ DDD のパターンを Rust で表現する

Slide 4

Slide 4 text

アジェンダ DDD とは 「ドメイン駆動設計⼊⾨」という書籍 Rust で DDD の概念を表現してみる DDD における Value Object とは 実装をいくつか紹介 まとめ

Slide 5

Slide 5 text

DDD とは Domain-Driven Design(ドメイン駆動設計)のこと エリック・エヴァンスが提唱 アプリケーションの扱う業務領域に焦点をあてた設計⼿法のこと

Slide 6

Slide 6 text

DDD って難しい... 抽象的な概念がたくさん登場する そうした概念を理解した上で実践し、理解を深めていく -> ⾝につけるのが難しい

Slide 7

Slide 7 text

とある本と出会う 「ドメイン駆動設計⼊⾨」(著: 成瀬 允宣⽒) DDD に登場するモデリングとパターンの⽤語うち、パターンを集中的に解説した⼊⾨書 これ 1 冊で DDD の内容を網羅しているわけではないが、抽象的な DDD のパターンを具 体的なコードとして学ぶことができる サンプルコードが豊富。C# で書かれているが、 C# を知らなくても雰囲気で読めるので ⼤丈夫

Slide 8

Slide 8 text

DDD の実装パターンを Rust で書いてみた DDD の理解を深めるためには⾃分で書いてみるのが早い ということ Rust で書いてみました kuwana-kb/ddd-in-rust https://github.com/kuwana-kb/ddd-in-rust コードは「ドメイン駆動設計⼊⾨」のサンプルコードを Rust で書き直したもので す。(⼀部サンプルコードと関係ないものも混じってます) 今回はその⼀部をご紹介

Slide 9

Slide 9 text

Rust で DDD の概念を表現してみる 今回は時間の都合上、 Value Object のみ扱います。

Slide 10

Slide 10 text

DDD における Value Object とは Value Object とは システム固有の値をオブジェクトとして定義したもの ex. ⾦銭や製品番号など プログラミング⾔語には、プリミティブな値が⽤意されているが... 業務領域で使う値をオブジェクトとして定義することで、業務ルールに反した値を 混⼊させない Value Object の性質 値が等しいかどうか、他と⽐較できる(値の等価性) 状態が不変である http://bliki-ja.github.io/ValueObject/

Slide 11

Slide 11 text

まずはシンプルに実装する① ⽒名 をコードで表現してみる ⽒名 は 姓 と 名 で構成される 姓 と 名 はプリミティブな型とする サンプルコード https://github.com/kuwana-kb/ddd-in- rust/blob/master/chapter02_value_object/src/a1_simple_vo.rs

Slide 12

Slide 12 text

まずはシンプルに実装する② #[derive(Clone, Debug)] pub struct FullName { first_name: String, last_name: String, } impl FullName { pub fn new(first_name: &str, last_name: &str) -> Self { Self { first_name: first_name.to_string(), last_name: last_name.to_string(), } } pub fn first_name(&self) -> String { self.first_name.clone() } pub fn last_name(&self) -> String { self.last_name.clone() } } // trait PartialEq は半同値関係の性質を表す // PartialEq を実装することで「== 」演算⼦による⽐較が可能になる impl PartialEq for FullName { fn eq(&self, other: &Self) -> bool { self.first_name() == other.first_name() && self.last_name() == other.last_name() } } impl Eq for FullName {}

Slide 13

Slide 13 text

まずはシンプルに実装する③ #[test] fn test_equality_of_vo() { let taro_tanaka_1 = FullName::new("taro", "tanaka"); let taro_tanaka_2 = FullName::new("taro", "tanaka"); let jiro_suzuki = FullName::new("jiro", "suzuki"); // 値が同じVO の⽐較。⼀致する assert_eq!(taro_tanaka_1, taro_tanaka_2); // equal // 値が異なるVO の⽐較。⼀致しない assert_ne!(taro_tanaka_1, jiro_suzuki); // not equal }

Slide 14

Slide 14 text

まずはシンプルに実装する④ 必要と思われるメソッドや trait を愚直に書いた 値の等価性 PartialEq と Eq を実装することで表現 値の不変性 値を immutable にする(= mut な処理を実装しない)

Slide 15

Slide 15 text

derive を使って実装を省略する① Rust には便利な derive があるのでそれを活⽤する サンプルコード https://github.com/kuwana-kb/ddd-in- rust/blob/master/chapter02_value_object/src/a2_derive_vo.rs

Slide 16

Slide 16 text

derive を使って実装を省略する② #[derive(Clone, Debug, Getters, PartialEq, Eq)] pub struct FullName { first_name: String, last_name: String, } impl FullName { fn new(first_name: &str, last_name: &str) -> Self { Self { first_name: first_name.to_string(), last_name: last_name.to_string(), } } }

Slide 17

Slide 17 text

deriveをを使って実装を省略する③ deriveマクロによって、記述量を削減できた PartialEq , Eq を derive で⾃動実装 getter を Getters という derive マクロで⾃動実装 外部crateには、便利な derive マクロがあります derive-getters フィールドのgetterを⾃動実装 公開したくないフィールドは skip attribute で⾶ばせる derive-new コンストラクタを⾃動実装 strum String , enum に対する Display , FromStr の⾃動実装 derive は便利だが、時と場合によって使い分ける必要がある derive-new コンストラクタにvalidationをはさみたい時は⾃分で実装したりとか

Slide 18

Slide 18 text

型による制約を与える① ビジネスの要求として、 姓 と 名 はアルファベットだけにしたい FullName.first_name , FullName.last_name がアルファベットしか受け付け ないようにする サンプルコード https://github.com/kuwana-kb/ddd-in- rust/blob/master/chapter02_value_object/src/a3_all_vo.rs

Slide 19

Slide 19 text

型による制約を与える② /// ⽒名 // このケースでは、プリミティブだったフィールドに対して、独⾃型(Name) を定義している // 独⾃型に対して制約を与えることで、「その型である = 制約を満たした値である」ことが保証される #[derive(Clone, Debug, Getters, PartialEq, Eq)] pub struct FullName { first_name: Name, last_name: Name, } impl FullName { pub fn new(first_name: Name, last_name: Name) -> Self { Self { first_name, last_name, } } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Name(String); impl FromStr for Name { type Err = String; fn from_str(s: &str) -> Result { let re = Regex::new(r#"^[a-zA-Z]+$"#).unwrap(); if re.is_match(s) { Ok(Name(s.to_string())) } else { bail!(MyError::type_error(" 許可されていない⽂字が使われています")) } } }

Slide 20

Slide 20 text

型による制約を与える③ #[test] fn show_full_name() { let first_name = "taro".parse().unwrap(); let last_name = "tanaka".parse().unwrap(); // この時点でfirst_name, last_name は型のコンストラクタがもつ制約によりアルファベットであることが保証されている let full_name = FullName::new(first_name, last_name); println!("{:?}", full_name); // FullName { first_name: Name("taro"), last_name: Name("tanaka") } } #[test] fn test_parse_name() { let valid_name = "taro".parse::(); let invalid_name_with_num = "taro123".parse::(); let invalid_name_with_jpn = " 太郎".parse::(); assert!(valid_name.is_ok()); // Ok() assert!(invalid_name_with_num.is_err()); // Err() assert!(invalid_name_with_jpn.is_err()); // Err() }

Slide 21

Slide 21 text

型による制約を与える④ ⽒名 を構成する 姓 と 名 に制約を設ける 姓 と 名  をプリミティブな型 String から、独⾃に定義した型 Name に置き換えた 型による制約 Name 型は、 Name::from_str() でregexによる値のチェックをし、不正な値を弾 くような実装 FullName インスタンスを⽣成できる == 制約を満たした値になっていることが保 証される

Slide 22

Slide 22 text

Value Object にふるまいをもたせる① Value Object は値としての意味だけでなく、ふるまいを持つことができる お⾦に対して加算のふるまいをもたせてみる Money というValue Objectを定義 このValue Objectに、 Add trait を実装する 今回は、同じ通貨である場合のみ加算ができる、とする サンプルコード https://github.com/kuwana-kb/ddd-in- rust/blob/master/chapter02_value_object/src/a4_vo_with_behavior.rs

Slide 23

Slide 23 text

Value Object にふるまいをもたせる② // 振る舞いを持つVO // 具体的には通貨単位が⼀致した場合に限り加算が可能 // // このケースでは通貨単位をフィールドの⼀部として定義している // 通貨単位をフィールドではなく、型として表現するケースは`a5_vo_with_phantom` 参照 #[derive(Clone, Debug, new, Eq, PartialEq)] struct Money { amount: Decimal, currency: String, } // Add trait は「+ 」演算⼦による加算を表現する impl Add for Money { type Output = Money; fn add(self, other: Money) -> Self::Output { // 通貨単位のチェック // 通貨単位が⼀致しない場合はpanic を起こす // trait のシグネチャ上、Result 型として返せないのでこれは仕⽅ないはず... // その意味で、通貨単位を型として表現することでコンパイル時に検査できる⽅が嬉しいと思われる if self.currency != other.currency { panic!("Invalid currency") } let new_amount = self.amount + other.amount; Money::new(new_amount, self.currency) } }

Slide 24

Slide 24 text

Value Object にふるまいをもたせる③ お⾦の持つ性質として、加算のふるまいを実装した 同じ通貨単位であれば、加算ができるように この実装の問題点 通貨単位はフィールドの⼀部として保持している Add trait の制約上、返り値を Result 型にはできないため、異なる通貨で⾜し算 をした時のエラーハンドリングは panic!() せざるを得ない

Slide 25

Slide 25 text

幽霊型を⽤いて Value Object の型を区別する① 先程の実装だと、異なる通貨単位同⼠の⾜し算をすると panic してしまう 型として通貨を表現すれば、 panic させずに、型の不⼀致によるエラーにできる ここで幽霊型を⽤いる

Slide 26

Slide 26 text

幽霊型を⽤いて Value Object の型を区別する② // 振る舞いを持つVO // 具体的には通貨単位が⼀致した場合に限り加算が可能 // // このケースでは通貨単位を型として表現している // Money のT で通貨単位を表すようにする // ここで嬉しいのは、誤った通貨単位同⼠の加算をコンパイル時に検査できること // T はただのラベルとして扱いたいだけだが消費しないと怒られるので、std::marker::PhantomData を⽤いる // 参考: https://keens.github.io/blog/2018/12/15/rustdetsuyomenikatawotsukerupart_1__new_type_pattern/ #[derive(Clone, Debug, PartialEq, Eq)] pub struct Money { amount: Decimal, currency: PhantomData, } impl Money { fn new(amount: Decimal) -> Self { Self { amount, currency: PhantomData::, } } } impl Add for Money { type Output = Money; fn add(self, other: Money) -> Self::Output { Self::new(self.amount + other.amount) } } #[derive(Clone, Debug, PartialEq, Eq)] pub enum JPY {} #[derive(Clone, Debug, PartialEq, Eq)] pub enum USD {}

Slide 27

Slide 27 text

幽霊型を⽤いて Value Object の型を区別する③ 以下のような使い⽅をする #[test] fn test_phantom_money() { let jpy_1 = Money::::new(Decimal::new(1, 0)); let jpy_2 = Money::::new(Decimal::new(2, 0)); let _usd = Money::::new(Decimal::new(3, 0)); let result = jpy_1 + jpy_2; // コンパイルOk assert_eq!(result, Money::::new(Decimal::new(3, 0))); // let cannot_compile = jpy_1 + usd; // コンパイルエラー }

Slide 28

Slide 28 text

幽霊型を⽤いて Value Object の型を区別する④ 型として定義することで、コンパイル時にエラーに気づくことができる 異なる通貨 = 異なる型となるため、異なる通貨の加算をコンパイル時点でエラー検 出できる なぜ幽霊型 同じ構造の型を複数実装しなくて済む 例えば、 MoneyUsd , MoneyJpy といった形で通貨ごとに型を分けることもで きるが、同じような実装が通貨分できるのはよろしくない... 幽霊型によって、通貨をラベルのように型として表現することができる

Slide 29

Slide 29 text

まとめ Rust の機能によって、Value Object を様々な⽅法で表現できる derive マクロ、 trait によるふるまいの実装、型システムによるコンパイル時チェッ クなど 他の⾔語で書かれたコードを移植すると、⾔語の特徴がコードに表れておもしろい 「ドメイン駆動設計⼊⾨」は良書。ぜひ⼀緒に DDD に⼊⾨しましょう!

Slide 30

Slide 30 text

参考⽂献 ドメイン駆動設計⼊⾨ ボトムアップでわかる!ドメイン駆動設計の基本 | Naruse Masanobu https://www.amazon.co.jp/dp/B082WXZVPC/ バリューオブジェクト | Martin Fowler's Bliki (ja) http://bliki-ja.github.io/Value Object/ DDD Propaganda | Naruse Masanobu https://speakerdeck.com/nrslib/ddd-propaganda Rust で強めに型をつける Part 1: New Type Pattern | κeen の Happy Hacκing Blog https://keens.github.io/blog/2018/12/15/rustdetsuyomenikatawotsukerupart_1__ new_type_pattern/

Slide 31

Slide 31 text

ご清聴いただきありがとうございました!