Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RustでDDD

 RustでDDD

Rust で DDD
DDD のパターンを Rust で表現する ~Value Object編~

スライドで扱うソースコード:
https://github.com/kuwana-kb/ddd-in-rust/tree/master/chapter02_value_object/src

kuwanakb

April 28, 2020
Tweet

More Decks by kuwanakb

Other Decks in Programming

Transcript

  1. 発表者 桑名 泰輔 Twitter: @kuwana_kb_ 仕事: バックエンドエンジニア @ CADDi WebAPI

    サーバを Rust で書いたり GCP を中⼼にインフラ触ったり 趣味: オンラインゲーム(FPSが特に!) オーディオ(ヘッドホンとカスタムIEM)
  2. とある本と出会う 「ドメイン駆動設計⼊⾨」(著: 成瀬 允宣⽒) DDD に登場するモデリングとパターンの⽤語うち、パターンを集中的に解説した⼊⾨書 これ 1 冊で DDD

    の内容を網羅しているわけではないが、抽象的な DDD のパターンを具 体的なコードとして学ぶことができる サンプルコードが豊富。C# で書かれているが、 C# を知らなくても雰囲気で読めるので ⼤丈夫
  3. DDD の実装パターンを Rust で書いてみた DDD の理解を深めるためには⾃分で書いてみるのが早い ということ Rust で書いてみました kuwana-kb/ddd-in-rust

    https://github.com/kuwana-kb/ddd-in-rust コードは「ドメイン駆動設計⼊⾨」のサンプルコードを Rust で書き直したもので す。(⼀部サンプルコードと関係ないものも混じってます) 今回はその⼀部をご紹介
  4. DDD における Value Object とは Value Object とは システム固有の値をオブジェクトとして定義したもの ex.

    ⾦銭や製品番号など プログラミング⾔語には、プリミティブな値が⽤意されているが... 業務領域で使う値をオブジェクトとして定義することで、業務ルールに反した値を 混⼊させない Value Object の性質 値が等しいかどうか、他と⽐較できる(値の等価性) 状態が不変である http://bliki-ja.github.io/ValueObject/
  5. まずはシンプルに実装する① ⽒名 をコードで表現してみる ⽒名 は 姓 と 名 で構成される 姓

    と 名 はプリミティブな型とする サンプルコード https://github.com/kuwana-kb/ddd-in- rust/blob/master/chapter02_value_object/src/a1_simple_vo.rs
  6. まずはシンプルに実装する② #[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 {}
  7. まずはシンプルに実装する③ #[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 }
  8. 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(), } } }
  9. 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をはさみたい時は⾃分で実装したりとか
  10. 型による制約を与える① ビジネスの要求として、 姓 と 名 はアルファベットだけにしたい FullName.first_name , FullName.last_name がアルファベットしか受け付け

    ないようにする サンプルコード https://github.com/kuwana-kb/ddd-in- rust/blob/master/chapter02_value_object/src/a3_all_vo.rs
  11. 型による制約を与える② /// ⽒名 // このケースでは、プリミティブだったフィールドに対して、独⾃型(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<Self, Self::Err> { let re = Regex::new(r#"^[a-zA-Z]+$"#).unwrap(); if re.is_match(s) { Ok(Name(s.to_string())) } else { bail!(MyError::type_error(" 許可されていない⽂字が使われています")) } } }
  12. 型による制約を与える③ #[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::<Name>(); let invalid_name_with_num = "taro123".parse::<Name>(); let invalid_name_with_jpn = " 太郎".parse::<Name>(); assert!(valid_name.is_ok()); // Ok() assert!(invalid_name_with_num.is_err()); // Err() assert!(invalid_name_with_jpn.is_err()); // Err() }
  13. 型による制約を与える④ ⽒名 を構成する 姓 と 名 に制約を設ける 姓 と 名

     をプリミティブな型 String から、独⾃に定義した型 Name に置き換えた 型による制約 Name 型は、 Name::from_str() でregexによる値のチェックをし、不正な値を弾 くような実装 FullName インスタンスを⽣成できる == 制約を満たした値になっていることが保 証される
  14. 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
  15. 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) } }
  16. 幽霊型を⽤いて Value Object の型を区別する② // 振る舞いを持つVO // 具体的には通貨単位が⼀致した場合に限り加算が可能 // //

    このケースでは通貨単位を型として表現している // Money<T> の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<T> { amount: Decimal, currency: PhantomData<T>, } impl<T> Money<T> { fn new(amount: Decimal) -> Self { Self { amount, currency: PhantomData::<T>, } } } impl<T> Add for Money<T> { type Output = Money<T>; fn add(self, other: Money<T>) -> Self::Output { Self::new(self.amount + other.amount) } } #[derive(Clone, Debug, PartialEq, Eq)] pub enum JPY {} #[derive(Clone, Debug, PartialEq, Eq)] pub enum USD {}
  17. 幽霊型を⽤いて Value Object の型を区別する③ 以下のような使い⽅をする #[test] fn test_phantom_money() { let

    jpy_1 = Money::<JPY>::new(Decimal::new(1, 0)); let jpy_2 = Money::<JPY>::new(Decimal::new(2, 0)); let _usd = Money::<USD>::new(Decimal::new(3, 0)); let result = jpy_1 + jpy_2; // コンパイルOk assert_eq!(result, Money::<JPY>::new(Decimal::new(3, 0))); // let cannot_compile = jpy_1 + usd; // コンパイルエラー }
  18. 幽霊型を⽤いて Value Object の型を区別する④ 型として定義することで、コンパイル時にエラーに気づくことができる 異なる通貨 = 異なる型となるため、異なる通貨の加算をコンパイル時点でエラー検 出できる なぜ幽霊型

    同じ構造の型を複数実装しなくて済む 例えば、 MoneyUsd , MoneyJpy といった形で通貨ごとに型を分けることもで きるが、同じような実装が通貨分できるのはよろしくない... 幽霊型によって、通貨をラベルのように型として表現することができる
  19. まとめ Rust の機能によって、Value Object を様々な⽅法で表現できる derive マクロ、 trait によるふるまいの実装、型システムによるコンパイル時チェッ クなど

    他の⾔語で書かれたコードを移植すると、⾔語の特徴がコードに表れておもしろい 「ドメイン駆動設計⼊⾨」は良書。ぜひ⼀緒に DDD に⼊⾨しましょう!
  20. 参考⽂献 ドメイン駆動設計⼊⾨ ボトムアップでわかる!ドメイン駆動設計の基本 | 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/